import decimal
import itertools
import os
import pathlib
import re

import canmatrix
import click.testing
import pytest

import epyqlib.tests.common

import epcpm.cli.importsym
import epcpm.cli.exportsym
import epcpm.importexport
import epcpm.importexportdialog


round_trip = pathlib.Path(__file__).parents[3] / 'roundtrip'
project = round_trip / 'project.pmp'
example_device = epyqlib.tests.common.new_devices()[('develop', 'factory')]
exported_can = (
        round_trip / 'exported'
).with_suffix(example_device.can.suffix)
exported_hierarchy = (
        round_trip / 'hierarchy.parameters.json'
).with_suffix(example_device.hierarchy.suffix)


def _full_round_trip():
    # directory = pathlib.Path('full_import')
    # paths = epcpm.importexportdialog.paths_from_directory(directory)
    # project = epcpm.importexport.full_import(paths=paths)
    import epcpm.project
    project = epcpm.project.loadp(pathlib.Path('full_import')/'project.pmp')

    directory = pathlib.Path(os.sep)/'epc'/'g'/'36'/'grid-tied'
    paths = epcpm.importexportdialog.paths_from_directory(directory)

    epcpm.importexport.full_export(project=project, paths=paths, first_time=True)

    # project = epcpm.importexport.full_import(paths=paths)
    # epcpm.importexport.full_export(project=project, paths=paths)


@pytest.mark.skip
def test_import():
    runner = click.testing.CliRunner()

    result = runner.invoke(
        epcpm.cli.importsym.cli,
        [
            '--sym', example_device.can,
            '--hierarchy', example_device.hierarchy,
            '--project', project,
            '--can', round_trip / 'can.json',
            '--parameters', round_trip / 'parameters.json',
            '--sunspec', round_trip / 'sunspec.json',
            '--add-tables',
            '--add-sunspec-types',
        ],
        catch_exceptions=False,
    )
    print(result.output)

    assert result.exit_code == 0


@pytest.mark.skip
def test_export():
    runner = click.testing.CliRunner()

    result = runner.invoke(
        epcpm.cli.exportsym.cli,
        [
            '--project', project,
            '--sym', exported_can,
            '--hierarchy', exported_hierarchy,
        ],
        catch_exceptions=False,
    )
    print(result.output)

    assert result.exit_code == 0


def normalized_attributes(attributes):
    attributes = dict(attributes)

    return attributes


def assert_signals_equal(
        original,
        original_frame,
        exported,
        exported_frame,
):
    print(
        original_frame.name,
        original_frame.mux_names.get(original.multiplex),
        original.name,
    )
    assert_attributes_equal(
        original=original,
        exported=exported,
        attributes=(
            'enumeration',
            'is_float',
            'is_little_endian',
            'is_multiplexer',
            'is_signed',
            'max',
            'min',
            'multiplex',
            'factor',
            'offset',
            'size',
            'startBit',
            'unit',
        ),
    )

    tweaked = dict(exported.attributes)
    if 'LongName' not in original.attributes:
        tweaked.pop('LongName', None)

    tweaked_attributes = normalized_attributes(tweaked)
    original_attributes = normalized_attributes(original.attributes)

    assert tweaked_attributes == original_attributes, original.name

    factory = '<factory>'
    configs = ('<HY>', '<MG3>', '<MG4>', '<DG>', '<DC>')

    def split_and_access_level(comment):

        split = comment.split()

        return (
            factory in split,
            (
                tag in split
                for tag in configs
            ),
            [x for x in split if x not in (factory, *configs)],
        )

    original_factory, original_configs, original_comment = (
        split_and_access_level(original.comment)
    )
    exported_factory, exported_configs, exported_comment = (
        split_and_access_level(exported.comment)
    )

    assert exported_comment == original_comment, original.name

    not_applicable = (
        'readparam_command',
        'readparam_status',
        'meta',
    )
    if original.name.casefold() not in not_applicable:
        original_mux = [
            x for x in original_frame.signals if x.multiplex == 'Multiplexor'
        ]
        original_mux, = original_mux if original_mux else (None,)

        exported_mux = [
            x for x in exported_frame.signals if x.multiplex == 'Multiplexor'
        ]
        exported_mux, = exported_mux if exported_mux else (None,)

        if original_mux is None:
            assert exported_mux == original_mux, original.name

        def tagged_including_mux_check(tag, tagged, signal, mux):
            if mux is not None:
                return (
                        tagged
                        or (
                                signal is not mux
                                and tag in mux.comments[
                                    signal.multiplex]
                        )
                )

            return (
                    tagged
                    or tag in original_frame.comment
            )

        original_access_level = tagged_including_mux_check(
            tag=factory,
            tagged=original_factory,
            signal=original,
            mux=original_mux,
        )
        exported_access_level = tagged_including_mux_check(
            tag=factory,
            tagged=exported_factory,
            signal=exported,
            mux=exported_mux,
        )

        assert exported_access_level == original_access_level, original.name

        z = zip(configs, original_configs, exported_configs)
        for config, original_tagged, exported_tagged in z:
            original_config = tagged_including_mux_check(
                tag=config,
                tagged=original_tagged,
                signal=original,
                mux=original_mux,
            )
            exported_config = tagged_including_mux_check(
                tag=config,
                tagged=exported_tagged,
                signal=exported,
                mux=exported_mux,
            )

            assert exported_config == original_config, (config, exported.comment, original.comment, original.name)

    assert_varied_dict_equal(original.values, exported.values)

    assert exported_comment == original_comment, original.name


