# Authors: The MNE-Python contributors.
# License: BSD-3-Clause
# Copyright the MNE-Python contributors.

from pathlib import Path

import numpy as np
import pytest

from mne.datasets.testing import data_path, requires_testing_data

from ..calibration import Calibration, read_eyelink_calibration

# for test_read_eylink_calibration
testing_path = data_path(download=False)
fname = testing_path / "eyetrack" / "test_eyelink.asc"

# for test_create_calibration
POSITIONS = np.array([[115.0, 540.0], [960.0, 540.0], [1804.0, 540.0]])
OFFSETS = np.array([0.42, 0.23, 0.17])
GAZES = np.array([[101.5, 554.8], [9.9, -4.1], [1795.9, 539.0]])

EXPECTED_REPR = (
    "Calibration |\n"
    "  onset: 0 seconds\n"
    "  model: H3\n"
    "  eye: right\n"
    "  average error: 0.5 degrees\n"
    "  max error: 1.0 degrees\n"
    "  screen size: (0.531, 0.298) meters\n"
    "  screen distance: 0.065 meters\n"
    "  screen resolution: (1920, 1080) pixels\n"
)


@pytest.mark.parametrize(
    (
        "onset, model, eye, avg_error, max_error, positions, offsets, gaze,"
        " screen_size, screen_distance, screen_resolution"
    ),
    [
        (
            0,
            "H3",
            "right",
            0.5,
            1.0,
            POSITIONS,
            OFFSETS,
            GAZES,
            (0.531, 0.298),
            0.065,
            (1920, 1080),
        ),
        (None, None, None, None, None, None, None, None, None, None, None),
    ],
)
def test_create_calibration(
    onset,
    model,
    eye,
    avg_error,
    max_error,
    positions,
    offsets,
    gaze,
    screen_size,
    screen_distance,
    screen_resolution,
):
    """Test creating a Calibration object."""
    kwargs = dict(
        onset=onset,
        model=model,
        eye=eye,
        avg_error=avg_error,
        max_error=max_error,
        positions=positions,
        offsets=offsets,
        gaze=gaze,
        screen_size=screen_size,
        screen_distance=screen_distance,
        screen_resolution=screen_resolution,
    )
    cal = Calibration(**kwargs)
    assert cal["onset"] == onset
    assert cal["model"] == model
    assert cal["eye"] == eye
    assert cal["avg_error"] == avg_error
    assert cal["max_error"] == max_error
    if positions is not None:
        assert isinstance(cal["positions"], np.ndarray)
        assert np.array_equal(cal["positions"], np.array(POSITIONS))
    else:
        assert cal["positions"] is None
    if offsets is not None:
        assert isinstance(cal["offsets"], np.ndarray)
        assert np.array_equal(cal["offsets"], np.array(OFFSETS))
    if gaze is not None:
        assert isinstance(cal["gaze"], np.ndarray)
        assert np.array_equal(cal["gaze"], np.array(GAZES))
    assert cal["screen_size"] == screen_size
    assert cal["screen_distance"] == screen_distance
    assert cal["screen_resolution"] == screen_resolution
    # test copy method
    copied_obj = cal.copy()
    # Check if the copied object is an instance of Calibration
    assert isinstance(copied_obj, Calibration)
    # Check if the an attribute of the copied object is equal to the original object
    assert copied_obj["onset"] == cal["onset"]
    # Modify the copied object and check if it is independent from the original object
    copied_obj["onset"] = 20
    assert copied_obj["onset"] != cal["onset"]
    # test __repr__
    if cal["onset"] is not None:
        assert repr(cal) == EXPECTED_REPR  # test __repr__


