""" Sequence classes for Ziffers """
from dataclasses import dataclass, field, replace
from itertools import product
from math import floor
import random
from types import LambdaType
from copy import deepcopy
import operator
from ..defaults import DEFAULT_OPTIONS
from ..common import cyclic_zip, euclidian_rhythm, flatten
from ..scale import note_from_pc, midi_to_freq
from .items import (
    Meta,
    Item,
    Event,
    DurationChange,
    OctaveChange,
    OctaveAdd,
    Pitch,
    Rest,
    RandomPitch,
    Chord,
    RomanNumeral,
    Cyclic,
    RandomInteger,
    Range,
    Integer,
    VariableAssignment,
    Variable,
    VariableList,
    Measure,
    Function,
    Modification,
    Whitespace,
    Sample,
    SampleList,
)


# TODO: Could be refactored to each class?
def resolve_item(item: Meta, options: dict):
    """Resolve cyclic value"""
    if isinstance(item, Sequence):
        if isinstance(item, ListOperation):
            yield from item.evaluate(options)
        elif isinstance(item, (RepeatedSequence, RepeatedListSequence)):
            yield from item.resolve_repeat(options)
        elif isinstance(item, Subdivision):
            item.evaluate_values(options)
            yield item
        elif isinstance(item, Eval):
            yield from item.evaluate_values(options)
        else:
            yield from item.evaluate_tree(options)
    elif isinstance(item, VariableAssignment):
        if item.pre_eval:
            pre_options = options.copy()
            pre_options["pre_eval"] = True
            options[item.variable.name] = Sequence(
                values=list(resolve_item(item.value, pre_options))
            )
        else:
            options[item.variable.name] = item.value
    elif isinstance(item, Variable):
        if options[item.name]:
            if item.name in options:
                opt_item = options[item.name]
                if isinstance(opt_item, LambdaType):
                    yield Function(
                        run=opt_item,
                        text=item.text,
                        kwargs=(options | item.local_options),
                        local_options=item.local_options,
                    )
                elif isinstance(opt_item, str):
                    yield Sample(
                        name=opt_item,
                        text=item.text,
                        kwargs=(options | item.local_options),
                        local_options=item.local_options,
                    )
                variable = deepcopy(opt_item)
                yield from resolve_item(variable, options)
    elif isinstance(item, VariableList):
        seqlist = []
        sample_list = True
        for var in item.values:
            if var.name in options:
                opt_item = options[var.name]
                if isinstance(opt_item, LambdaType):
                    sample_list = False
                    seqlist.append(
                        Function(
                            run=opt_item,
                            text=var.text,
                            kwargs=(options | var.local_options),
                            local_options=var.local_options,
                        )
                    )
                elif isinstance(opt_item, str):
                    seqlist.append(
                        Sample(
                            name=opt_item,
                            text=var.text,
                            kwargs=(options | var.local_options),
                            local_options=var.local_options,
                        )
                    )
                elif isinstance(opt_item, Sequence):
                    sample_list = False
                    seqlist.append(opt_item)
        if len(seqlist) > 0:
            if sample_list:
                yield SampleList(values=seqlist, kwargs=options)
            else:
                yield PolyphonicSequence(values=seqlist)
    elif isinstance(item, Range):
        yield from item.evaluate(options)
    elif isinstance(item, Cyclic):
        yield from resolve_item(item.get_value(), options)
    elif isinstance(item, Euclid):
        yield from euclidean_items(item, options)
    elif isinstance(item, Modification):
        update_modifications(item, options)
    elif isinstance(item, Measure):
        item.reset_options(options)
    elif options["degrees"] is True and isinstance(item, Pitch) and item.pitch_class is 0:
        yield Rest(text="r", kwargs=options)
    elif isinstance(item, Meta):  # Filters whitespace
        yield update_item(item, options)


def resolve_integer_value(item, options):
    """Helper for resolving integer value of different types"""
    while isinstance(item, Cyclic):
        item = item.get_value(options)
    if isinstance(item, Pitch):
        return item.get_value(options)
    if isinstance(item, Integer):
        return item.get_value(options)
    return item


def update_item(item, options):
    """Update or create new pitch"""
    if set(("key", "scale")) <= options.keys():
        if isinstance(item, Pitch):
            item.update_options(options)
            item.update_note()
            if options.get("pre_eval", False):
                item.duration = options["duration"]
        if isinstance(item, Rest):
            item.update_options(options)
        elif isinstance(item, (RandomPitch, RandomInteger)):
            item = create_pitch(item, options)
        elif isinstance(item, Chord):
            item.update_options(options)
            item.update_notes(options)
        elif isinstance(item, RomanNumeral):
            item = item.evaluate_chord(options)
    return item


