from fractions import Fraction
import os
import unittest
from pathlib import Path

from diff_pdf_visually import pdf_similar  # type: ignore

from musurgia.trees.fractaltimelinetree import FractalTimelineTree
from musurgia.pdf import TextLabel, DrawObjectColumn, StraightLine
from musurgia.pdf.drawobject import MasterDrawObject
from musurgia.trees.musicaltree import FractalMusicalTree, FractalRelativeMusicTree, MidiMusicalTreeChordFactory
from musurgia.trees.timelinetree import TimelineDuration, TimelineTree
from musurgia.trees.valuedtree import ValuedTree

test_fractal_structur_list = [
    Fraction(10, 1),
    [
        Fraction(3, 1),
        [Fraction(3, 5)],
        [
            Fraction(6, 5),
            [Fraction(6, 25)],
            [Fraction(12, 25)],
            [Fraction(3, 25)],
            [Fraction(9, 25)],
        ],
        [Fraction(3, 10)],
        [
            Fraction(9, 10),
            [Fraction(27, 100)],
            [Fraction(9, 100)],
            [Fraction(9, 25)],
            [Fraction(9, 50)],
        ],
    ],
    [Fraction(1, 1)],
    [
        Fraction(4, 1),
        [Fraction(2, 5)],
        [Fraction(4, 5)],
        [
            Fraction(6, 5),
            [Fraction(6, 25)],
            [Fraction(12, 25)],
            [Fraction(3, 25)],
            [Fraction(9, 25)],
        ],
        [
            Fraction(8, 5),
            [Fraction(4, 25)],
            [Fraction(8, 25)],
            [Fraction(12, 25)],
            [Fraction(16, 25)],
        ],
    ],
    [
        Fraction(2, 1),
        [
            Fraction(4, 5),
            [Fraction(4, 25)],
            [Fraction(8, 25)],
            [Fraction(2, 25)],
            [Fraction(6, 25)],
        ],
        [
            Fraction(3, 5),
            [Fraction(9, 50)],
            [Fraction(3, 50)],
            [Fraction(6, 25)],
            [Fraction(3, 25)],
        ],
        [Fraction(2, 5)],
        [Fraction(1, 5)],
    ],
]


def create_test_path(path, test_name):
    return path.parent.joinpath(f"{path.stem}_{test_name}")


class FilePath:
    def __init__(self, unittest, parent_path, name, extension):
        self._unittest = None
        self._parent_path = None
        # self._name = None
        # self._extension = None

        self.unittest = unittest
        self.parent_path = parent_path
        self.name = name
        self.extension = extension
        self.out_path = None

    @property
    def unittest(self):
        return self._unittest

    @unittest.setter
    def unittest(self, val):
        # if not isinstance(val, PdfTestCase):
        #     raise TypeError(f"unittest.value must be of type {type(PdfTestCase)} not{type(val)}")
        self._unittest = val

    @property
    def parent_path(self):
        return self._parent_path

    @parent_path.setter
    def parent_path(self, val):
        if not isinstance(val, Path):
            raise TypeError(
                f"parent_path.value must be of type {type(Path)} not{type(val)}"
            )
        self._parent_path = val

    def __enter__(self):
        self.out_path = create_test_path(
            self.parent_path, self.name + "." + self.extension
        )
        return self.out_path

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.unittest.assertCompareFiles(self.out_path)


class PdfTestCase(unittest.TestCase):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

    def _compare_pdfs(self, actual_file_path, expected_file_path, verbosity):
        self.assertTrue(
            pdf_similar(actual_file_path, expected_file_path, verbosity=verbosity)
        )

    def _compare_contents(self, actual_file_path, expected_file_path):
        with open(actual_file_path, "r") as myfile:
            result = myfile.read()

        with open(expected_file_path, "r") as myfile:
            expected = myfile.read()

        self.assertEqual(expected, result)

    def assertCompareFiles(
        self, actual_file_path, expected_file_path=None, verbosity=0
    ):
        file_name, extension = os.path.splitext(actual_file_path)
        if not expected_file_path:
            if not extension:
                raise ValueError("actual_file_path has no file extension")
            expected_file_path = file_name + "_expected" + extension

        if extension == ".pdf":
            self._compare_pdfs(
                actual_file_path, expected_file_path, verbosity=verbosity
            )
        else:
            self._compare_contents(actual_file_path, expected_file_path)

    def file_path(self, parent_path, name, extension):
        tfp = FilePath(self, parent_path, name, extension)
        return tfp


