#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import absolute_import, division, unicode_literals
from logging import getLogger
import re
from os import path
import xml.etree.ElementTree as etree

from . import app
from . import path_ops
from . import variables as v

LOG = getLogger('PLEX.subtitles')

# See https://kodi.wiki/view/Subtitles
SUBTITLE_LANGUAGE = re.compile(r'''(?i)[\. -]*(.*?)([\. -]forced)?$''')

# Plex support for external subtitles: srt, smi, ssa, aas, vtt
# https://support.plex.tv/articles/200471133-adding-local-subtitles-to-your-media/

# Which subtitles files are picked up by Kodi, what extensions do they need?
KODI_SUBTITLE_EXTENSIONS = ('srt', 'ssa', 'ass', 'usf', 'cdg', 'idx', 'sub',
                            'utf', 'aqt', 'jss', 'psb', 'rt', 'smi', 'txt',
                            'smil', 'stl', 'dks', 'pjs', 'mpl2', 'mks')

# Official language designations. Tuples consist of
#    (ISO language name, ISO 639-1, ISO 639-2, ISO 639-2/B)
# source: https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes
LANGUAGE_ISO_CODES = (
    ('abkhazian', 'ab', 'abk', 'abk'),
    ('afar', 'aa', 'aar', 'aar'),
    ('afrikaans', 'af', 'afr', 'afr'),
    ('akan', 'ak', 'aka', 'aka'),
    ('albanian', 'sq', 'sqi', 'alb'),
    ('amharic', 'am', 'amh', 'amh'),
    ('arabic', 'ar', 'ara', 'ara'),
    ('aragonese', 'an', 'arg', 'arg'),
    ('armenian', 'hy', 'hye', 'arm'),
    ('assamese', 'as', 'asm', 'asm'),
    ('avaric', 'av', 'ava', 'ava'),
    ('avestan', 'ae', 'ave', 'ave'),
    ('aymara', 'ay', 'aym', 'aym'),
    ('azerbaijani', 'az', 'aze', 'aze'),
    ('bambara', 'bm', 'bam', 'bam'),
    ('bashkir', 'ba', 'bak', 'bak'),
    ('basque', 'eu', 'eus', 'baq'),
    ('belarusian', 'be', 'bel', 'bel'),
    ('bengali', 'bn', 'ben', 'ben'),
    ('bislama', 'bi', 'bis', 'bis'),
    ('bosnian', 'bs', 'bos', 'bos'),
    ('breton', 'br', 'bre', 'bre'),
    ('bulgarian', 'bg', 'bul', 'bul'),
    ('burmese', 'my', 'mya', 'bur'),
    ('catalan', 'ca', 'cat', 'cat'),
    ('chamorro', 'ch', 'cha', 'cha'),
    ('chechen', 'ce', 'che', 'che'),
    ('chichewa', 'ny', 'nya', 'nya'),
    ('chinese', 'zh', 'zho', 'chi'),
    ('chuvash', 'cv', 'chv', 'chv'),
    ('cornish', 'kw', 'cor', 'cor'),
    ('corsican', 'co', 'cos', 'cos'),
    ('cree', 'cr', 'cre', 'cre'),
    ('croatian', 'hr', 'hrv', 'hrv'),
    ('czech', 'cs', 'ces', 'cze'),
    ('danish', 'da', 'dan', 'dan'),
    ('divehi', 'dv', 'div', 'div'),
    ('dutch', 'nl', 'nld', 'dut'),
    ('dzongkha', 'dz', 'dzo', 'dzo'),
    ('english', 'en', 'eng', 'eng'),
    ('esperanto', 'eo', 'epo', 'epo'),
    ('estonian', 'et', 'est', 'est'),
    ('ewe', 'ee', 'ewe', 'ewe'),
    ('faroese', 'fo', 'fao', 'fao'),
    ('fijian', 'fj', 'fij', 'fij'),
    ('finnish', 'fi', 'fin', 'fin'),
    ('french', 'fr', 'fra', 'fre'),
    ('fulah', 'ff', 'ful', 'ful'),
    ('galician', 'gl', 'glg', 'glg'),
    ('georgian', 'ka', 'kat', 'geo'),
    ('german', 'de', 'deu', 'ger'),
    ('greek', 'el', 'ell', 'gre'),
    ('guarani', 'gn', 'grn', 'grn'),
    ('gujarati', 'gu', 'guj', 'guj'),
    ('haitian', 'ht', 'hat', 'hat'),
    ('hausa', 'ha', 'hau', 'hau'),
    ('hebrew', 'he', 'heb', 'heb'),
    ('herero', 'hz', 'her', 'her'),
    ('hindi', 'hi', 'hin', 'hin'),
    ('hiri motu', 'ho', 'hmo', 'hmo'),
    ('hungarian', 'hu', 'hun', 'hun'),
    ('interlingua', 'ia', 'ina', 'ina'),
    ('indonesian', 'id', 'ind', 'ind'),
    ('interlingue', 'ie', 'ile', 'ile'),
    ('irish', 'ga', 'gle', 'gle'),
    ('igbo', 'ig', 'ibo', 'ibo'),
    ('inupiaq', 'ik', 'ipk', 'ipk'),
    ('ido', 'io', 'ido', 'ido'),
    ('icelandic', 'is', 'isl', 'ice'),
    ('italian', 'it', 'ita', 'ita'),
    ('inuktitut', 'iu', 'iku', 'iku'),
    ('japanese', 'ja', 'jpn', 'jpn'),
    ('javanese', 'jv', 'jav', 'jav'),
    ('kalaallisut', 'kl', 'kal', 'kal'),
    ('kannada', 'kn', 'kan', 'kan'),
    ('kanuri', 'kr', 'kau', 'kau'),
    ('kashmiri', 'ks', 'kas', 'kas'),
    ('kazakh', 'kk', 'kaz', 'kaz'),
    ('central khmer', 'km', 'khm', 'khm'),
    ('kikuyu', 'ki', 'kik', 'kik'),
    ('kinyarwanda', 'rw', 'kin', 'kin'),
    ('kirghiz', 'ky', 'kir', 'kir'),
    ('komi', 'kv', 'kom', 'kom'),
    ('kongo', 'kg', 'kon', 'kon'),
    ('korean', 'ko', 'kor', 'kor'),
    ('kurdish', 'ku', 'kur', 'kur'),
    ('kuanyama', 'kj', 'kua', 'kua'),
    ('latin', 'la', 'lat', 'lat'),
    ('luxembourgish', 'lb', 'ltz', 'ltz'),
    ('ganda', 'lg', 'lug', 'lug'),
    ('limburgan', 'li', 'lim', 'lim'),
    ('lingala', 'ln', 'lin', 'lin'),
    ('lao', 'lo', 'lao', 'lao'),
    ('lithuanian', 'lt', 'lit', 'lit'),
    ('luba-katanga', 'lu', 'lub', 'lub'),
    ('latvian', 'lv', 'lav', 'lav'),
    ('manx', 'gv', 'glv', 'glv'),
    ('macedonian', 'mk', 'mkd', 'mac'),
    ('malagasy', 'mg', 'mlg', 'mlg'),
    ('malay', 'ms', 'msa', 'may'),
    ('malayalam', 'ml', 'mal', 'mal'),
    ('maltese', 'mt', 'mlt', 'mlt'),
    ('maori', 'mi', 'mri', 'mao'),
    ('marathi', 'mr', 'mar', 'mar'),
    ('marshallese', 'mh', 'mah', 'mah'),
    ('mongolian', 'mn', 'mon', 'mon'),
    ('nauru', 'na', 'nau', 'nau'),
    ('navajo', 'nv', 'nav', 'nav'),
    ('north ndebele', 'nd', 'nde', 'nde'),
    ('nepali', 'ne', 'nep', 'nep'),
    ('ndonga', 'ng', 'ndo', 'ndo'),
    ('norwegian bokmål', 'nb', 'nob', 'nob'),
    ('norwegian nynorsk', 'nn', 'nno', 'nno'),
    ('norwegian', 'no', 'nor', 'nor'),
    ('sichuan yi', 'ii', 'iii', 'iii'),
    ('south ndebele', 'nr', 'nbl', 'nbl'),
    ('occitan', 'oc', 'oci', 'oci'),
    ('ojibwa', 'oj', 'oji', 'oji'),
    ('church slavic', 'cu', 'chu', 'chu'),
    ('oromo', 'om', 'orm', 'orm'),
    ('oriya', 'or', 'ori', 'ori'),
    ('ossetian', 'os', 'oss', 'oss'),
    ('punjabi', 'pa', 'pan', 'pan'),
    ('pali', 'pi', 'pli', 'pli'),
    ('persian', 'fa', 'fas', 'per'),
    ('polish', 'pl', 'pol', 'pol'),
    ('pashto', 'ps', 'pus', 'pus'),
    ('portuguese', 'pt', 'por', 'por'),
    ('quechua', 'qu', 'que', 'que'),
    ('romansh', 'rm', 'roh', 'roh'),
    ('rundi', 'rn', 'run', 'run'),
    ('romanian', 'ro', 'ron', 'rum'),
    ('russian', 'ru', 'rus', 'rus'),
    ('sanskrit', 'sa', 'san', 'san'),
    ('sardinian', 'sc', 'srd', 'srd'),
    ('sindhi', 'sd', 'snd', 'snd'),
    ('northern sami', 'se', 'sme', 'sme'),
    ('samoan', 'sm', 'smo', 'smo'),
    ('sango', 'sg', 'sag', 'sag'),
    ('serbian', 'sr', 'srp', 'srp'),
    ('gaelic', 'gd', 'gla', 'gla'),
    ('shona', 'sn', 'sna', 'sna'),
    ('sinhala', 'si', 'sin', 'sin'),
    ('slovak', 'sk', 'slk', 'slo'),
    ('slovenian', 'sl', 'slv', 'slv'),
    ('somali', 'so', 'som', 'som'),
    ('southern sotho', 'st', 'sot', 'sot'),
    ('spanish', 'es', 'spa', 'spa'),
    ('sundanese', 'su', 'sun', 'sun'),
    ('swahili', 'sw', 'swa', 'swa'),
    ('swati', 'ss', 'ssw', 'ssw'),
    ('swedish', 'sv', 'swe', 'swe'),
    ('tamil', 'ta', 'tam', 'tam'),
    ('telugu', 'te', 'tel', 'tel'),
    ('tajik', 'tg', 'tgk', 'tgk'),
    ('thai', 'th', 'tha', 'tha'),
    ('tigrinya', 'ti', 'tir', 'tir'),
    ('tibetan', 'bo', 'bod', 'tib'),
    ('turkmen', 'tk', 'tuk', 'tuk'),
    ('tagalog', 'tl', 'tgl', 'tgl'),
    ('tswana', 'tn', 'tsn', 'tsn'),
    ('tonga', 'to', 'ton', 'ton'),
    ('turkish', 'tr', 'tur', 'tur'),
    ('tsonga', 'ts', 'tso', 'tso'),
    ('tatar', 'tt', 'tat', 'tat'),
    ('twi', 'tw', 'twi', 'twi'),
    ('tahitian', 'ty', 'tah', 'tah'),
    ('uighur', 'ug', 'uig', 'uig'),
    ('ukrainian', 'uk', 'ukr', 'ukr'),
    ('urdu', 'ur', 'urd', 'urd'),
    ('uzbek', 'uz', 'uzb', 'uzb'),
    ('venda', 've', 'ven', 'ven'),
    ('vietnamese', 'vi', 'vie', 'vie'),
    ('volapük', 'vo', 'vol', 'vol'),
    ('walloon', 'wa', 'wln', 'wln'),
    ('welsh', 'cy', 'cym', 'wel'),
    ('wolof', 'wo', 'wol', 'wol'),
    ('western frisian', 'fy', 'fry', 'fry'),
    ('xhosa', 'xh', 'xho', 'xho'),
    ('yiddish', 'yi', 'yid', 'yid'),
    ('yoruba', 'yo', 'yor', 'yor'),
    ('zhuang', 'za', 'zha', 'zha'),
    ('zulu', 'zu', 'zul', 'zul'),
)