def get_name(x):
    return x.name


def sorted_names(names):
    return sorted(names, key=lambda x: (x.casefold(), x))


def unvary_signal_names(name):
    changes = (
        (r'(IGBT|Diode)_R(\d)', r'\1_r\2'),
        (r'(IGBT|Diode)_Tc(\d)', r'\1_tc\2'),
        ('Rii', 'rii'),
        ('Rdi', 'rdi'),
        ('Alpha', 'alpha'),
        ('Amps', 'amps'),
        ('Beta', 'beta'),
        (r'Leg(\d)', r'leg\1'),
        ('CurrentLCL', 'currentLCL'),
        ('MinPulseTime', 'minPulseTime'),
        ('N15V_Supply', 'n15V_Supply'),
        (r'V([p\d]+)_Supply', r'v\1_Supply'),
        ('Phase([ABC])', r'phase\1'),
        ('PostTriggerDuration_S', 'PostTriggerDuration_s'),
        (r'LegInductance(\d)', r'legInductance\1'),
        ('TempSetpoint', 'tempSetpoint'),
        ('LineCurrentLimit', 'lineCurrentLimit'),
        ('Dc(Current|Voltage)Limit', r'dc\1Limit'),
        ('Dc(Current|Voltage)Limit_Echo', r'dc\1Limit_echo'),
        ('Line(Current|Voltage)Limit_Echo', r'line\1Limit_echo'),
        ('PpcIslDroop([VF])_IBpt', r'PpcIslDroop\1_iBpt'),
        ('PpcIslDroop([VF])_DBpt', r'PpcIslDroop\1_dBpt'),
        ('PpcIslDroop([VF])_QBpt', r'PpcIslDroop\1_qBpt'),
        ('PpcIslDestab([VF])_VBpt', r'PpcIslDestab\1_vBpt'),
        ('PpcIslDestab([VF])_DBpt', r'PpcIslDestab\1_dBpt'),
        ('PpcIslDestab([VF])_QBpt', r'PpcIslDestab\1_qBpt'),
        ('PpcIslDestab([VF])_FBpt', r'PpcIslDestab\1_fBpt'),
        ('Ig_Max', 'Ig_max'),
        (
            r'(MesdConfigLutCalibration_)T(Igbt|Aux|Motor)_(\d+)',
            r'\1t\2_\3',
        ),
        (
            '(ProtTemperatureLimitTrip_)T({})_(Running|Stopped)'.format(
                '|'.join((
                    'Inverter',
                    'Internal',
                    'Device',
                    'Inlet',
                    'Delta',
                )),
            ),
            lambda m: f'{m.group(1)}t{m.group(2)}_{m.group(3).casefold()}',
        ),
        (
            '(ProtCtrlVoltageLimitTrip)_V(Pos|Neg)(24|15|5|3)',
            r'\1_v\2\3',
        ),
        (r'(.*)_TxRate', r'\1_txRate'),
        *(
            (x, lambda m: m.group(0)[0].casefold() + m.group(0)[1:])
            for x in (
                'DIg',
                'DpwmStrategy',
                'DcGroup',
                'D[012]',
                'F[012]',
                'Q([012]|Ig)',
                'V([012])',
                '(F|V|W|XpVa)Tau',
                'AcGroup',
                '(C|G)(L|R)s',
                'AcHalfPeriodTrack',
                'DtCompEnable',
                'DutyLimit',
                'EeSaveInProgress',
                'EntryPulseNr',
                'FCut(F|Ig|V|Vg)',
                'FDev(H|L)',
                'FMax',
                'FMin',
                'FNom',
                'FourWireEnable',
                'FSw(Hysteresis|Minimum|Request)',
                'I([012]|Abs|Delta|Dlt)',
                'Ind(IslStart|Threshold)',
                'K(Alpha|Apply|Phi|Vq)',
                'Limit(XY|Z)',
                'Lim(Lower|Upper)',
                'Load(L|PhiTau|R)',
                'Min(Current|VectorTime|Volt)',
                'ModDepthLimit(Beg|End)',
                'NetworkFrequency',
                'P(Delta|PhaseSel)',
                'PhaseSel',
                'PhiAbs',
                'Sat(CompEnable|Voltage)',
                'S(C|R)s',
                'SRv',
                'SimModeEnable',
                'SpectrumAdjustTau',
                'SpreadSpect(Enable|Inject)',
                'StandardDeviation',
                'SyncMod(Enable|Strategy)',
                'Tau',
                'T(Adapt|Deadband|Delta)',
                'Threshold',
                'TimeConst',
                'TrigCalTime',
                'TripTime',
                'TSwitch',
                'U',
                'VDelta',
                'VectAlignCtrl_(K[dp]|StepSat|Ti)',
                'V(Max|Min)',
                'VoltMag',
                'VsComp(Enable|Tau)',
                'Y',
                'ZeroVectorStrategy',
            )
        ),
        *(
            (
                r'(.*)_({x})_(.*)'.format(x=x),
                lambda
                    m: f'{m.group(1)}_{m.group(2).casefold()}_{m.group(3)}',
            )
            for x in (
                'Seconds',
                'Hertz',
                'Percent_Nominal_Volts',
                'Percent_Nominal_Var',
                'Percent_Nominal_Pwr',
                'Percent',
                'Nominal_Volts',
            )
        ),
        *(
            (x, lambda m: m.group(1) + m.group(2) + m.group(3).casefold())
            for x in (
                '(DcCurrent|DcVoltage|ReactivePower|RealPower)()(Limit|_Echo)',
                '(ReadParam|SaveToEE)(_)(Command|Status)',
                '(TempTmodel)(_)(Calculated)',
                '({})(_)(Echo)'.format(
                    '|'.join((
                        'Enable',
                        'FaultClr',
                        'WarningClr',
                        'ReactiveCurrent',
                        'RealCurrent',
                        'IslandReconnect',
                    )),
                ),
                '({})(_)(Measured)'.format(
                    '|'.join((
                        'TempDeviceBrdg[ABC]',
                        'CurrentAC',
                        'Frequency',
                        'VoltageAC',
                        'FrequencyRemote',
                        'VoltageAcRemote',
                        'SolarCurrent',
                        'SolarVoltage',
                        'CurrentDC',
                        'VoltageDCBus',
                        'VoltageDCInput',
                        'L[123]Current',
                        'L[123]ToL[123]Voltage',
                        '.*',
                    )),
                ),
        )
        ),
    )

    tweaked = name
    for pattern, replacement in changes:
        tweaked = re.sub(f'^{pattern}$', replacement, tweaked)

    return tweaked


