import unicodedata

import attr
from clldutils.misc import nfilter, lazyproperty

from pyclts.util import norm

__all__ = [
    'is_valid_sound',
    'Symbol',
    'Sound',
    'Consonant',
    'Vowel',
    'Tone',
    'Marker',
    'Diphthong',
    'Cluster',
    'UnknownSound']
EXCLUDE_FEATURES = [
    'apical',
    'laminal',
    'ejective',
    'with_falling_tone',
    'with_extra-low_tone',
    'with_extra-high_tone',
    'with_falling_tone',
    'with_low_tone',
    'with_global_fall',
    'with_global_rise',
    'with_high_tone',
    'with_mid_tone',
    'with_rising_tone',
    'with_upstep'
]


def is_valid_sound(sound, ts):
    """Check the consistency of a given transcription system conversino"""
    if isinstance(sound, (Marker, UnknownSound)):
        return False
    s1 = ts[sound.name]
    s2 = ts[sound.s]
    return s1.name == s2.name and s1.s == s2.s


@attr.s(cmp=False)
class Symbol(object):
    ts = attr.ib()
    grapheme = attr.ib()
    source = attr.ib(default=None)
    generated = attr.ib(default=False, validator=attr.validators.instance_of(bool))
    note = attr.ib(default=None)

    @lazyproperty
    def type(self):
        return self.__class__.__name__.lower()

    def __str__(self):
        return self.grapheme

    def __eq__(self, other):
        """
        In the absence of features, we consider symbols equal, if they belong to the same
        system and are represented by the same grapheme.
        """
        return self.ts.id == other.ts.id and self.grapheme == other.grapheme

    @property
    def name(self):
        return None

    @property
    def uname(self):
        "Return unicode name(s) for a character set."
        try:
            return ' / '.join(unicodedata.name(ss) for ss in str(self))
        except TypeError:
            return '-'
        except ValueError:
            return '?'

    @property
    def codepoints(self):
        "Return unicode codepoint(s) for a grapheme."
        return ' '.join('U+' + ('000' + hex(ord(x))[2:])[-4:] for x in str(self))


@attr.s(cmp=False)
class UnknownSound(Symbol):
    pass


