# This file is part of xenharmlib.
#
# xenharmlib is free software: you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# xenharmlib is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
# See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with xenharmlib. If not, see <https://www.gnu.org/licenses/>.

import numpy as np
from typing import Tuple
from ..core.tunings import EDOTuning
from ..core.notation import NatAccNotation
from ..core.symbols import SymbolArithmetic
from ..core.symbols import SymbolArithmeticSet
from ..core.notes import NatAccNote
from ..core.notes import NatAccNoteInterval
from ..core.note_scale import NatAccNoteScale
from ..exc import UnknownNoteSymbol
from ..exc import UnfittingNotation


# These are placeholders for future implementations


class UpDownNote(NatAccNote):
    pass


class UpDownNoteInterval(NatAccNoteInterval):
    pass


class UpDownNoteScale(NatAccNoteScale):
    pass


SMUFL_MAP = dict(
    [
        ("", "\uE261"),
        ("^", "\uE272"),
        ("^^", "\uE2D1"),
        ("^^^", "\uE2DB"),
        ("t", "\uE282"),
        ("#vvv", "\uE2D7"),
        ("#vv", "\uE2CD"),
        ("#v", "\uE2C3"),
        ("#", "\uE262"),
        ("#^", "\uE2C8"),
        ("#^^", "\uE2D2"),
        ("#^^^", "\uE2DC"),
        ("t#", "\uE283"),
        ("xvvv", "\uE2D8"),
        ("xvv", "\uE2CE"),
        ("xv", "\uE2C4"),
        ("x", "\uE263"),
        ("x^", "\uE2C9"),
        ("x^^", "\uE2D3"),
        ("x^^^", "\uE2DD"),
        ("v", "\uE2C2"),
        ("vv", "\uE2CC"),
        ("vvv", "\uE2D6"),
        ("d", "\uE280"),
        ("b^^^", "\uE2DA"),
        ("b^^", "\uE2D0"),
        ("b^", "\uE2C6"),
        ("b", "\uE260"),
        ("bv", "\uE2C1"),
        ("bvv", "\uE2CB"),
        ("bvvv", "\uE2D5"),
        ("db", "\uE281"),
        ("bb^^^", "\uE2D9"),
        ("bb^^", "\uE2CF"),
        ("bb^", "\uE2C5"),
        ("bb", "\uE264"),
        ("bbv", "\uE2C0"),
        ("bbvv", "\uE2CA"),
        ("bbvvv", "\uE2D4"),
    ]
)