def accessible_plex_subtitles(playmethod, playing_file, xml_streams):
    if not playmethod == v.PLAYBACK_METHOD_DIRECT_PATH:
        # We can access all subtitles because we're downloading additional
        # external ones into the Kodi PKC add-on directory
        streams = []
        # Kodi ennumerates EXTERNAL subtitles first, then internal ones
        for stream in xml_streams:
            if stream.get('streamType') == '3' and 'key' in stream.attrib:
                streams.append(stream)
        for stream in xml_streams:
            if stream.get('streamType') == '3' and 'key' not in stream.attrib:
                streams.append(stream)
        if streams:
            LOG.debug('Working with the following Plex subtitle streams:')
            log_plex_streams(streams)
        return streams
    kodi_subs = kodi_subs_from_player()
    plex_streams_int, plex_streams_ext = accessible_plex_sub_streams(xml_streams)
    # Kodi appends internal streams at the end of its list
    kodi_subs_ext = kodi_subs[:len(kodi_subs) - len(plex_streams_int)]
    LOG.debug('Kodi list of external subs: %s', kodi_subs_ext)
    LOG.debug('Kodi has %s external subs, Plex %s, trying to match them',
              len(kodi_subs_ext), len(plex_streams_ext))
    dirname, basename = path.split(playing_file)
    filename, _ = path.splitext(basename)
    try:
        kodi_subs_file = kodi_external_subs(dirname, filename, kodi_subs_ext)
        reordered_plex_streams_ext = reorder_plex_streams(plex_streams_ext,
                                                          kodi_subs_file)
    except SubtitleError:
        # Add dummy subtitles so we won't match against Plex subtitles that
        # are in an incorrect order - keeps Kodi order of subs intact
        reordered_plex_streams_ext = [DummySub()
                                      for _ in range(len(kodi_subs_ext))]
    reordered_plex_streams_ext.extend(plex_streams_int)
    return reordered_plex_streams_ext