@requires_testing_data
@pytest.mark.parametrize("fname", [(fname)])
def test_read_calibration(fname):
    """Test reading calibration data from an eyelink asc file."""
    calibrations = read_eyelink_calibration(fname)
    # These numbers were pulled from the file and confirmed.
    POSITIONS_L = (
        [960, 540],
        [960, 92],
        [960, 987],
        [115, 540],
        [1804, 540],
        [216, 145],
        [1703, 145],
        [216, 934],
        [1703, 934],
        [537, 316],
        [1382, 316],
        [537, 763],
        [1382, 763],
    )

    DIFF_L = (
        [9.9, -4.1],
        [-7.8, 16.0],
        [-1.9, -14.2],
        [13.5, -14.8],
        [8.1, 1.0],
        [-7.0, -15.4],
        [-10.1, -1.4],
        [-0.3, 6.9],
        [-32.3, -28.1],
        [8.2, 7.6],
        [9.6, 2.1],
        [-10.6, -2.0],
        [-11.8, 8.4],
    )
    GAZE_L = np.array(POSITIONS_L) + np.array(DIFF_L)

    POSITIONS_R = (
        [960, 540],
        [960, 92],
        [960, 987],
        [115, 540],
        [1804, 540],
        [216, 145],
        [1703, 145],
        [216, 934],
        [1703, 934],
        [537, 316],
        [1382, 316],
        [537, 763],
        [1382, 763],
    )
    DIFF_R = (
        [-5.2, -16.1],
        [23.7, 1.3],
        [2.0, -9.3],
        [4.4, 1.5],
        [-6.5, -12.7],
        [16.6, -7.5],
        [5.7, -1.8],
        [15.4, -3.5],
        [-2.0, -10.2],
        [0.1, 8.3],
        [1.9, -15.8],
        [-24.8, -2.3],
        [3.2, -9.2],
    )
    GAZE_R = np.array(POSITIONS_R) + np.array(DIFF_R)

    OFFSETS_R = [
        0.36,
        0.50,
        0.20,
        0.10,
        0.30,
        0.38,
        0.13,
        0.33,
        0.22,
        0.18,
        0.34,
        0.52,
        0.21,
    ]

    assert len(calibrations) == 2  # calibration[0] is left, calibration[1] is right
    np.testing.assert_allclose(calibrations[0]["onset"], -6.85)
    np.testing.assert_allclose(calibrations[1]["onset"], -6.85)
    assert calibrations[0]["model"] == "HV13"
    assert calibrations[1]["model"] == "HV13"
    assert calibrations[0]["eye"] == "left"
    assert calibrations[1]["eye"] == "right"
    assert calibrations[0]["avg_error"] == 0.30
    assert calibrations[0]["max_error"] == 0.90
    assert calibrations[1]["avg_error"] == 0.31
    assert calibrations[1]["max_error"] == 0.52
    np.testing.assert_array_equal(POSITIONS_L, calibrations[0]["positions"])
    np.testing.assert_array_equal(POSITIONS_R, calibrations[1]["positions"])
    np.testing.assert_array_equal(GAZE_L, calibrations[0]["gaze"])
    np.testing.assert_array_equal(GAZE_R, calibrations[1]["gaze"])
    np.testing.assert_array_equal(OFFSETS_R, calibrations[1]["offsets"])


@requires_testing_data
@pytest.mark.parametrize(
    "fname, axes",
    [(fname, None), (fname, True)],
)
def test_plot_calibration(fname, axes):
    """Test plotting calibration data."""
    import matplotlib.pyplot as plt

    # Set the non-interactive backend
    plt.switch_backend("agg")

    if axes:
        axes = plt.subplot()
    calibrations = read_eyelink_calibration(fname)
    cal_left = calibrations[0]
    fig = cal_left.plot(show=True, show_offsets=True, axes=axes)
    ax = fig.axes[0]

    scatter1 = ax.collections[0]
    scatter2 = ax.collections[1]
    px, py = cal_left["positions"].T
    gaze_x, gaze_y = cal_left["gaze"].T

    assert ax.title.get_text() == f"Calibration ({cal_left['eye']} eye)"
    assert len(ax.collections) == 2  # Two scatter plots

    np.testing.assert_allclose(scatter1.get_offsets(), np.column_stack((px, py)))
    np.testing.assert_allclose(
        scatter2.get_offsets(), np.column_stack((gaze_x, gaze_y))
    )
    plt.close(fig)


@requires_testing_data
@pytest.mark.parametrize("fname", [(fname)])
def test_calibration_newlines(fname, tmp_path):
    """Test reading a calibration with blank lines between each data line."""
    # Calibration reading should be robust to what we are about to do to this file
    want_cals = read_eyelink_calibration(fname)

    # Let's interleave blank lines into this file
    lines = Path(fname).read_text().splitlines()
    cal_start = lines.index(">>>>>>> CALIBRATION (HV13,P-CR) FOR LEFT: <<<<<<<<<")
    cal_end = lines.index("INPUT\t5509657\t0")
    cal_block = lines[cal_start : cal_end + 1]

    # Inject empty strings between each line in the calibration block
    interleaved = [elem for line in cal_block for elem in (line, "")]

    # Replace the original calibration block with the interleaved one
    new_lines = lines[:cal_start] + interleaved + lines[cal_end + 1 :]

    out_fname = tmp_path / "weird_calibration.asc"
    out_fname.write_text("\n".join(new_lines))
    cals = read_eyelink_calibration(out_fname)

    # The added blank lines should not affect the values that we read in...
    assert len(cals) == len(want_cals)
    assert cals[1]["eye"] == want_cals[1]["eye"]
    np.testing.assert_allclose(cals[0]["onset"], want_cals[0]["onset"])
    np.testing.assert_allclose(cals[0]["positions"], want_cals[0]["positions"])
    np.testing.assert_allclose(cals[1]["offsets"], want_cals[1]["offsets"])
    np.testing.assert_allclose(cals[0]["gaze"], want_cals[0]["gaze"])