def euclidean_items(euclid: Item, options: dict):
    """Loops values from generated euclidean sequence"""
    euclid.evaluate(options)
    for item in euclid.evaluated_values:
        yield from resolve_item(item, options)


def update_modifications(current: Item, options: dict) -> dict:
    """Update options based on current item"""
    if isinstance(current, (OctaveChange, DurationChange)):
        options[current.key] = current.value
    elif isinstance(current, OctaveAdd):
        if current.key in options:  # Add to existing value
            options[current.key] += current.value
        else:  # Create value if not existing
            options[current.key] = current.value


def create_pitch(current: Item, options: dict) -> Pitch:
    """Create pitch based on values and options"""

    merged_options = options | current.local_options

    if "modifier" in merged_options:
        c_modifier = merged_options["modifier"]
    else:
        c_modifier = 0

    if hasattr(current, "modifier") and current.modifier is not None:
        c_modifier += current.modifier

    if "octave" in merged_options:
        c_octave = merged_options["octave"]
        if "octave" in options:
            c_octave = options["octave"] + c_octave
    else:
        c_octave = 0

    if hasattr(current, "octave") and current.octave is not None:
        c_octave += current.octave

    current_value = current.get_value(merged_options)

    note, pitch_bend = note_from_pc(
        root=merged_options["key"],
        pitch_class=current_value,
        intervals=merged_options["scale"],
        modifier=c_modifier,
        octave=c_octave,
        degrees=merged_options["degrees"]
    )
    new_pitch = Pitch(
        pitch_class=current_value,
        text=str(current_value),
        freq=midi_to_freq(note),
        note=floor(note),
        pitch_bend=pitch_bend,
        octave=c_octave,
        modifier=c_modifier,
        kwargs=merged_options,
    )
    return new_pitch


@dataclass(kw_only=True)
class Sequence(Meta):
    """Class for sequences of items"""

    values: list
    text: str = field(default=None)
    wrap_start: str = field(default=None, repr=False)
    wrap_end: str = field(default=None, repr=False)
    local_index: int = field(default=0, init=False)
    evaluated_values: list = field(default=None)

    def __post_init__(self):
        super().__post_init__()
        self.text = self.__collect_text()
        self.update_local_options()

    def __getitem__(self, index):
        return self.values[index]

    def __len__(self):
        return len(self.values)

    def update_local_options(self):
        """Update value attributes from dict"""
        if self.local_options:
            for obj in self.values:
                if isinstance(obj, Event):
                    if obj.local_options:
                        obj.local_options = (
                            obj.local_options | self.local_options.copy()
                        )
                    else:
                        obj.local_options = self.local_options.copy()

    def __collect_text(self) -> str:
        """Collect text value from values"""
        text = "".join([val.text for val in self.values])
        if self.wrap_start is not None:
            text = self.wrap_start + text
        if self.wrap_end is not None:
            text = text + self.wrap_end
        return text

    def evaluate_tree(self, options: dict = None, eval_tree: bool = False):
        """Evaluate the tree and return array of resolved pitches

        Args:
            options (dict, optional): Options for the pitches. Defaults to None.
            eval_tree (bool, optional): Flag for using the evaluated subtree. Defaults to False.
        """

        # Start of the main function: Evaluate and flatten the Ziffers object tree
        values = self.evaluated_values if eval_tree else self.values
        for item in values:
            yield from resolve_item(item, options)

    def filter(self, keep: tuple):
        """Filter out items from sequence.

        Args:
            keep (tuple): Tuple describing classes to keep

        Returns:
            Sequence: Copy of the sequence with filtered values.
        """
        return replace(
            self, values=[item for item in self.values if isinstance(item, keep)]
        )


@dataclass(kw_only=True)
class PolyphonicSequence:
    """Class for polyphonic sequence"""

    values: list


@dataclass(kw_only=True)
class ListSequence(Sequence):
    """Class for Ziffers list sequences"""

    wrap_start: str = field(default="(", repr=False)
    wrap_end: str = field(default=")", repr=False)