def assert_signal_list_equal(
        original,
        original_frame,
        exported,
        exported_frame,
):
    original_names = sorted_names(
        x.name
        for x in original
    )
    exported_names = sorted_names(
        unvary_signal_names(x.name)
        for x in exported
    )

    case_insensitive = (
        'aclosshotstandbydc',
        'aclosshotstandbypc',
        'acwaitenabledcatref',
        'argadjust',
        'argerrmax',
        'argerrsat',
        'argerrtau',
        'autodisconnectongridloss',
        'autofollowingpostconnect',
        'autoreconnectongridavail',
        'autosequencewhendisabled',
        'autovolttrackduringsynch',
        'conntimeout',
        'controlsvariant',
        'current',
        'dcwaitenabledcatref',
        'disctimeout',
        'enabledelayac',
        'enabledelaydc',
        'didt',
        'dqccterm',
        'dummy',
        'freq',
        'fswactual',
        # 'ftau',
        'gain',
        'groupedenable',
        'hardwarevariant',
        'level',
        'limit',
        'magadjust',
        'magerrmax',
        'magerrtau',
        'moddepthxy',
        'moddepthz',
        'max',
        'min',
        'pllenable',
        'pulsenractual',
        'r',
        'rate',
        'resistance',
        'syncactive',
        'syncholdtime',
        'synctimeout',
        'tc',
        'temp',
        'temperature',
        'time',
        'tisllatch',
        'tunload',
        'vdevh',
        'vdevl',
        'volts',
        # 'vtau',
        # 'wtau',
    )

    zipped = itertools.zip_longest(original_names, exported_names)
    for original_name, exported_name in zipped:
        if original_name.casefold() in case_insensitive:
            assert (
                exported_name.casefold()
                == original_name.casefold()
            ), original.name
        else:
            assert exported_name == original_name

    def key(x):
        return (
            x.name.casefold(), x.multiplex
            if x.multiplex is not None
            else float('-inf')
        )

    zipped = itertools.zip_longest(
        sorted(original, key=key),
        sorted(exported, key=key),
    )
    for original_signal, exported_signal in zipped:
        assert_signals_equal(
            original=original_signal,
            original_frame=original_frame,
            exported=exported_signal,
            exported_frame=exported_frame,
        )


