"""
"""

import abc
import dataclasses
import typing
import warnings

import abjad  # type: ignore

from mutwo.parameters import abc as parameters_abc
from mutwo.parameters import notation_indicators
from mutwo.parameters import playing_indicators


class AbjadAttachment(abc.ABC):
    @abc.abstractmethod
    def process_leaves(
        self,
        leaves: typing.Tuple[abjad.Leaf, ...],
        previous_attachment: typing.Optional["AbjadAttachment"],
    ) -> typing.Tuple[abjad.Leaf, ...]:
        raise NotImplementedError()

    @property
    @abc.abstractmethod
    def is_active(self) -> bool:
        raise NotImplementedError()


class ToggleAttachment(AbjadAttachment):
    @abc.abstractmethod
    def process_leaf(
        self, leaf: abjad.Leaf, previous_attachment: typing.Optional["AbjadAttachment"]
    ) -> abjad.Leaf:
        raise NotImplementedError()

    def process_leaves(
        self,
        leaves: typing.Tuple[abjad.Leaf, ...],
        previous_attachment: typing.Optional["AbjadAttachment"],
    ) -> typing.Tuple[abjad.Leaf, ...]:
        if previous_attachment != self:
            return (self.process_leaf(leaves[0], previous_attachment),) + leaves[1:]
        else:
            return leaves


class BangAttachment(AbjadAttachment):
    @abc.abstractmethod
    def process_first_leaf(self, leaf: abjad.Leaf) -> abjad.Leaf:
        raise NotImplementedError()

    @abc.abstractmethod
    def process_last_leaf(self, leaf: abjad.Leaf) -> abjad.Leaf:
        raise NotImplementedError()

    @abc.abstractmethod
    def process_central_leaf(self, leaf: abjad.Leaf) -> abjad.Leaf:
        raise NotImplementedError()

    def process_leaves(
        self,
        leaves: typing.Tuple[abjad.Leaf, ...],
        previous_attachment: typing.Optional["AbjadAttachment"],
    ) -> typing.Tuple[abjad.Leaf, ...]:
        n_leaves = len(leaves)

        new_leaves = []

        if n_leaves > 0:
            new_leaves.append(self.process_first_leaf(leaves[0]))

        if n_leaves > 2:
            for leaf in leaves[1:-1]:
                new_leaves.append(self.process_central_leaf(leaf))

        if n_leaves > 1:
            new_leaves.append(self.process_last_leaf(leaves[-1]))

        return tuple(new_leaves)


class BangEachAttachment(BangAttachment):
    @abc.abstractmethod
    def process_leaf(self, leaf: abjad.Leaf) -> abjad.Leaf:
        raise NotImplementedError()

    def process_first_leaf(self, leaf: abjad.Leaf) -> abjad.Leaf:
        return self.process_leaf(leaf)

    def process_last_leaf(self, leaf: abjad.Leaf) -> abjad.Leaf:
        return self.process_leaf(leaf)

    def process_central_leaf(self, leaf: abjad.Leaf) -> abjad.Leaf:
        return self.process_leaf(leaf)


class BangFirstAttachment(BangAttachment):
    @abc.abstractmethod
    def process_leaf(self, leaf: abjad.Leaf) -> abjad.Leaf:
        raise NotImplementedError()

    def process_first_leaf(self, leaf: abjad.Leaf) -> abjad.Leaf:
        return self.process_leaf(leaf)

    def process_last_leaf(self, leaf: abjad.Leaf) -> abjad.Leaf:
        return leaf

    def process_central_leaf(self, leaf: abjad.Leaf) -> abjad.Leaf:
        return leaf


class BangLastAttachment(BangAttachment):
    @abc.abstractmethod
    def process_leaf(self, leaf: abjad.Leaf) -> abjad.Leaf:
        raise NotImplementedError()

    def process_first_leaf(self, leaf: abjad.Leaf) -> abjad.Leaf:
        return leaf

    def process_last_leaf(self, leaf: abjad.Leaf) -> abjad.Leaf:
        return self.process_leaf(leaf)

    def process_central_leaf(self, leaf: abjad.Leaf) -> abjad.Leaf:
        return leaf

    def process_leaves(
        self,
        leaves: typing.Tuple[abjad.Leaf, ...],
        previous_attachment: typing.Optional["AbjadAttachment"],
    ) -> typing.Tuple[abjad.Leaf, ...]:
        n_leaves = len(leaves)
        if n_leaves > 0:
            return leaves[:-1] + (self.process_last_leaf(leaves[-1]),)
        else:
            return leaves


