from typing import Optional, Set, List

from musicscore.finalize import FinalizeMixin
from musicscore.quantize import QuantizeMixin
from musicxml.xmlelement.xmlelement import XMLStaff

from musicscore.clef import Clef
from musicscore.exceptions import StaffHasNoParentError, AlreadyFinalizedError
from musicscore.musictree import MusicTree
from musicscore.voice import Voice
from musicscore.xmlwrapper import XMLWrapper

__all__ = ['Staff']


class Staff(MusicTree, QuantizeMixin, FinalizeMixin, XMLWrapper):
    _ATTRIBUTES = {'clef', 'default_clef', 'number'}
    _ATTRIBUTES = _ATTRIBUTES.union(MusicTree._ATTRIBUTES)
    _ATTRIBUTES = _ATTRIBUTES.union(QuantizeMixin._ATTRIBUTES)
    XMLClass = XMLStaff

    def __init__(self, number=None, clef=None, **kwargs):
        super().__init__()
        self._xml_object = self.XMLClass(value_=1, **kwargs)
        self._number = None
        self._clef = None
        self.clef = clef
        self.number = number

    @property
    def clef(self) -> Clef:
        """
        :type: :obj:`~musicscore.clef.Clef`
        :return: Clef associated with the staff.
        :rtype: :obj:`~musicscore.clef.Clef`
        """
        if not self._clef:
            if self.get_previous_staff() and self.get_previous_staff().clef:
                self._clef = self.get_previous_staff().clef.__copy__()
                self._clef.show = False
        return self._clef

    @clef.setter
    def clef(self, val):
        if val is not None and not isinstance(val, Clef):
            raise TypeError
        if val and not val.number and self.clef:
            val.number = self.clef.number
        self._clef = val

    @property
    def number(self):
        """
        :type: None or int. If None number is set to 1.
        :return: positive int or None
        """
        if self._number is not None:
            return self.xml_object.value_
        else:
            return self._number

    @number.setter
    def number(self, val):
        self._number = val
        if val is None:
            self.xml_object.value_ = 1
        else:
            self.xml_object.value_ = val

    @XMLWrapper.xml_object.getter
    def xml_object(self) -> XMLClass:
        return super().xml_object

    def add_child(self, child: Voice) -> Voice:
        """
        - Adds a :obj:`~musicscore.voice.Voice` as child to staff.
        - If voice number is ``None``, it is determined as length of children + 1.
        - If voice number is already set an is not equal to length of children + 1. a ``ValueError`` is raised.
        - If Staff has no parent :obj:`~musicscore.measure.Measure` ``StaffHasNoParentError`` is raised.
        - After adding voice its :obj:`~musicscore.voice.Voice.update_beats()` is called.

        :param child: :obj:`~musicscore.voice.Voice` , required
        :return: added :obj:`~musicscore.voice.Voice`
        """
        if self._finalized is True:
            raise AlreadyFinalizedError(self, 'add_child')
        self._check_child_to_be_added(child)

        if not self.up:
            raise StaffHasNoParentError('A child Voice can only be added to a Staff if staff has a Measure parent.')

        if child.number is not None and child.number != len(self.get_children()) + 1:
            raise ValueError(f'Voice number must be None or {len(self.get_children()) + 1}')
        if child.number is None:
            child.number = len(self.get_children()) + 1
        else:
            child.number = len(self.get_children()) + 1

        child._parent = self
        self._children.append(child)

        return child

    def add_voice(self, voice_number: Optional[int] = None) -> Voice:
        """
        - Creates and adds a new :obj:`~musicscore.voice.Voice` object as child to staff.
        - If voice number is greater than length of children + 1 all missing voices are created and added first.

        :param voice_number: positive int or None. If ``None`` voice number is determined as length of children + 1.
        :return: new :obj:`~musicscore.voice.Voice`
        """
        if self._finalized is True:
            raise AlreadyFinalizedError(self, 'add_voice')
        if voice_number is None:
            voice_number = len(self.get_children()) + 1

        voice_object = self.get_voice(voice_number=voice_number)

        if voice_object is None:
            for _ in range(voice_number - len(self.get_children())):
                voice_object = self.add_child(Voice())
                voice_object.update_beats()
            return voice_object

        voice_object.update_beats()
        return voice_object

    def get_children(self) -> List[Voice]:
        """
        :return: list of added children.
        :rtype: List[:obj:`~musicscore.voice.Voice`]
        """
        return super().get_children()

    def get_parent(self) -> 'Measure':
        """
        :return: parent
        :rtype: :obj:`~musicscore.measure.Measure`
        """
        return super().get_parent()

    def get_previous_staff(self) -> Optional['Staff']:
        """
        :return: :obj:`Staff` with the same number in previous :obj:`~musicscore.measure.Measure` if existing else ``None``
        """
        if self.up and self.up.previous:
            my_index = self.up.get_children().index(self)
            try:
                return self.up.previous.get_children()[my_index]
            except IndexError:
                return None
        return None

    def get_last_pitch_steps_with_accidentals(self) -> Set[str]:
        """
        :return: a set of pitch steps with not natural accidental. This method is used to take track of accidental signs which needs to
                 be shown hidden
        :rtype:  Set[str]
        """
        output = set()
        for v in self.get_children():
            if v.get_chords():
                last_chord = v.get_chords()[-1]
                if not last_chord.is_rest:
                    for m in last_chord.midis:
                        if m.accidental.sign != 'natural':
                            step = m.accidental.get_pitch_parameters()[0]
                            if step not in output:
                                output.add(step)
        return output
