# Licensed under a 3-clause BSD style license - see LICENSE.rst
from itertools import product

import pytest

import numpy as np
from numpy.testing import assert_allclose, assert_equal

from astropy import units as u
from astropy.coordinates import Longitude, Latitude

from ..core import (nside_to_pixel_area, nside_to_pixel_resolution, pixel_resolution_to_nside,
                    nside_to_npix, npix_to_nside, healpix_to_lonlat,
                    lonlat_to_healpix, interpolate_bilinear_lonlat,
                    neighbours, healpix_cone_search, boundaries_lonlat,
                    level_to_nside, nside_to_level,
                    nested_to_ring, ring_to_nested,
                    level_ipix_to_uniq, uniq_to_level_ipix,
                    bilinear_interpolation_weights)


def test_level_to_nside():
    assert level_to_nside(5) == 2 ** 5
    with pytest.raises(ValueError) as exc:
        level_to_nside(-1)
    assert exc.value.args[0] == 'level must be positive'


def test_nside_to_level():
    assert nside_to_level(1024) == 10
    with pytest.raises(ValueError) as exc:
        nside_to_level(511)
    assert exc.value.args[0] == 'nside must be a power of two'


def test_level_ipix_to_uniq():
    assert 11 + 4*4**0 == level_ipix_to_uniq(0, 11)
    assert 62540 + 4*4**15 == level_ipix_to_uniq(15, 62540)
    with pytest.raises(ValueError) as exc:
        level_ipix_to_uniq(1, 49)
    assert exc.value.args[0] == 'ipix for a specific level must be inferior to npix'