class XMLTestCase(unittest.TestCase):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

    def _compare_contents(self, actual_file_path, expected_file_path):
        with open(actual_file_path, "r") as myfile:
            result = myfile.read()

        with open(expected_file_path, "r") as myfile:
            expected = myfile.read()

        self.assertEqual(expected, result)

    def assertCompareFiles(self, actual_file_path, expected_file_path=None):
        file_name, extension = os.path.splitext(actual_file_path)
        if not expected_file_path:
            if not extension:
                expected_file_path += ".xml"
            expected_file_path = file_name + "_expected" + ".xml"

        self._compare_contents(actual_file_path, expected_file_path)

    def file_path(self, parent_path, name):
        tfp = FilePath(self, parent_path, name, "xml")
        return tfp


def fractal_node_info(node):
    return f"{node.get_fractal_order()}: {node.get_permutation_index()}: {round(float(node.get_value()), 2)}"


def fractal_node_info_with_permutation_order(node):
    return f"{node.get_fractal_order()}: {node.get_permutation_index()}: {node.get_permutation_order()}: {round(float(node.get_value()), 2)}"


class DummyMaster(MasterDrawObject):
    def get_slave_margin(self, slave, margin):
        return 10

    def get_slave_position(self, slave, position):
        return 20

    def draw(self, pdf):
        pass

    def get_relative_x2(self):
        pass

    def get_relative_y2(self):
        pass


def add_control_positions_to_draw_object(draw_object):
    text_1 = TextLabel(draw_object.positions, font_size=8)
    text_2 = TextLabel(
        draw_object.get_end_positions(),
        placement="below",
        font_size=8,
        left_margin=(draw_object.get_relative_x2() - draw_object.relative_x),
    )

    draw_object.add_text_label(text_1)
    draw_object.add_text_label(text_2)


numbers = ["one", "two", "three"]


def add_test_left_labels(drawobject):
    for t in numbers:
        drawobject.add_text_label(f"left label {t}", placement="left", font_size=8)


def add_test_below_labels(drawobject):
    for t in numbers:
        drawobject.add_text_label(f"below label {t}", placement="below", font_size=8)


# def add_test_right_labels(drawobject):
#     for t in numbers:
#         drawobject.add_text_label(f'right label {t}', placement='right', font_size=8)


def add_test_above_labels(drawobject):
    for t in numbers:
        drawobject.add_text_label(f"above label {t}", placement="above", font_size=8)


def add_test_right_above_labels(drawobject):
    for t in numbers:
        drawobject.add_text_label(
            f"right above label {t}", placement="right_above", font_size=8
        )


def add_test_right_below_labels(drawobject):
    for t in numbers:
        drawobject.add_text_label(
            f"right below label {t}", placement="right_below", font_size=8
        )


def add_test_labels(drawobject):
    add_test_above_labels(drawobject)
    add_test_below_labels(drawobject)
    add_test_left_labels(drawobject)
    # add_test_right_labels(drawobject)
    # add_test_right_above_labels(drawobject)
    # add_test_right_below_labels(drawobject)


def create_simple_column(list_of_draw_objects):
    c = DrawObjectColumn(show_borders=True, show_margins=True)
    for do in list_of_draw_objects:
        do.relative_y = 5
        do.margins = (10, 10, 10, 10)
    divider = StraightLine("h", max([do.get_width() for do in list_of_draw_objects]))
    for do in list_of_draw_objects:
        c.add_draw_object(do)
        if do != list_of_draw_objects[-1]:
            c.add_draw_object(divider)

    return c


def create_test_fractal_timline_tree():
    ft = FractalTimelineTree(
        duration=TimelineDuration(10),
        proportions=(1, 2, 3, 4),
        main_permutation_order=(3, 1, 4, 2),
        permutation_index=(1, 1),
    )
    ft.add_layer()

    ft.add_layer(lambda node: node.get_fractal_order() > 1)
    ft.add_layer(lambda node: node.get_fractal_order() > 2)
    return ft


def create_test_fractal_musical_tree():
    ft = FractalMusicalTree(
        duration=TimelineDuration(10),
        proportions=(1, 2, 3, 4),
        main_permutation_order=(3, 1, 4, 2),
        permutation_index=(1, 1),
    )
    ft.add_layer()

    ft.add_layer(lambda node: node.get_fractal_order() > 1)
    ft.add_layer(lambda node: node.get_fractal_order() > 2)
    return ft