class Arpeggio(playing_indicators.Arpeggio, BangFirstAttachment):
    def process_leaf(self, leaf: abjad.Leaf) -> abjad.Leaf:
        abjad.attach(abjad.Arpeggio(direction=self.direction), leaf)
        return leaf


class Articulation(playing_indicators.Articulation, BangEachAttachment):
    def process_leaf(self, leaf: abjad.Leaf) -> abjad.Leaf:
        abjad.attach(abjad.Articulation(self.name), leaf)
        return leaf


class Tremolo(playing_indicators.Tremolo, BangEachAttachment):
    def process_leaf(self, leaf: abjad.Leaf) -> abjad.Leaf:
        abjad.attach(
            abjad.StemTremolo(self.n_flags * (2 ** leaf.written_duration.flag_count)),
            leaf,
        )
        return leaf


class ArtificalHarmonic(playing_indicators.ArtificalHarmonic, BangEachAttachment):
    @staticmethod
    def _change_note_head_style(leaf: abjad.Chord) -> None:
        abjad.tweak(leaf.note_heads[1]).NoteHead.style = "#'harmonic"

    @staticmethod
    def _convert_and_test_leaf(leaf: abjad.Leaf) -> typing.Tuple[abjad.Leaf, bool]:
        # return True if artifical_harmonic can be attached and False if
        # artifical harmonic can't be attached

        if isinstance(leaf, abjad.Chord):
            try:
                assert len(leaf.note_heads) == 1
            except AssertionError:
                message = (
                    "Can't attach artifical harmonic on chord with more or less"
                    " than one pitch!"
                )
                warnings.warn(message)
                return leaf, False

            return leaf, True

        elif isinstance(leaf, abjad.Note):
            new_abjad_leaf = abjad.Chord([leaf.written_pitch], leaf.written_duration,)
            for indicator in abjad.get.indicators(leaf):
                if type(indicator) != dict:
                    abjad.attach(indicator, new_abjad_leaf)

            return new_abjad_leaf, True

        else:
            message = (
                "Can't attach artifical harmonic on abjad leaf '{}' of type '{}'!"
                .format(leaf, type(leaf))
            )
            warnings.warn(message)
            return leaf, False

    def _get_second_pitch(self, abjad_pitch: abjad.Pitch) -> abjad.Pitch:
        return abjad_pitch + self.n_semitones

    def process_leaf(self, leaf: abjad.Leaf) -> abjad.Leaf:
        leaf, is_attachable = ArtificalHarmonic._convert_and_test_leaf(leaf)
        if is_attachable:
            first_pitch = leaf.note_heads[0].written_pitch
            second_pitch = self._get_second_pitch(first_pitch)
            leaf.written_pitches = abjad.PitchSegment([first_pitch, second_pitch])
            ArtificalHarmonic._change_note_head_style(leaf)
        return leaf


class StringContactPoint(playing_indicators.StringContactPoint, ToggleAttachment):
    def process_leaf(
        self, leaf: abjad.Leaf, previous_attachment: typing.Optional["AbjadAttachment"]
    ) -> abjad.Leaf:
        string_contact_point_markup = abjad.StringContactPoint(
            self.contact_point
        ).markup
        if previous_attachment:
            if previous_attachment.contact_point == "pizzicato":  # type: ignore
                string_contact_point_markup = abjad.Markup(
                    [
                        abjad.markups.MarkupCommand(
                            "caps",
                            "arco {}".format(
                                " ".join(
                                    string_contact_point_markup.contents[0].arguments
                                )
                            ),
                        )
                    ]
                )

        abjad.attach(
            abjad.Markup(
                [abjad.markups.MarkupCommand("fontsize", -2.4)]
                + string_contact_point_markup.contents,
                direction="up",
            ),
            leaf,
        )
        return leaf

    def process_leaves(
        self,
        leaves: typing.Tuple[abjad.Leaf, ...],
        previous_attachment: typing.Optional["AbjadAttachment"],
    ) -> typing.Tuple[abjad.Leaf, ...]:
        # don't attach ordinario at start (this is the default playing technique)
        if previous_attachment is not None or self.contact_point != "ordinario":
            return super().process_leaves(leaves, previous_attachment)
        else:
            return leaves