def reorder_plex_streams(plex_streams_ext, kodi_subs_file):
    """
    Returns the Plex streams in a "best-guess" order as indicated by the
    Kodi external subtitles kodi_subs_file
    """
    order = [None for i in range(len(kodi_subs_file))]
    # Pick subtitles with known language, extension and "forced" True first
    for i, kodi_sub in enumerate(kodi_subs_file):
        if not kodi_sub['iso'] or not kodi_sub['forced']:
            continue
        for plex_stream in plex_streams_ext:
            if not plex_stream.get('forced'):
                continue
            elif not kodi_sub['iso'][1] == plex_stream.get('languageTag'):
                continue
            elif not kodi_sub['codec'] == plex_stream.get('codec').lower():
                continue
            # Pick the first matching result - even though it's a best guess
            order[i] = plex_stream
            plex_streams_ext.remove(plex_stream)
            break
    # Pick non-forced
    for i, kodi_sub in enumerate(kodi_subs_file):
        if order[i] is not None or not kodi_sub['iso']:
            continue
        for plex_stream in plex_streams_ext:
            if not kodi_sub['iso'][1] == plex_stream.get('languageTag'):
                continue
            elif not kodi_sub['codec'] == plex_stream.get('codec').lower():
                continue
            elif not (kodi_sub['forced'] is (plex_stream.get('forced') == '1')):
                continue
            # Pick the first matching result - even though it's a best guess
            order[i] = plex_stream
            plex_streams_ext.remove(plex_stream)
            break
    # Pick subs irrelevant of forced flag
    for i, kodi_sub in enumerate(kodi_subs_file):
        if order[i] is not None or not kodi_sub['iso']:
            continue
        for plex_stream in plex_streams_ext:
            if not kodi_sub['iso'][1] == plex_stream.get('languageTag'):
                continue
            elif not kodi_sub['codec'] == plex_stream.get('codec').lower():
                continue
            # Pick the first matching result - even though it's a best guess
            order[i] = plex_stream
            plex_streams_ext.remove(plex_stream)
            break
    # Pick subs based on codec (Plex does not detect "English" as en). Forced
    # ones first
    for i, kodi_sub in enumerate(kodi_subs_file):
        if order[i] is not None or not kodi_sub['forced']:
            continue
        for plex_stream in plex_streams_ext:
            if not kodi_sub['codec'] == plex_stream.get('codec').lower():
                continue
            elif not plex_stream.get('forced'):
                continue
            elif plex_stream.get('languageTag') and kodi_sub['iso'] \
                    and not plex_stream.get('languageTag') == kodi_sub['iso'][1]:
                continue
            # Pick the first matching result - even though it's a best guess
            order[i] = plex_stream
            plex_streams_ext.remove(plex_stream)
            break
    # Pick subs based on codec alone (Plex does not detect "English" as en).
    # Non-forced
    for i, kodi_sub in enumerate(kodi_subs_file):
        if order[i] is not None:
            continue
        for plex_stream in plex_streams_ext:
            if not kodi_sub['codec'] == plex_stream.get('codec').lower():
                continue
            elif not (kodi_sub['forced'] is (plex_stream.get('forced') == '1')):
                continue
            elif plex_stream.get('languageTag') and kodi_sub['iso'] \
                    and not plex_stream.get('languageTag') == kodi_sub['iso'][1]:
                continue
            # Pick the first matching result - even though it's a best guess
            order[i] = plex_stream
            plex_streams_ext.remove(plex_stream)
            break
    # Pick subs based on codec alone (Plex does not detect "English" as en).
    # Even with miss-matching forced flag
    for i, kodi_sub in enumerate(kodi_subs_file):
        if order[i] is not None:
            continue
        for plex_stream in plex_streams_ext:
            if not kodi_sub['codec'] == plex_stream.get('codec').lower():
                continue
            elif plex_stream.get('languageTag') and kodi_sub['iso'] \
                    and not plex_stream.get('languageTag') == kodi_sub['iso'][1]:
                continue
            # Pick the first matching result - even though it's a best guess
            order[i] = plex_stream
            plex_streams_ext.remove(plex_stream)
            break
    # Now lets add dummies for Kodi subs we could not match
    for i, kodi_sub in enumerate(kodi_subs_file):
        if order[i] is not None:
            continue
        LOG.debug('Could not match Kodi sub number %s %s, adding a dummy',
                  i, kodi_sub)
        order[i] = DummySub()
    if plex_streams_ext:
        LOG.debug('We could not match the following Plex subtitles:')
        log_plex_streams(plex_streams_ext)
    if order:
        LOG.debug('Derived order of external subtitle streams:')
        log_plex_streams(order)
    return order


