from typing import Optional, Union, List

from musicscore.beat import Beat
from musicscore.chord import GraceChord
from musicscore.exceptions import VoiceHasNoBeatsError, VoiceHasNoParentError, VoiceIsFullError, \
    AddChordError, AlreadyFinalizedError
from musicscore.musictree import MusicTree
from musicscore.finalize import FinalizeMixin
from musicscore.quantize import QuantizeMixin
from musicscore.xmlwrapper import XMLWrapper
from musicxml.xmlelement.xmlelement import XMLVoice

__all__ = ['Voice']


class Voice(MusicTree, QuantizeMixin, FinalizeMixin, XMLWrapper):
    _ATTRIBUTES = {'number', 'leftover_chord', 'is_filled'}
    _ATTRIBUTES = _ATTRIBUTES.union(MusicTree._ATTRIBUTES)
    _ATTRIBUTES = _ATTRIBUTES.union(QuantizeMixin._ATTRIBUTES)
    XMLClass = XMLVoice

    def __init__(self, number=None, *args, **kwargs):
        super().__init__()
        self._xml_object = self.XMLClass(value_='1', *args, **kwargs)
        self._number = None
        self.number = number
        self._current_beat_index = None
        self._leftover_chord = None
        self._final_updated = False

    def _add_chord(self, chord: 'Chord') -> List['Chord']:
        """
        :param chord: :obj:`~musicscore.chord.Chord`, required
        :return: added chord or a list of split chords
        """
        if not self.get_children():
            raise VoiceHasNoBeatsError

        try:
            current_beat = self.get_children()[self.get_current_beat_index()]
        except IndexError:
            raise VoiceIsFullError(f'Voice number {self.value_} of Measure number {self.up.up.number} is full.')

        if isinstance(chord, GraceChord) and chord.position == 'after':
            return current_beat.add_child(chord)

        if current_beat.is_filled:
            self._current_beat_index += 1
            return self._add_chord(chord)
        else:
            return current_beat.add_child(chord)

    @property
    def is_filled(self) -> bool:
        """
        :return: ``True`` if voice has :obj:`~musicscore.beat.Beat` children and the last child is filled, else ``False``
        """
        if self.get_children():
            return self.get_children()[-1].is_filled
        else:
            return False

    @property
    def leftover_chord(self) -> Optional['Chord']:
        """
        :return: None or a :obj:`~musicscore.chord.Chord` which is left over after adding a chord to the voice.
        """
        return self._leftover_chord

    @leftover_chord.setter
    def leftover_chord(self, val):
        self._leftover_chord = val

    @property
    def number(self) -> Optional[int]:
        """
        :type: None or int. If None number is set to 1.
        :return: positive int or None
        """
        if self._number is None:
            return None
        object_value = self.xml_object.value_
        if object_value is not None:
            return int(object_value)

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

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

    def add_beat(self, beat_quarter_duration: Optional[Union['QuarterDuration', 'Fraction', int, float]] = 1) -> Beat:
        """
        Creates and adds a :obj:`~musicscore.beat.Beat` to voice

        :param beat_quarter_duration: if None beat_quarter_duration is set to 1.
        :return: :obj:`~musicscore.beat.Beat`
        """
        if self._finalized is True:
            raise AlreadyFinalizedError(self, 'add_beat')
        if beat_quarter_duration is None:
            beat_quarter_duration = 1
        return self.add_child(Beat(beat_quarter_duration))

    def add_chord(self, *args, **kwargs):
        raise AddChordError()

    def add_child(self, child: Beat) -> Beat:
        """
        Check and add child to list of children. Child's parent is set to self.

        :param child: :obj:`~musicscore.beat.Beat`
        :return: child
        :rtype: :obj:`~musicscore.beat.Beat`
        """
        if self._finalized is True:
            raise AlreadyFinalizedError(self, 'add_child')
        if not self.up:
            raise VoiceHasNoParentError('A child Beat can only be added to a Voice if voice has a Staff parent.')
        return super().add_child(child)

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

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

    def get_current_beat_index(self) -> int:
        """
        :return: First not filled child :obj:`~musicscore.beat.Beat`
        """
        if not self.get_children():
            raise ValueError('Voice has no beats.')
        else:
            if not self._current_beat_index:
                self._current_beat_index = 0
        return self._current_beat_index

    def get_current_beat(self) -> 'Beat':
        try:
            current_beat = self.get_children()[self.get_current_beat_index()]
        except IndexError:
            raise VoiceIsFullError()
        if current_beat.is_filled:
            self._current_beat_index += 1
            return self.get_current_beat()
        return current_beat

    def update_beats(self, *quarter_durations) -> Optional[List[Beat]]:
        """
        Creates and adds or replaces Beats.

        :param quarter_durations: if None and a measure as ancestor exists, this measure's
                                  :obj:`musicscore.time.Time.get_beats_quarter_durations()` method is called.
        :return: None if quarter_durations is None and no measures as ancestor exists, else list of created beats.
        """
        if not quarter_durations:
            if self.up and self.up.up:
                quarter_durations = self.up.up.time.get_beats_quarter_durations()
            else:
                return
        else:
            if len(quarter_durations) == 1 and hasattr(quarter_durations[0], '__iter__'):
                quarter_durations = quarter_durations[0]

        self.remove_children()

        for quarter_duration in quarter_durations:
            self.add_child(Beat(quarter_duration))
        return self.get_children()