@dataclass(kw_only=True)
class RepeatedListSequence(Sequence):
    """Class for Ziffers list sequences"""

    repeats: RandomInteger | Integer = field(default_factory=Integer(value=1, text="1"))
    wrap_start: str = field(default="(:", repr=False)
    wrap_end: str = field(default=":)", repr=False)

    def resolve_repeat(self, options: dict):
        """Repeats items and generates new random values"""
        repeats = self.repeats.get_value(options)
        if not isinstance(repeats, int):
            repeats = resolve_integer_value(repeats, options)
        for _ in range(repeats):
            for item in self.evaluate_tree(options):
                yield from resolve_item(item, options)


@dataclass(kw_only=True)
class Subdivision(Sequence):
    """Class for subdivisions"""

    full_duration: float = field(default=None, init=False)

    def evaluate_values(self, options):
        """Evaluate values and store to evaluated_values"""
        self.full_duration = options["duration"]
        self.evaluated_values = list(self.evaluate_tree(options))

    def evaluate_durations(self, duration=None):
        """Calculate new durations by dividing with the number of items in the sequence"""
        if duration is None:
            duration = self.full_duration
        new_d = duration / len(self.evaluated_values)
        for item in self.evaluated_values:
            if isinstance(item, Subdivision):
                yield from item.evaluate_durations(new_d)
            if isinstance(item, Event):
                if duration is not None:
                    item.duration = new_d
                    item.beat = new_d * 4
                yield item