def log_plex_streams(plex_streams):
    for i, stream in enumerate(plex_streams):
        LOG.debug('Number %s: %s: %s', i, stream.tag, stream.attrib)


def accessible_plex_sub_streams(xml):
    # Any additionally downloaded subtitles are not accessible for Kodi
    # We're identifying them by the additional key 'providerTitle'
    plex_streams = [stream for stream in xml
                    if stream.get('streamType') == '3'
                    and not stream.get('providerTitle')]
    LOG.debug('Available Plex subtitle streams for currently playing item:')
    log_plex_streams(plex_streams)
    # Kodi can display internal subtitle streams for sure
    plex_streams_int = [x for x in plex_streams if 'key' not in x.attrib]
    # We need to check external ones
    # If the movie name is 'The Dark Knight (2008).mkv', Kodi finds
    # subtitles 'The Dark Knight (2008)*.*.<ext>'
    plex_streams_ext = [x for x in plex_streams if 'key' in x.attrib]
    return plex_streams_int, plex_streams_ext


def kodi_subs_from_player():
    """
    Kodi can only play subtitles that it pickes up itself: They lie in the
    same folder as the video file and are named similarly
    """
    kodi_subs = app.APP.player.getAvailableSubtitleStreams()
    LOG.debug('Kodi list of available subtitles: %s', kodi_subs)
    return kodi_subs