@attr.s(cmp=False, repr=False)
class Sound(Symbol):
    """
    Sound object stores basic features of the individual sound objects.
    """
    base = attr.ib(default=None)
    alias = attr.ib(default=None)
    normalized = attr.ib(default=None)
    unknown = attr.ib(default=None)
    stress = attr.ib(default=None)

    _name_order = []
    _write_order = dict(pre=[], post=[])

    def __eq__(self, other):
        if isinstance(other, Sound):
            return self.name == other.name
        return False

    def __repr__(self):
        return '<{0}.{1}: {2}>'.format(
            self.__module__, self.__class__.__name__, self.name)

    def __add__(self, other):
        return str(self) + str(other)

    def __hash__(self):
        return hash(self.name)

    @property
    def s(self):
        return str(self)

    def _features(self):
        return nfilter(getattr(self, p, None) for p in self._name_order)

    @property
    def featuredict(self):
        return {f: getattr(self, f, None) for f in self._name_order}

    @property
    def featureset(self):
        return frozenset(self._features() + [self.type])

    def similarity(self, other):
        f1, f2 = self.featureset, other.featureset
        return len(f1.intersection(f2)) / len(f1.union(f2))

    def __str__(self):
        """
        Return the reference representation of the sound.

        Note
        ----
        We first try to return the non-alias value in our data. If this fails,
        we create the sound based on it's feature representation.
        """
        # generated sounds need to be re-produced for double-checking
        if not self.generated:
            if not self.alias and self.grapheme in self.ts.sounds:
                return self.grapheme
            elif self.alias and self.featureset in self.ts.features:
                return str(self.ts.features[self.featureset])
            # this can usually not happen, as we catch these errors when loading a ts!
            raise ValueError(
                'Orphaned alias {0}'.format(self.grapheme))  # pragma: no cover

        # search for best base-string
        elements = [f for f in self._features() if f not in EXCLUDE_FEATURES] + [self.type]
        base_str = self.base or '<?>'
        base_graphemes = []
        while elements:
            base = self.ts.features.get(frozenset(elements))
            if base:
                base_graphemes.append(base.grapheme)
            elements.pop(0)
        base_str = base_graphemes[-1] if base_graphemes else base_str or '<?>'
        base_vals = {
            self.ts._feature_values[elm] for elm in
            self.ts.sounds[base_str].name.split(' ')[:-1]} if \
            base_str != '<?>' else {}
        out = []
        for p in self._write_order['pre']:
            if p not in base_vals and getattr(self, p, '') in self._features():
                out.append(
                    norm(self.ts.features[self.type].get(getattr(self, p, ''), '<!>')))
        out.append(base_str)
        for p in self._write_order['post']:
            if p not in base_vals and getattr(self, p, '') in self._features():
                out.append(
                    norm(self.ts.features[self.type].get(getattr(self, p, ''), '<!>')))
        return ''.join(out)

    @property
    def name(self):
        return ' '.join([f or '' for f in self._features()] + [self.type])

    @property
    def table(self):
        """Returns the tabular representation of the sound as given in our data
        """
        tbl = []
        features = [
            f for f in self._name_order if f not in self.ts.columns[self.type]]
        # make sure to mark generated sounds
        if self.generated and self.s != self.source:
            tbl += [str(self) + ' | ' + self.source]
        else:
            tbl += [str(self)]
        for name in self.ts.columns[self.type][1:]:
            if name != 'extra' and name != 'alias':
                tbl += [getattr(self, name) or '']
            elif name == 'alias':
                tbl += ['+' if getattr(self, name) else '']
            else:
                bundle = []
                for f in features:
                    val = getattr(self, f)
                    if val:
                        bundle += ['{0}:{1}'.format(f, val)]
                tbl += [','.join(bundle)]
        return tbl


@attr.s(cmp=False)
class Marker(Symbol):
    alias = attr.ib(default=None)
    feature = attr.ib(default=None)
    value = attr.ib(default=None)
    unknown = attr.ib(default=None)

    @property
    def name(self):
        return self.grapheme

    @property
    def featureset(self):
        return frozenset([self.grapheme, self.type])


@attr.s(cmp=False, repr=False)
class Consonant(Sound):

    # features follow basic information about IPA from various sources, they
    # are potentially not yet exhaustive and should be updated at some point
    manner = attr.ib(default=None)
    place = attr.ib(default=None)
    aspiration = attr.ib(default=None)
    labialization = attr.ib(default=None)
    palatalization = attr.ib(default=None)
    preceding = attr.ib(default=None)
    velarization = attr.ib(default=None)
    duration = attr.ib(default=None)
    phonation = attr.ib(default=None)
    release = attr.ib(default=None)
    syllabicity = attr.ib(default=None)
    nasalization = attr.ib(default=None)
    glottalization = attr.ib(default=None)
    pharyngealization = attr.ib(default=None)
    ejection = attr.ib(default=None)
    voicing = attr.ib(default=None)
    breathiness = attr.ib(default=None)
    creakiness = attr.ib(default=None)
    sibilancy = attr.ib(default=None)
    laterality = attr.ib(default=None)
    laminality = attr.ib(default=None)
    articulation = attr.ib(default=None)
    raising = attr.ib(default=None)
    relative_articulation = attr.ib(default=None)

    # write order determines how consonants are written according to their
    # features, so this normalizes the order of diacritics preceding and
    # following the base part of the consonant
    _write_order = dict(
        pre=['preceding'],
        post=[
            'raising', 'relative_articulation',
            'laminality', 'creakiness', 'phonation', 'ejection', 'syllabicity', 'voicing',
            'articulation',
            'nasalization', 'palatalization', 'labialization',
            'breathiness', 'aspiration', 'glottalization', 'velarization',
            'pharyngealization', 'release', 'duration'])
    _name_order = [
        'raising', 'relative_articulation',
        'articulation', 'preceding', 'syllabicity', 'nasalization', 'palatalization',
        'labialization', 'glottalization', 'aspiration', 'velarization',
        'pharyngealization', 'duration', 'release', 'voicing', 'creakiness',
        'breathiness', 'phonation', 'laminality', 'place', 'ejection', 'laterality',
        'sibilancy', 'manner']