@dataclass(kw_only=True)
class ListOperation(Sequence):
    """Class for list operations"""

    evaluated_values: list = None

    def evaluate(self, options=DEFAULT_OPTIONS.copy()):
        """Evaluates the operation"""

        def _filter_whitespace(input_list):
            for item in input_list:
                if isinstance(item, Meta):
                    yield item

        def _filter_operation(input_list, options):
            """Filter and evaluate values"""
            flattened_list = []
            for item in input_list:
                if isinstance(item, (list, Sequence)):
                    if isinstance(item, ListOperation):
                        flattened_list.extend(item.evaluated_values)
                    elif isinstance(item, Subdivision):
                        item.evaluate_values(options)
                        flattened_list.extend(list(item.evaluate_durations()))
                    elif isinstance(item, RepeatedListSequence):
                        flattened_list.extend(list(item.resolve_repeat(options)))
                    elif isinstance(item, Eval):
                        flattened_list.extend(item.evaluate_values(options))
                    else:
                        flattened_list.append(_filter_operation(item, options))
                elif isinstance(item, Cyclic):
                    value = item.get_value(options)
                    if isinstance(value, Sequence):
                        flattened_list.extend(_filter_operation(value, options))
                    elif isinstance(value, (Event, RandomInteger, Integer)):
                        flattened_list.append(value)
                elif isinstance(item, Modification):
                    options = options | item.as_options()
                elif isinstance(item, RomanNumeral):
                    item = item.evaluate_chord(options)
                    flattened_list.append(item)
                elif isinstance(item, Range):
                    flattened_list.extend(list(item.evaluate(options)))
                elif isinstance(item, (Event, RandomInteger, Integer)):
                    item.update_options(options)
                    item = update_item(item, options)
                    flattened_list.append(item)

            if isinstance(input_list, Sequence):
                return replace(input_list, values=flattened_list)

            return flattened_list

        def _pick_from_list(left, right, options):
            """Pick random numbers from a list"""
            if isinstance(left, Sequence):
                left = _filter_operation(left, options)
            
            if isinstance(right, Sequence):
                right = _filter_operation(right, options)
            
            if not isinstance(right, (list, Sequence)):
                right = Sequence(values=[right])
            
            result = []

            for num in right.values:
                for _ in range(num.get_value(options)):
                    result.append(random.choice(left.values))
            return flatten(result)
        

        def _select_from_list(left, right, options):
            """Select number of items from shuffled list"""
            if isinstance(left, Sequence):
                left = _filter_operation(left, options)
            
            if isinstance(right, Sequence):
                right = _filter_operation(right, options)
            
            if not isinstance(right, (list, Sequence)):
                right = Sequence(values=[right])
            
            result = []
            left = left.values

            for num in right.values:
                random.shuffle(left)
                num = num.get_value(options)
                new_list = [left[i % len(left)] for i in range(num)]
                result += new_list
                
            return flatten(result)

        def _vertical_arpeggio(left, right, options):
            """Vertical arpeggio operation, eg. (135)@(q 1 2 021)"""
            left = _filter_operation(left, options)
            right = _filter_operation(right, options)
            if not isinstance(left, list):
                left = list(left.evaluate_tree(options))
            if not isinstance(right, list):
                right = list(right.evaluate_tree(options))
            arp_items = []

            for item in left:
                for index in right:
                    pcs = item.pitch_classes
                    if isinstance(index, Pitch):
                        new_pitch = deepcopy(pcs[index.get_value(options) % len(pcs)])
                        new_pitch.duration = index.duration
                        arp_items.append(new_pitch)
                    else:  # Should be a chord
                        new_pitches = []
                        for pitch in index.pitch_classes:
                            new_pitch = deepcopy(
                                pcs[pitch.get_value(options) % len(pcs)]
                            )
                            new_pitch.duration = pitch.duration
                            new_pitches.append(new_pitch)
                        new_chord = Chord(pitch_classes=new_pitches, kwargs=options)
                        new_chord.update_notes()
                        new_chord.text = "".join(
                            [val.text for val in new_chord.pitch_classes]
                        )
                        arp_items.append(new_chord)

            return Sequence(values=arp_items)

        def _horizontal_arpeggio(left, right, options):
            """Horizontal arpeggio operation, eg. (1 2 3 4)#(0 3 2 1)"""
            left = _filter_operation(left, options)
            right = _filter_operation(right, options)
            left = list(left.evaluate_tree(options))
            right = list(right.evaluate_tree(options))
            arp_items = []
            for index in right:
                new_item = deepcopy(left[index.get_value(options) % len(left)])
                arp_items.append(new_item)
            return Sequence(values=arp_items)

        def _cyclic_zip(left, right, options):
            """Cyclic zip operaiton, eg. (q e)<>(1 2 3)"""
            left = list(_filter_whitespace(left))
            right = list(_filter_whitespace(right))
            result = Sequence(values=cyclic_zip(left, right))
            return _filter_operation(result, options)

        def _python_operations(left, right, options):
            """Python math operations"""

            def __chord_operation(chord, pitch_y, yass, options):
                """Operation for single chords"""
                new_pitches = []
                pitch_y = pitch_y.get_value(options)
                for pitch_x in chord.pitch_classes:
                    pitch_x = pitch_x.pitch_class
                    pitch_y = resolve_integer_value(pitch_y, options)
                    new_pitch = Pitch(
                        pitch_class=operation(
                            pitch_y if yass else pitch_x, pitch_x if yass else pitch_y
                        ),
                        kwargs=options,
                    )
                    new_pitches.append(new_pitch)
                new_chord = Chord(pitch_classes=new_pitches, kwargs=options)
                new_chord.update_notes()
                return new_chord

            # _python_operation starts. Filter & evaluate items.

            left = _filter_operation(left, options)
            if isinstance(right, Sequence):
                right = _filter_operation(right, options)
            elif isinstance(right, Cyclic):
                right = right.get_value(options)

            # Create product of items.
            pairs = product(
                (right.values if isinstance(right, Sequence) else [right]), left
            )

            results = []
            for first, second in pairs:
                if isinstance(first, Chord) and isinstance(second, Chord):
                    new_pitches = []
                    for pitch_x in first.pitch_classes:
                        for pitch_y in second.pitch_classes:
                            new_pitch = Pitch(
                                pitch_class=operation(
                                    pitch_x.pitch_class, pitch_y.pitch_class
                                ),
                                kwargs=options,
                            )
                            new_pitches.append(new_pitch)
                        new_chord = Chord(pitch_classes=new_pitches, kwargs=options)
                        new_chord.update_notes()
                        outcome = new_chord
                elif isinstance(first, Chord):
                    outcome = __chord_operation(first, second, False, options)
                elif isinstance(second, Chord):
                    outcome = __chord_operation(second, first, True, options)
                else:
                    outcome = Pitch(
                        pitch_class=operation(
                            first.get_value(options), second.get_value(options)
                        ),
                        kwargs=second.get_options(),
                    )
                results.append(outcome)
            return results

        # Start of the evaluate() function

        operators = self.values[1::2]  # Fetch every second operator element
        values = self.values[::2]  # Fetch every second list element
        # values = _filter_operation(values, options)  # Filter out
        if len(values) == 1:
            return values[0]  # If right hand doesnt contain anything sensible
        left = values[0]  # Start results with the first array

        for i, operand in enumerate(operators):
            operation = operand.value
            right = values[i + 1]
            if isinstance(operation, str):
                if operation == "vertical":
                    left = _vertical_arpeggio(left, right, options)
                elif operation == "horizontal":
                    left = _horizontal_arpeggio(left, right, options)
                elif operation == "zip":
                    left = _cyclic_zip(left, right, options)
                elif operation == "pick":
                    left = _pick_from_list(left, right, options)
                elif operation == "select":
                    left = _select_from_list(left, right, options)
                
            else:
                left = _python_operations(left, right, options)
        return left