def kodi_external_subs(dirname, filename, kodi_subs_ext):
    file_subs = external_subs_from_filesystem(dirname, filename)
    if len(file_subs) != len(kodi_subs_ext):
        LOG.warn('Unexpected missmatch of number of Kodi subtitles')
        LOG.warn('Kodi subs: %s', kodi_subs_ext)
        LOG.warn('Subs from the filesystem: %s', file_subs)
        raise SubtitleError()
    for i, sub in enumerate(file_subs):
        if sub['iso'] and kodi_subs_ext[i].lower() not in sub['iso']:
            LOG.warn('Unexpected Kodi external subtitle language combo')
            LOG.warn('Kodi subs: %s', kodi_subs_ext)
            LOG.warn('Subs from the filesystem: %s', file_subs)
            raise SubtitleError()
    return file_subs


def external_subs_from_filesystem(dirname, filename):
    """
    Returns a list of dicts of subtitles lying within the directory dirname:
        {'iso': tuple of detected ISO language (see LANGUAGE_ISO_CODES) or None,
         'language': language string that Kodi might show in its GUI,
         'forced': has '[. -]forced' been appended to the filename?
         'file': subtitle file name}
    Supply with the currently playing filename as Kodi uses that to search
    for subtitles. See https://kodi.wiki/view/Subtitles
    """
    file_subs = []
    for root, dirs, files in path_ops.walk(dirname):
        for file in files:
            name, extension = path.splitext(file)
            # Get rid of the dot and force lowercase
            extension = extension[1:].lower()
            if extension not in KODI_SUBTITLE_EXTENSIONS:
                # Not an extension Kodi supports
                continue
            elif not name.startswith(filename):
                # Naming not up to standards, Kodi won't pick up this file
                # (but Plex might!!)
                continue
            regex = SUBTITLE_LANGUAGE.search(name.replace(filename, '', 1))
            language = (regex.group(1) if regex.group(1) else '').lower()
            forced = True if regex.group(2) else False
            iso = None
            if len(language) == 2:
                language_searchgrid = (1, )
            elif len(language) == 3:
                language_searchgrid = (2, 3)
            else:
                language_searchgrid = (0, )
            for lang in LANGUAGE_ISO_CODES:
                for i in language_searchgrid:
                    if lang[i] == language:
                        iso = lang
                        break
                else:
                    continue
                break
            file_subs.append({'iso': iso,
                              'language': language,
                              'forced': forced,
                              'codec': extension,
                              'file': '%s.%s' % (name, extension)})
    LOG.debug('Detected these external subtitles while scanning the file '
              'system: %s', file_subs)
    return file_subs


class DummySub(etree.Element):
    def __init__(self):
        super(DummySub, self).__init__('Stream-subtitle-dummy')


class SubtitleError(Exception):
    pass