class Pedal(playing_indicators.Pedal, ToggleAttachment):
    def process_leaf(
        self, leaf: abjad.Leaf, previous_attachment: typing.Optional["AbjadAttachment"]
    ) -> abjad.Leaf:
        if self.pedal_activity:
            pedal_class = abjad.StartPianoPedal
        else:
            pedal_class = abjad.StopPianoPedal

        abjad.attach(pedal_class(self.pedal_type), leaf)
        return leaf

    def process_leaves(
        self,
        leaves: typing.Tuple[abjad.Leaf, ...],
        previous_attachment: typing.Optional["AbjadAttachment"],
    ) -> typing.Tuple[abjad.Leaf, ...]:
        # don't attach pedal down at start
        if previous_attachment is not None or self.is_active:
            return super().process_leaves(leaves, previous_attachment)
        else:
            return leaves


class Hairpin(playing_indicators.Hairpin, ToggleAttachment):
    def process_leaf(
        self, leaf: abjad.Leaf, previous_attachment: typing.Optional["AbjadAttachment"]
    ) -> typing.Tuple[abjad.Leaf, ...]:
        if self.symbol == "!":
            abjad.attach(
                abjad.StopHairpin(), leaf,
            )
        else:
            abjad.attach(
                abjad.StartHairpin(self.symbol), leaf,
            )
        return leaf


class BartokPizzicato(parameters_abc.ExplicitPlayingIndicator, BangFirstAttachment):
    def process_leaf(self, leaf: abjad.Leaf) -> abjad.Leaf:
        abjad.attach(
            abjad.LilyPondLiteral("\\snappizzicato", format_slot="absolute_after"),
            leaf,
        )
        return leaf


class Fermata(playing_indicators.Fermata, BangFirstAttachment):
    def process_leaf(self, leaf: abjad.Leaf) -> abjad.Leaf:
        abjad.attach(
            abjad.Fermata(self.fermata_type), leaf,
        )
        return leaf


class NaturalHarmonic(parameters_abc.ExplicitPlayingIndicator, BangFirstAttachment):
    def process_leaf(self, leaf: abjad.Leaf) -> abjad.Leaf:
        abjad.attach(
            abjad.LilyPondLiteral("\\flageolet", directed="up", format_slot="after"),
            leaf,
        )

        return leaf


class Prall(parameters_abc.ExplicitPlayingIndicator, BangFirstAttachment):
    def process_leaf(self, leaf: abjad.Leaf) -> abjad.Leaf:
        abjad.attach(
            abjad.LilyPondLiteral("^\\prall", format_slot="after"), leaf,
        )
        return leaf


class Tie(parameters_abc.ExplicitPlayingIndicator, BangLastAttachment):
    def process_leaf(self, leaf: abjad.Leaf) -> abjad.Leaf:
        abjad.attach(
            abjad.Tie(), leaf,
        )
        return leaf


class LaissezVibrer(parameters_abc.ExplicitPlayingIndicator, BangLastAttachment):
    def process_leaf(self, leaf: abjad.Leaf) -> abjad.Leaf:
        abjad.attach(
            abjad.LaissezVibrer(), leaf,
        )
        return leaf


class BarLine(notation_indicators.BarLine, BangLastAttachment):
    def process_leaf(self, leaf: abjad.Leaf) -> abjad.Leaf:
        abjad.attach(
            abjad.BarLine(self.abbreviation), leaf,
        )
        return leaf


class Clef(notation_indicators.Clef, BangFirstAttachment):
    def process_leaf(self, leaf: abjad.Leaf) -> abjad.Leaf:
        abjad.attach(
            abjad.Clef(self.name), leaf,
        )
        return leaf


class Ottava(notation_indicators.Ottava, ToggleAttachment):
    def process_leaf(
        self, leaf: abjad.Leaf, previous_attachment: typing.Optional["AbjadAttachment"]
    ) -> abjad.Leaf:
        abjad.attach(
            abjad.Ottava(self.n_octaves, format_slot="before"), leaf,
        )
        return leaf

    def process_leaves(
        self,
        leaves: typing.Tuple[abjad.Leaf, ...],
        previous_attachment: typing.Optional["AbjadAttachment"],
    ) -> typing.Tuple[abjad.Leaf, ...]:
        # don't attach ottava = 0 at start (this is the default notation)
        if previous_attachment is not None or self.n_octaves != 0:
            return super().process_leaves(leaves, previous_attachment)
        else:
            return leaves