@dataclass(kw_only=True)
class Eval(Sequence):
    """Class for evaluation notation"""

    wrap_start: str = field(default="{", repr=False)
    wrap_end: str = field(default="}", repr=False)

    # def __post_init__(self):
    #    self.text = "".join([val.text for val in flatten(self.values)])
    #   super().__post_init__()

    def evaluate_values(self, options):
        operations = [val for val in self.values if isinstance(val, (Operation, Rest))]
        eval_values = []
        for val in operations:
            if isinstance(val,Operation):
                eval_values.append(Pitch(pitch_class=val.evaluate(), kwargs=options | val.local_options))
            else:
                eval_values.append(val)

        self.evaluated_values = eval_values

        return self.evaluated_values


@dataclass(kw_only=True)
class LispOperation(Sequence):
    """Class for lisp-like operations: (+ 1 2 3) etc."""

    values: list
    operator: operator


@dataclass(kw_only=True)
class Operation(Sequence):
    """Class for sequential operations"""

    values: list

    def evaluate(self):
        return eval(self.text)


@dataclass(kw_only=True)
class RepeatedSequence(Sequence):
    """Class for repeats"""

    repeats: RandomInteger | Integer = field(default_factory=Integer(value=1, text="1"))
    wrap_start: str = field(default="[:", repr=False)
    wrap_end: str = field(default=":]", repr=False)

    evaluated_values: list = None

    def resolve_repeat(self, options):
        """Resolves all items"""
        self.evaluate_values(options)
        repeats = self.repeats.get_value(options)
        if not isinstance(repeats, int):
            repeats = resolve_integer_value(repeats, options)
        for _ in range(repeats):
            for item in self.evaluated_values:
                yield from resolve_item(item, options)

    def evaluate_values(self, options):
        """Evaluate values and store to evaluated_values"""
        self.evaluated_values = list(self.evaluate(options))

    def evaluate(self, options: dict):
        """Evaluate repeated sequence partially. Leaves Cycles intact."""
        self.local_options = options.copy()
        for item in self.values:
            if isinstance(item, Sequence):
                if isinstance(item, ListOperation):
                    yield from item.evaluate_tree(self.local_options, True)
                elif isinstance(item, RepeatedSequence):
                    yield item
                elif isinstance(item, Subdivision):
                    item.evaluate_values(options)
                    yield item
                elif isinstance(item, Eval):
                    yield from item.evaluate_values(options)
                else:
                    yield from item
            elif isinstance(item, Cyclic):
                yield item  # Return the cycle instead of values
            elif isinstance(item, Modification):
                self.local_options = self.local_options | item.as_options()
            elif isinstance(item, Rest):
                yield item.get_updated_item(self.local_options)
            elif isinstance(item, Range):
                yield from item.evaluate(self.local_options)
            elif isinstance(item, (Pitch, Chord, RomanNumeral)):
                yield item
            elif isinstance(item, (Event, RandomInteger)):
                yield Pitch(
                    pitch_class=item.get_value(self.local_options),
                    kwargs=self.local_options,
                )


@dataclass(kw_only=True)
class Euclid(Item):
    """Class for euclidean cycles"""

    pulses: int
    length: int
    onset: ListSequence
    offset: ListSequence = field(default=None)
    rotate: int = field(default=0)
    evaluated_values: list = field(default=None)

    def evaluate(self, options):
        """Evaluate values using euclidean spread"""
        onset_values = [
            val for val in self.onset.values if not isinstance(val, Whitespace)
        ]
        onset_length = len(onset_values)
        booleans = euclidian_rhythm(self.pulses, self.length, self.rotate)
        self.evaluated_values = []

        if self.offset is not None:
            offset_values = [
                val for val in self.offset.values if not isinstance(val, Whitespace)
            ]
            offset_length = len(offset_values)

        on_i = 0
        off_i = 0

        for i in range(self.length):
            if booleans[i]:
                value = onset_values[on_i % onset_length]
                on_i += 1
            else:
                if self.offset is None:
                    value = Rest(duration=options["duration"])
                else:
                    value = offset_values[off_i % offset_length]
                off_i += 1

            self.evaluated_values.append(value)