@pytest.mark.parametrize("level", [
    0, 5, 10, 15, 20, 22, 25, 26, 27, 28, 29
])
def test_uniq_to_level_ipix(level):
    npix = 3 << 2*(level + 1)
    # Take 10 pixel indices between 0 and npix - 1
    size = 10

    ipix = np.arange(size, dtype=np.int64) * (npix // size)
    level = np.ones(size) * level

    level_res, ipix_res = uniq_to_level_ipix(level_ipix_to_uniq(level, ipix))
    assert np.all(level_res == level) & np.all(ipix_res == ipix)


def test_nside_to_pixel_area():
    resolution = nside_to_pixel_area(256)
    assert_allclose(resolution.value, 1.5978966540475428e-05)
    assert resolution.unit == u.sr


def test_nside_to_pixel_resolution():
    resolution = nside_to_pixel_resolution(256)
    assert_allclose(resolution.value, 13.741945647269624)
    assert resolution.unit == u.arcmin


def test_pixel_resolution_to_nside():

    # Check the different rounding options
    nside = pixel_resolution_to_nside(13 * u.arcmin, round='nearest')
    assert nside == 256

    nside = pixel_resolution_to_nside(13 * u.arcmin, round='up')
    assert nside == 512

    nside = pixel_resolution_to_nside(13 * u.arcmin, round='down')
    assert nside == 256

    # Check that it works with arrays
    nside = pixel_resolution_to_nside([1e3, 10, 1e-3] * u.deg, round='nearest')
    assert_equal(nside, [1, 8, 65536])

    with pytest.raises(ValueError) as exc:
        pixel_resolution_to_nside(13 * u.arcmin, round='peaches')
    assert exc.value.args[0] == "Invalid value for round: 'peaches'"

    with pytest.raises(AttributeError) as exc:
        pixel_resolution_to_nside(13)
    assert exc.value.args[0] == "'int' object has no attribute 'to'"


def test_nside_to_npix():
    npix = nside_to_npix(4)
    assert npix == 192

    npix = nside_to_npix([4, 4])
    assert_equal(npix, 192)

    with pytest.raises(ValueError) as exc:
        nside_to_npix(15)
    assert exc.value.args[0] == 'nside must be a power of two'


def test_npix_to_nside():
    nside = npix_to_nside(192)
    assert nside == 4

    nside = npix_to_nside([192, 192])
    assert_equal(nside, 4)

    with pytest.raises(ValueError) as exc:
        npix_to_nside(7)
    assert exc.value.args[0] == 'Number of pixels must be divisible by 12'

    with pytest.raises(ValueError) as exc:
        npix_to_nside(12 * 8 * 9)
    assert exc.value.args[0] == 'Number of pixels is not of the form 12 * nside ** 2'


# For the following tests, the numerical accuracy of this function is already
# tested in test_cython_api.py, so we focus here on functionality specific to
# the Python functions.


@pytest.mark.parametrize('order', ['nested', 'ring'])
def test_healpix_to_lonlat(order):
    lon, lat = healpix_to_lonlat([1, 2, 3], 4, order=order)

    assert isinstance(lon, Longitude)
    assert isinstance(lat, Latitude)

    index = lonlat_to_healpix(lon, lat, 4, order=order)

    assert_equal(index, [1, 2, 3])

    lon, lat = healpix_to_lonlat([1, 2, 3], 4,
                                 dx=[0.1, 0.2, 0.3],
                                 dy=[0.5, 0.4, 0.7], order=order)

    assert isinstance(lon, Longitude)
    assert isinstance(lat, Latitude)

    index, dx, dy = lonlat_to_healpix(lon, lat, 4, order=order, return_offsets=True)

    assert_equal(index, [1, 2, 3])
    assert_allclose(dx, [0.1, 0.2, 0.3])
    assert_allclose(dy, [0.5, 0.4, 0.7])


def test_healpix_to_lonlat_invalid():
    dx = [0.1, 0.4, 0.9]
    dy = [0.4, 0.3, 0.2]

    with pytest.warns(RuntimeWarning, match='invalid value'):
        lon, lat = healpix_to_lonlat([-1, 2, 3], 4)

    with pytest.warns(RuntimeWarning, match='invalid value'):
        lon, lat = healpix_to_lonlat([192, 2, 3], 4)

    with pytest.raises(ValueError) as exc:
        lon, lat = healpix_to_lonlat([1, 2, 3], 5)
    assert exc.value.args[0] == 'nside must be a power of two'

    with pytest.raises(ValueError) as exc:
        lon, lat = healpix_to_lonlat([1, 2, 3], 4, order='banana')
    assert exc.value.args[0] == "order must be 'nested' or 'ring'"

    with pytest.raises(ValueError) as exc:
        lon, lat = healpix_to_lonlat([1, 2, 3], 4, dx=[-0.1, 0.4, 0.5], dy=dy)
    assert exc.value.args[0] == 'dx must be in the range [0:1]'

    with pytest.raises(ValueError) as exc:
        lon, lat = healpix_to_lonlat([1, 2, 3], 4, dx=dx, dy=[-0.1, 0.4, 0.5])
    assert exc.value.args[0] == 'dy must be in the range [0:1]'


def test_healpix_to_lonlat_shape():
    lon, lat = healpix_to_lonlat(2, 8)
    assert lon.isscalar and lat.isscalar

    lon, lat = healpix_to_lonlat([[1, 2, 3], [3, 4, 4]], 8)
    assert lon.shape == (2, 3) and lat.shape == (2, 3)

    lon, lat = healpix_to_lonlat([[1], [2], [3]], nside=8, dx=0.2, dy=[[0.1, 0.3]])
    assert lon.shape == (3, 2) and lat.shape == (3, 2)


def test_lonlat_to_healpix_shape():
    healpix_index = lonlat_to_healpix(2 * u.deg, 3 * u.deg, 8)
    assert np.can_cast(healpix_index, np.int64)

    lon, lat = np.ones((2, 4)) * u.deg, np.zeros((2, 4)) * u.deg
    healpix_index = lonlat_to_healpix(lon, lat, 8)
    assert healpix_index.shape == (2, 4)

    healpix_index, dx, dy = lonlat_to_healpix(2 * u.deg, 3 * u.deg, 8, return_offsets=True)
    assert np.can_cast(healpix_index, np.int64)
    assert isinstance(dx, float)
    assert isinstance(dy, float)

    lon, lat = np.ones((2, 4)) * u.deg, np.zeros((2, 4)) * u.deg
    healpix_index, dx, dy = lonlat_to_healpix(lon, lat, 8, return_offsets=True)
    assert healpix_index.shape == (2, 4)
    assert dx.shape == (2, 4)
    assert dy.shape == (2, 4)


@pytest.mark.parametrize('function', [nested_to_ring, ring_to_nested])
def test_nested_ring_shape(function):
    index = function(1, 8)
    assert np.can_cast(index, np.int64)

    index = function([[1, 2, 3], [2, 3, 4]], 8)
    assert index.shape == (2, 3)


@pytest.mark.parametrize('order', ['nested', 'ring'])
def test_bilinear_interpolation_weights(order):

    indices, weights = bilinear_interpolation_weights(100 * u.deg, 10 * u.deg,
                                                      nside=4, order=order)
    if order == 'nested':
        indices = nested_to_ring(indices, nside=4)
    assert_equal(indices, [76, 77, 60, 59])
    assert_allclose(weights, [0.532723, 0.426179, 0.038815, 0.002283], atol=1e-6)


def test_bilinear_interpolation_weights_invalid():
    with pytest.raises(ValueError) as exc:
        bilinear_interpolation_weights(1 * u.deg, 2 * u.deg, nside=5)
    assert exc.value.args[0] == 'nside must be a power of two'

    with pytest.raises(ValueError) as exc:
        bilinear_interpolation_weights(3 * u.deg, 4 * u.deg,
                                       nside=4, order='banana')
    assert exc.value.args[0] == "order must be 'nested' or 'ring'"


def test_bilinear_interpolation_weights_shape():

    indices, weights = bilinear_interpolation_weights(3 * u.deg, 4 * u.deg, nside=8)
    assert indices.shape == (4,)
    assert weights.shape == (4,)

    indices, weights = bilinear_interpolation_weights([[1, 2, 3], [2, 3, 4]] * u.deg,
                                                      [[1, 2, 3], [2, 3, 4]] * u.deg, nside=8)
    assert indices.shape == (4, 2, 3)
    assert weights.shape == (4, 2, 3)


@pytest.mark.parametrize('order', ['nested', 'ring'])
def test_interpolate_bilinear_lonlat(order):
    values = np.ones(192) * 3
    result = interpolate_bilinear_lonlat([1, 3, 4] * u.deg, [3, 2, 6] * u.deg,
                                         values, order=order)
    assert_allclose(result, [3, 3, 3])


def test_interpolate_bilinear_invalid():
    values = np.ones(133)
    with pytest.raises(ValueError) as exc:
        interpolate_bilinear_lonlat([1, 3, 4] * u.deg, [3, 2, 6] * u.deg, values)
    assert exc.value.args[0] == 'Number of pixels must be divisible by 12'

    values = np.ones(192)
    with pytest.raises(ValueError) as exc:
        interpolate_bilinear_lonlat([1, 3, 4] * u.deg, [3, 2, 6] * u.deg,
                                    values, order='banana')
    assert exc.value.args[0] == "order must be 'nested' or 'ring'"

    result = interpolate_bilinear_lonlat([0, np.nan] * u.deg,
                                         [0, np.nan] * u.deg, values,
                                         order='nested')
    assert result.shape == (2,)
    assert result[0] == 1
    assert np.isnan(result[1])


def test_interpolate_bilinear_lonlat_shape():

    values = np.ones(192) * 3

    result = interpolate_bilinear_lonlat(3 * u.deg, 4 * u.deg, values)
    assert isinstance(result, float)

    result = interpolate_bilinear_lonlat([[1, 2, 3], [2, 3, 4]] * u.deg,
                                         [[1, 2, 3], [2, 3, 4]] * u.deg, values)
    assert result.shape == (2, 3)

    values = np.ones((192, 50)) * 3

    lon = np.ones((3, 6, 5)) * u.deg
    lat = np.ones((3, 6, 5)) * u.deg

    result = interpolate_bilinear_lonlat(lon, lat, values)
    assert result.shape == (3, 6, 5, 50)


@pytest.mark.parametrize('order', ['nested', 'ring'])
def test_neighbours(order):
    neigh = neighbours([1, 2, 3], 4, order=order)

    if order == 'nested':
        expected = [[0, 71, 2],
                    [2, 77, 8],
                    [3, 8, 9],
                    [6, 9, 12],
                    [4, 3, 6],
                    [94, 1, 4],
                    [91, 0, 1],
                    [90, 69, 0]]
    else:

        expected = [[6, 8, 10],
                    [5, 7, 9],
                    [0, 1, 2],
                    [3, 0, 1],
                    [2, 3, 0],
                    [8, 10, 4],
                    [7, 9, 11],
                    [16, 19, 22]]

    assert_equal(neigh, expected)


def test_neighbours_invalid():
    with pytest.warns(RuntimeWarning, match='invalid value'):
        neighbours([-1, 2, 3], 4)

    with pytest.warns(RuntimeWarning, match='invalid value'):
        neighbours([192, 2, 3], 4)

    with pytest.raises(ValueError) as exc:
        neighbours([1, 2, 3], 5)
    assert exc.value.args[0] == 'nside must be a power of two'

    with pytest.raises(ValueError) as exc:
        neighbours([1, 2, 3], 4, order='banana')
    assert exc.value.args[0] == "order must be 'nested' or 'ring'"


def test_neighbours_shape():
    neigh = neighbours([[1, 2, 3], [2, 3, 4]], 4)
    assert neigh.shape == (8, 2, 3)


@pytest.mark.parametrize('order', ['nested', 'ring'])
def test_healpix_cone_search(order):
    indices = healpix_cone_search(10 * u.deg, 20 * u.deg, 1 * u.deg,
                                  nside=256, order=order)

    assert len(indices) == 80


@pytest.mark.parametrize(('step', 'order'), product([1, 4, 10], ['nested', 'ring']))
def test_boundaries_lonlat(step, order):
    lon, lat = boundaries_lonlat([10, 20, 30], step, 256, order=order)
    assert lon.shape == (3, 4 * step)
    assert lat.shape == (3, 4 * step)