class Markup(notation_indicators.Markup, BangFirstAttachment):
    def process_leaf(self, leaf: abjad.Leaf) -> abjad.Leaf:
        abjad.attach(
            abjad.Markup(self.content), leaf,
        )
        return leaf


class MarginMarkup(notation_indicators.MarginMarkup, BangFirstAttachment):
    def process_leaf(self, leaf: abjad.Leaf) -> abjad.Leaf:
        command = "\\set {}.instrumentName = \\markup ".format(self.context)
        command += "{ " + self.content + " }"  # type: ignore
        abjad.attach(
            abjad.LilyPondLiteral(command), leaf,
        )
        return leaf


class Ornamentation(playing_indicators.Ornamentation, BangFirstAttachment):
    _direction_to_ornamentation_command = {
        "up": """
    #'((moveto 0 0)
      (lineto 0.5 0)
      (curveto 0.5 0 1.5 1.75 2.5 0)
      (lineto 3.5 0))""",
        "down": """
    #'((moveto 0 0)
      (lineto 0.5 0)
      (curveto 0.5 0 1.5 -1.75 2.5 0)
      (lineto 3.5 0))""",
    }

    def _make_markup(self) -> abjad.Markup:
        return abjad.Markup(
            [
                abjad.markups.MarkupCommand("vspace", -0.25),
                abjad.markups.MarkupCommand("fontsize", -4),
                abjad.markups.MarkupCommand("rounded-box", ["{}".format(self.n_times)]),
                abjad.markups.MarkupCommand("hspace", -0.4),
                abjad.markups.MarkupCommand(
                    "path",
                    0.25,
                    abjad.LilyPondLiteral(
                        self._direction_to_ornamentation_command[self.direction]  # type: ignore
                    ),
                ),
            ],
            direction="up",
        )

    def process_leaf(self, leaf: abjad.Leaf) -> abjad.Leaf:
        abjad.attach(self._make_markup(), leaf)
        return leaf


@dataclasses.dataclass()
class Dynamic(ToggleAttachment):
    dynamic_indicator: str = "mf"  # TODO(for future usage add typing.Literal)

    @property
    def is_active(self) -> bool:
        return True

    def process_leaf(
        self, leaf: abjad.Leaf, previous_attachment: typing.Optional["AbjadAttachment"]
    ) -> abjad.Leaf:
        abjad.attach(abjad.Dynamic(self.dynamic_indicator), leaf)
        return leaf


@dataclasses.dataclass()
class Tempo(BangFirstAttachment):
    reference_duration: typing.Optional[typing.Tuple[int, int]] = (1, 4)
    units_per_minute: typing.Optional[int] = 60
    textual_indication: typing.Optional[str] = None
    # TODO(for future usage add typing.Literal['rit.', 'acc.'])
    dynamic_change_indication: typing.Optional[str] = None
    stop_dynamic_change_indicaton: bool = False
    print_metronome_mark: bool = True

    @property
    def is_active(self) -> bool:
        return True

    def _attach_metronome_mark(self, leaf: abjad.Leaf) -> None:
        if self.print_metronome_mark:
            abjad.attach(
                abjad.MetronomeMark(
                    reference_duration=self.reference_duration,
                    units_per_minute=self.units_per_minute,
                    textual_indication=self.textual_indication,
                ),
                leaf,
            )

    def process_leaf(self, leaf: abjad.Leaf) -> abjad.Leaf:
        self._attach_metronome_mark(leaf)

        if self.dynamic_change_indication is not None:
            dynamic_change_indication = abjad.StartTextSpan(
                left_text=abjad.Markup(self.dynamic_change_indication)
            )
            abjad.attach(dynamic_change_indication, leaf)

        return leaf


class DynamicChangeIndicationStop(BangFirstAttachment):
    @property
    def is_active(self) -> bool:
        return True

    def process_leaf(self, leaf: abjad.Leaf) -> abjad.Leaf:
        abjad.attach(abjad.StopTextSpan(), leaf)
        return leaf