class FractalMusicalTreeChordFactory(MidiMusicalTreeChordFactory):
    def update_chord_fractal_order(self):
        self._chord.add_words(str(self.get_musical_tree_node().get_fractal_order()), placement="below", relative_y=-10)

class MyFractalRelativeMusicTree(FractalRelativeMusicTree):
    TreeChordFactoryClass = FractalMusicalTreeChordFactory

def create_test_fractal_relative_musical_tree():
    ft = MyFractalRelativeMusicTree(
        duration=TimelineDuration(10),
        proportions=(1, 2, 3, 4),
        main_permutation_order=(3, 1, 4, 2),
        permutation_index=(1, 1),
        midi_value_range=(60, 84),
    )
    ft.add_layer()

    ft.add_layer(lambda node: node.get_fractal_order() > 1)
    ft.add_layer(lambda node: node.get_fractal_order() > 2)
    ft.set_relative_midis()
    return ft


def create_test_timeline_tree():
    tlt = TimelineTree(duration=TimelineDuration(60))
    for d in [20, 10, 30]:
        tlt.add_child(TimelineTree(TimelineDuration(d)))
    for list_of_d, child in zip([[10, 2, 3, 5], [], [5, 20, 3, 2]], tlt.get_children()):
        for d in list_of_d:
            child.add_child(TimelineTree(TimelineDuration(d)))
    return tlt


class DemoValuedTree(ValuedTree):
    def __init__(self, value, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._value = value

    def _check_child_to_be_added(self, child):
        if not isinstance(child, ValuedTree):
            raise TypeError
        else:
            return True

    def _set_value(self, value):
        self._value = Fraction(value)

    def get_value(self):
        return Fraction(self._value)


def create_test_valued_tree() -> DemoValuedTree:
    vt = DemoValuedTree.create_tree_from_list(test_fractal_structur_list, "value")
    # print(vt.get_tree_representation(key=lambda node: node.get_value()))

    """└── 10
    ├── 3
    │   ├── 3/5
    │   ├── 6/5
    │   │   ├── 6/25
    │   │   ├── 12/25
    │   │   ├── 3/25
    │   │   └── 9/25
    │   ├── 3/10
    │   └── 9/10
    │       ├── 27/100
    │       ├── 9/100
    │       ├── 9/25
    │       └── 9/50
    ├── 1
    ├── 4
    │   ├── 2/5
    │   ├── 4/5
    │   ├── 6/5
    │   │   ├── 6/25
    │   │   ├── 12/25
    │   │   ├── 3/25
    │   │   └── 9/25
    │   └── 8/5
    │       ├── 4/25
    │       ├── 8/25
    │       ├── 12/25
    │       └── 16/25
    └── 2
        ├── 4/5
        │   ├── 4/25
        │   ├── 8/25
        │   ├── 2/25
        │   └── 6/25
        ├── 3/5
        │   ├── 9/50
        │   ├── 3/50
        │   ├── 6/25
        │   └── 3/25
        ├── 2/5
        └── 1/5
    """
    return vt


def add_node_infos_to_graphic(vt, gt):
    for gn, vn in zip(gt.traverse(), vt.traverse()):
        value_label = gn.get_segment().start_mark_line.add_text_label(
            round(float(vn.get_value()), 1), font_size=8, bottom_margin=1
        )

        position_in_tree_label = gn.get_segment().start_mark_line.add_text_label(
            f"{vn.get_position_in_tree()}", font_size=8, placement="below", top_margin=1
        )
        if gn.get_distance() == 3:
            if vn.up.up.get_position_in_tree() == "4":
                if gn.up.get_children().index(gn) % 4 == 1:
                    position_in_tree_label.top_margin = 3
                    value_label.bottom_margin = 4
                elif gn.up.get_children().index(gn) % 4 == 2:
                    position_in_tree_label.top_margin = 5
                    value_label.bottom_margin = 7
                elif gn.up.get_children().index(gn) % 4 == 3:
                    position_in_tree_label.top_margin = 7
                    value_label.bottom_margin = 10
            else:
                if gn.up.get_children().index(gn) % 2 == 1:
                    position_in_tree_label.top_margin = 3
                    value_label.bottom_margin = 4