@attr.s(cmp=False, repr=False)
class ComplexSound(Sound):
    from_sound = attr.ib(default=None)
    to_sound = attr.ib(default=None)

    def __str__(self):
        return str(self.from_sound) + str(self.to_sound)

    @property
    def name(self):
        n1 = ' '.join(self.from_sound.name.split(' ')[:-1])
        n2 = ' '.join(self.to_sound.name.split(' ')[:-1])
        return 'from ' + n1 + ' to ' + n2 + ' ' + self.type

    @classmethod
    def from_sounds(cls, source, sound1, sound2, ts):
        return cls(
            source=source,
            grapheme=sound1.grapheme + sound2.grapheme,
            from_sound=sound1,
            to_sound=sound2,
            ts=ts,
            generated=True,
            stress=sound1.stress or sound2.stress
        )

    @property
    def table(self):
        """Overwrite the table attribute for complex sounds"""
        return [self.grapheme, self.from_sound.name, self.to_sound.name]


@attr.s(cmp=False, repr=False)
class Cluster(ComplexSound):
    """
    A cluster of two consonants whose manner is either plosive or implosive.

    Notes
    -----
    To keep the search space low and to avoid that users start defining too
    invalid sound clusters, we restrict the ```manner``` attribute of the two
    sounds to ```plosive``` and ```implosive```.
    """


@attr.s(cmp=False, repr=False)
class Vowel(Sound):
    roundedness = attr.ib(default=None)
    height = attr.ib(default=None)
    nasalization = attr.ib(default=None)
    frication = attr.ib(default=None)
    duration = attr.ib(default=None)
    voicing = attr.ib(default=None)
    breathiness = attr.ib(default=None)
    creakiness = attr.ib(default=None)
    release = attr.ib(default=None)
    syllabicity = attr.ib(default=None)
    pharyngealization = attr.ib(default=None)
    rhotacization = attr.ib(default=None)
    centrality = attr.ib(default=None)
    glottalization = attr.ib(default=None)
    velarization = attr.ib(default=None)
    relative_articulation = attr.ib(default=None)
    tone = attr.ib(default=None)
    raising = attr.ib(default=None)
    rounding = attr.ib(default=None)
    tongue_root = attr.ib(default=None)
    articulation = attr.ib(default=None)  # compare
    # https://en.wikipedia.org/wiki/Faucalized_voice

    _write_order = dict(
        pre=[],
        post=[
            'tongue_root', 'raising', 'centrality', 'rounding',
            'voicing', 'breathiness', 'creakiness',
            'syllabicity', 'nasalization', 'tone', 'articulation', 'rhotacization',
            'pharyngealization', 'glottalization', 'velarization', 'duration',
            'frication', 'relative_articulation'])
    _name_order = [
        'duration', 'rhotacization', 'pharyngealization',
        'glottalization', 'velarization', 'syllabicity',
        'relative_articulation',
        'tongue_root', 'raising', 'rounding',
        'articulation', 'nasalization', 'voicing', 'creakiness',
        'breathiness', 'roundedness', 'height', 'frication', 'centrality',
        'tone']


@attr.s(cmp=False, repr=False)
class Diphthong(ComplexSound):
    """
    A dipthong consists of two vowels.
    """


@attr.s(cmp=False, repr=False)
class Tone(Sound):
    contour = attr.ib(default=None)
    start = attr.ib(default=None)
    middle = attr.ib(default=None)
    end = attr.ib(default=None)

    _name_order = ['contour', 'start', 'middle', 'end']