class UpDownNotation(NatAccNotation):
    """
    UpDownNotation is an implementation of Kite Giedraitis' classic
    ups/down notation for EDOs 5-72. It differs from the paper in
    a couple of minor ways:

    * EDOs for which the paper does recommend subnotation are not
      generated by subnotation in this implementation. The class
      is a 'pure' implementation of the system, that does not
      resort to heuristics even if the pure implementation is
      unfitting for a specific EDO. In the future xenharmlib
      will provide a more generic subnotation wrapper class
      to make subnotation of trivial EDOs (like 2, 3, 4, 6)
      or special EDOs like 8-EDO possible.

    * EDOs that are only mentioned in the variant of a flattened
      fifth (like 13b, 18b) are calculated in their sharp variant
      if EDOTuning is used. In order to generate a notation for
      the flattened variant you have to use the EDOBTuning class
      which uses the second best fifth as it's designated fifth.

    * In interval naming the mid symbol (~) and related symbols
      (like ^~, v~) are not implemented, because they introduce
      ambigouity when transposing notes (There is e.g. no strict
      definition whether in 31-EDO C0 transposed by ~3 results in
      the note vE or ^Em)
    """

    def __init__(
        self,
        tuning,
        note_cls=UpDownNote,
        note_interval_cls=UpDownNoteInterval,
        note_scale_cls=UpDownNoteScale,
    ):

        if not isinstance(tuning, EDOTuning):
            raise UnfittingNotation('UpDownNotation only supports EDO tunings')

        super().__init__(
            tuning,
            note_cls,
            note_interval_cls,
            note_scale_cls,
        )

        self._init_naturals()

        if tuning.sharpness == 0:
            self._init_perfect_edo_acc()
            self._init_perfect_edo_intervals()
        else:
            self._init_imperfect_edo_acc()
            self._init_imperfect_edo_intervals()

    @property
    def edo_category(self) -> str:
        """
        Returns the category of this EDO ('trivial', 'pentatonic',
        'supersharp', 'superflat', 'perfect' or 'diatonic')

        * trivial EDOs are subsets of 12-EDO (EDOs 2, 3, 4, 6)
        * diatonic EDOs have a minor second interval that is
          smaller than the major second interval (like 12-EDO)
        * in perfect EDOs minor second and major second are
          identical (these EDOs do not have sharps or flats)
        * pentatonic EDOs have a zero-size minor second interval
          (F=E, B=C)
        * supsharp EDOs have a negative minor second interval
        * superflat EDOs have a minor second interval that is
          bigger than the major second interval
        """

        tuning = self.tuning
        major2_size = (tuning.fifth.pitch_index * 2) % len(tuning)
        minor2_size = major2_size - tuning.sharpness

        if len(tuning) in {2, 3, 4, 6}:
            return 'trivial'

        if minor2_size == 0:
            return 'pentatonic'

        if minor2_size < 0:
            return 'supersharp'

        if minor2_size > major2_size:
            return 'superflat'

        if minor2_size == major2_size:
            return 'perfect'

        if minor2_size < major2_size:
            return 'diatonic'

        # this is just to make mypy happy
        return 'unknown'

    def _init_naturals(self):
        """
        This method initializes the naturals of this notation
        """

        tuning = self.tuning
        tuning_len = len(tuning)

        # in order to get the pitch indices of the naturals we
        # define that C should refer to the pitch index 0 and
        # then move forward (and once backward for F) in the
        # pitch class space using the cycle of fiths as step
        # width to receive G, D, A, E, B

        FIFTH = tuning.fifth.pc_index

        naturals = [
            ('C', (0 * FIFTH) % tuning_len),  # C
            ('D', (2 * FIFTH) % tuning_len),  # D
            ('E', (4 * FIFTH) % tuning_len),  # E
            ('F', (-1 * FIFTH) % tuning_len),  # F
            ('G', (1 * FIFTH) % tuning_len),  # G
            ('A', (3 * FIFTH) % tuning_len),  # A
            ('B', (5 * FIFTH) % tuning_len),  # B
        ]

        if self.edo_category == 'pentatonic':

            # in pentatonic EDOs the minor second is tempered
            # out making (B, 0) the same as (C, 1). since we
            # constructed the naturals with modulo arithmetic
            # the value for natc_pitch_index of B will be 0
            # which is the correct pitch class but not the
            # correct pitch index. In pentatonic EDOs (B, 0)
            # reaches into the second base interval, making
            # the correct pitch index for (B, 0) the number
            # of divisions of the respective tuning

            naturals[-1] = ('B', tuning_len)

        if self.edo_category == 'supersharp':

            # supersharp EDOs are even a bit weirder insofar
            # as m2 < 0. This makes E bigger than F and (B, 0)
            # bigger than (C, 1). in the original paper
            # supersharp EDOs are somewhat omitted (probably
            # because of their weirdness in this regard).
            # 8-EDO is given as a subnotation of 24-EDO and
            # other supersharp EDOs are only mentioned in
            # their flattened variant (like 13b, 18b, etc)

            # we will not balance the weird properties here with
            # some heuristic but calculate everything strictly
            # by the numbers. modifications should be done
            # with a dedicated EDOBTuning or SubNotation class
            # in the future

            # (B, 0) reaches even further into the second base
            # interval than with pentatonic EDOs. we translate
            # the pitch class to the pitch index in a similar
            # manner to the pentatonic EDOs

            naturals[-1] = ('B', naturals[-1][1] + tuning_len)

        for natc_symbol, natc_pitch_index in naturals:
            self.append_natural(natc_symbol, natc_pitch_index)

    def _init_perfect_edo_acc(self):
        """
        This method initializes the accidentals for perfect
        EDOs (sharpness=0) in which m2 = M2 and sharps and
        flats do not exist
        """

        acc_arith = SymbolArithmetic(allow_empty=True)

        acc_arith.add_symbol('^', (1,))
        acc_arith.add_symbol('v', (-1,))
        self.acc_symbol_code = acc_arith

    def _init_imperfect_edo_acc(self):
        """
        This method initializes accidentals for imperfect
        EDOs (sharpness != 0) in which m2 != M2 and sharps
        and flats are necessary
        """

        sharpness = self.tuning.sharpness
        acc_arith = SymbolArithmetic(dimensions=2, allow_empty=True)

        acc_arith.add_symbol('#', (sharpness, 0))
        acc_arith.add_symbol('b', ((-1) * sharpness, 0))
        acc_arith.add_symbol('x', (2 * sharpness, 0))

        if abs(sharpness) != 1:
            acc_arith.add_symbol("^", (0, 1))
            acc_arith.add_symbol("v", (0, -1))

        self.acc_symbol_code = acc_arith

    def _init_perfect_edo_intervals(self):
        """
        This method initializes interval names for perfect
        EDOs (sharpness 0) in which no major/minor intervals
        exist
        """

        p_arith = SymbolArithmeticSet()

        p_sub_arith_zero = SymbolArithmetic()
        p_sub_arith_zero.add_symbol(
            'P', (0,), min_occurence=1, max_occurence=1
        )
        p_arith.add_arithmetic(p_sub_arith_zero)

        p_sub_arith_ud = SymbolArithmetic()
        p_sub_arith_ud.add_symbol("^", (1,))
        p_sub_arith_ud.add_symbol("v", (-1,))
        p_arith.add_arithmetic(p_sub_arith_ud)

        for nat_index_diff in range(0, self.nat_count):
            self.set_interval_symbol_code(nat_index_diff, p_arith)

    def _init_imperfect_edo_intervals(self):
        """
        This method initializes interval names for imperfect
        EDOs (sharpness != 0) in which intervals have two
        different qualities (perfect for fifth and fourth,
        major/minor for others)
        """

        sharpness = self.tuning.sharpness

        # ------------------------------------------------
        # 1) Arithmetics for interval quality 'perfect'
        # ------------------------------------------------

        # Interval notation for perfect intervals (Unison, Fifth, Fourth)
        # is assembled out of 4 different symbol arithmetics.
        #
        #   * a trivial arithmetic with only one symbol P
        #   * an arithmetic with only ups and downs
        #   * an arithmetic with symbols A, ^, v
        #   * an arithmetic with symbols d, ^, v

        # we will give a graphical example for each subarithmetic for
        # sharpness 2 to accompany the generation of the symbol codes

        #       -2         -1         0          1          2
        #  --------------------------------------------------------
        #  |          |          | (+0, +0) |          |          |
        #  --------------------------------------------------------
        #                             P

        p_arith = SymbolArithmeticSet(dimensions=2)

        p_sub_arith_zero = SymbolArithmetic(dimensions=2)
        p_sub_arith_zero.add_symbol(
            'P', (0, 0), min_occurence=1, max_occurence=1
        )
        p_arith.add_arithmetic(p_sub_arith_zero)

        if sharpness != 1:

            # on sharpness 1 perfect intervals do not have ups and downs,
            # because it would be the same as using A and d.

            #       -2         -1         0          1          2
            #  --------------------------------------------------------
            #  | (+0, -2) | (+0, -1) |          | (+0, +1) | (+0, +2) |
            #  --------------------------------------------------------
            #       vv          v                    ^          ^^

            p_sub_arith_ud = SymbolArithmetic(dimensions=2)
            p_sub_arith_ud.add_symbol("v", (0, -1))
            p_sub_arith_ud.add_symbol("^", (0, 1))
            p_arith.add_arithmetic(p_sub_arith_ud)

        # on non-perfect EDOs intervals with perfect quality can be
        # augmented and diminished. we show different ways to reach
        # the same total pitch alteration (the sum of the vector)

        #       0         1           2          3           4
        #  --------------------------------------------------------
        #  | (+2, -2) | (+2, -1) | (+2, +0) | (+2, +1) | (+4, +0) |
        #  --------------------------------------------------------
        #      vvA        vA          A         A^          AA

        #  --------------------------------------------------------
        #  | (+0, -2) | (+0, -1) | (+2, +0) | (+2, +1) | (+2, +2) |
        #  --------------------------------------------------------
        #      vvA        vA          A         A^         A^^

        p_sub_arith_aug = SymbolArithmetic(dimensions=2)
        p_sub_arith_aug.add_symbol(
            "A", (sharpness, 0), min_occurence=1, position=1
        )
        p_arith.add_arithmetic(p_sub_arith_aug)

        #       -4         -3         -2         -1           0
        #  --------------------------------------------------------
        #  | (+4, +0) | (-2, -1) | (-2, +0) | (-2, +1) | (-2, +2) |
        #  --------------------------------------------------------
        #       dd        vd          d          ^d         ^^d

        #  --------------------------------------------------------
        #  | (+2, -2) | (-2, -1) | (-2, +0) | (-2, +1) | (-2, +2) |
        #  --------------------------------------------------------
        #      vvd        vd          d          ^d         ^^d

        p_sub_arith_dim = SymbolArithmetic(dimensions=2)
        p_sub_arith_dim.add_symbol(
            "d", ((-1) * sharpness, 0), min_occurence=1, position=1
        )
        p_arith.add_arithmetic(p_sub_arith_dim)

        if sharpness != 1:

            p_sub_arith_aug.add_symbol('^', (0, 1), position=0)
            p_sub_arith_aug.add_symbol('v', (0, -1), position=0)
            p_sub_arith_dim.add_symbol('^', (0, 1), position=0)
            p_sub_arith_dim.add_symbol('v', (0, -1), position=0)

        # set perfect arithmetic for C, F and G

        for nat_index_diff in [0, 3, 4]:
            self.set_interval_symbol_code(nat_index_diff, p_arith)

        # ------------------------------------------------
        # 2) Arithmetics for interval quality 'imperfect'
        # ------------------------------------------------

        # Interval notation for imperfect intervals (Second, Third,
        # Sixth, Seventh) is assembled out of 4 different symbol
        # arithmetics:
        #
        #   * a major arithmetic with exactly one 'M' and ups and downs
        #   * a minor arithmetic with exactly one 'm' and ups and downs
        #   * an augmented arithmetic with symbols A, ^, v
        #   * a diminished arithmetic with symbols d, ^, v

        imp_arith = SymbolArithmeticSet(dimensions=2)

        # Major arithmetic

        #       0         1            0          3           4
        #  --------------------------------------------------------
        #  | (+0, -2) | (+0, -1) | (+0, +0) | (+0, +1) | (+0, +2) |
        #  --------------------------------------------------------
        #      vvM        vM          M         ^M         ^^M

        imp_maj_arith = SymbolArithmetic(dimensions=2)
        imp_maj_arith.add_symbol(
            'M', (0, 0), min_occurence=1, max_occurence=1, position=1
        )

        if sharpness != 1:
            imp_maj_arith.add_symbol('^', (0, 1), position=0)
            imp_maj_arith.add_symbol('v', (0, -1), position=0)

        imp_arith.add_arithmetic(imp_maj_arith)

        # Minor arithmetic

        #      -4          -3         -2         -1         0
        #  --------------------------------------------------------
        #  | (-2, -2) | (-2, -1) | (-2, +0) | (-2, +1) | (-2, +2) |
        #  --------------------------------------------------------
        #      vvm        vm          m         ^m          ^m

        imp_min_arith = SymbolArithmetic(
            dimensions=2, offset=((-1) * sharpness, 0)
        )
        imp_min_arith.add_symbol(
            'm', (0, 0), min_occurence=1, max_occurence=1, position=1
        )

        if sharpness != 1:
            imp_min_arith.add_symbol('^', (0, 1), position=0)
            imp_min_arith.add_symbol('v', (0, -1), position=0)

        imp_arith.add_arithmetic(imp_min_arith)

        # Augmented arithmetic

        #       0          1          2          3          4
        #  --------------------------------------------------------
        #  | (+2, -2) | (+2, -1) | (+2, +0) | (+2, +1) | (+4, +0) |
        #  --------------------------------------------------------
        #      vvA        vA          A         ^A         AA

        #  --------------------------------------------------------
        #  | (+2, -2) | (+2, -1) | (+2, +0) | (+2, +1) | (+2, +2) |
        #  --------------------------------------------------------
        #      vvA        vA          A         ^A         ^^A

        imp_aug_arith = SymbolArithmetic(dimensions=2)
        imp_aug_arith.add_symbol(
            'A', (sharpness, 0), min_occurence=1, position=1
        )

        if sharpness != 1:
            imp_aug_arith.add_symbol('^', (0, 1), position=0)
            imp_aug_arith.add_symbol('v', (0, -1), position=0)

        imp_arith.add_arithmetic(imp_aug_arith)

        # diminished arithmetic

        #       -6         -5         -4         -3         -2
        #  --------------------------------------------------------
        #  | (-6, +0) | (-4, -1) | (-4, +0) | (-4, +1) | (-4, +2) |
        #  --------------------------------------------------------
        #       dd        vd          d         ^d         ^^d

        #  --------------------------------------------------------
        #  | (-4, -2) | (-4, -1) | (-4, +0) | (-4, +1) | (-4, +2) |
        #  --------------------------------------------------------
        #      vvd        vd          d         ^d         ^^d

        imp_dim_arith = SymbolArithmetic(
            dimensions=2, offset=((-1) * sharpness, 0)
        )
        imp_dim_arith.add_symbol(
            'd', ((-1) * sharpness, 0), min_occurence=1, position=1
        )

        if sharpness != 1:
            imp_dim_arith.add_symbol('^', (0, 1), position=0)
            imp_dim_arith.add_symbol('v', (0, -1), position=0)

        imp_arith.add_arithmetic(imp_dim_arith)

        # add imperfect interval arithmetic to the
        # corresponding natural index difference

        # 1 ^= major second
        # 2 ^= major third
        # 5 ^= major sixth
        # 6 ^= major seventh

        for nat_index_diff in [1, 2, 5, 6]:
            self.set_interval_symbol_code(nat_index_diff, imp_arith)

    def parse_pc_symbol(
        self, pc_symbol: str
    ) -> Tuple[str, str, int, Tuple[int]]:
        """
        Parses a pitch class symbol into its natural class symbol
        part and its accidental symbol part. Returns a 4-tuple
        (natc_symbol, acc_symbol, natc_index, acc_vactor) with
        the parsing result.
        """

        # Classic UpDownNotation puts ^ and v before (!) the natural
        # name. The base class however assumes that all accidentals
        # are put after the natural, so we need to slightly change
        # the logic by overwriting parse_pc_symbol

        acc_head = ''

        while pc_symbol.startswith('^') or pc_symbol.startswith('v'):
            acc_head += pc_symbol[0]
            pc_symbol = pc_symbol[1:]

        acc_head_value = self.acc_symbol_code.get_vector(acc_head)
        parse_result = super().parse_pc_symbol(pc_symbol)

        natc_symbol = parse_result[0]
        acc_tail = parse_result[1]
        natc_index = parse_result[2]
        acc_tail_value = parse_result[3]

        if 'v' in acc_tail or '^' in acc_tail:
            raise UnknownNoteSymbol(
                'Up and down arrows must be placed before the natural symbol'
            )

        acc_symbol = acc_head + acc_tail
        acc_value = tuple(np.add(acc_head_value, acc_tail_value))

        return natc_symbol, acc_symbol, natc_index, acc_value

    def gen_pc_symbol(
        self, nat_index: int, acc_vector: Tuple[int]
    ) -> Tuple[str, str, str]:
        """
        Creates a pitch class symbol from a natural class index and an
        accidental vector.

        The method will return a tuple of 3 with the following semantics:
        * First element will be the pitch class symbol
        * Second element will be the natural class symbol
        * Third element will be the accidental symbol string

        :param natc_index: A natural class index of a note
        :param acc_vector: An accidental value vector
        """

        # again.. because up/downs are prefix accidentals in
        # classic UpDownNotation we need to slightly change
        # the base class mechanism which assumes post-fix
        # accidentals

        natc_index = nat_index % self.nat_count
        natc_symbol = self.get_natc_symbol(natc_index)
        acc_symbol = self.get_acc_symbol(acc_vector)

        acc_head = ''
        acc_tail = ''

        for letter in acc_symbol:
            if letter in {'^', 'v'}:
                acc_head += letter
            else:
                acc_tail += letter

        return (
            acc_head + natc_symbol + acc_tail,
            natc_symbol,
            acc_head + acc_tail,
        )

    @property
    def name(self):
        return f'{self.__class__.__name__}({self.tuning.name})'

    def __repr__(self):
        return f'{self.__class__.__name__}({self.tuning.name})'