def assert_attributes_equal(original, exported, *, attributes):
    for attribute in attributes:
        assert (
            getattr(exported, attribute) == getattr(original, attribute)
        ), (original.name, attribute)


def assert_varied_dict_equal(original, exported):
    zipped = itertools.zip_longest(
        sorted(original.items()),
        sorted(exported.items()),
    )
    for (original_value, original_name), (exported_value, exported_name) in zipped:
        assert exported_value == original_value

        tweaked = unvary_signal_names(exported_name)
        assert tweaked == original_name


def assert_frames_equal(original, exported):
    assert_attributes_equal(
        original=original,
        exported=exported,
        attributes=(
            'name',
            'id',
            'extended',
            'attributes',
            'comment',
            'is_multiplexed',
            'size',
        ),
    )

    assert_varied_dict_equal(original.mux_names, exported.mux_names)

    assert_signal_list_equal(
        original=original.signals,
        original_frame=original,
        exported=exported.signals,
        exported_frame=exported,
    )


def collect_attribute(sequence, name):
    return {getattr(x, name) for x in sequence}


def assert_frame_lists_equal(original, exported):
    original_names = collect_attribute(original, 'name')
    exported_names = collect_attribute(exported, 'name')

    assert exported_names == original_names, original.name

    zipped = itertools.zip_longest(
        sorted(original, key=get_name),
        sorted(exported, key=get_name),
    )
    for original_frame, exported_frame in zipped:
        assert_frames_equal(original_frame, exported_frame)


def assert_value_tables_equal(original, exported):
    return exported == original


@pytest.mark.skip
def test_roundtrip():
    original, = canmatrix.formats.loadp(os.fspath(example_device.can)).values()
    assert original.load_errors == []
    exported, = canmatrix.formats.loadp(os.fspath(exported_can)).values()
    assert exported.load_errors == []

    assert_frame_lists_equal(original.frames, exported.frames)
    assert_value_tables_equal(original.value_tables, exported.value_tables)
