from __future__ import unicode_literals, division, absolute_import

import logging
import re
from abc import abstractproperty, abstractmethod, ABCMeta
from string import capwords

from flexget.utils.tools import ReList


log = logging.getLogger('parser')

SERIES_ID_TYPES = ['ep', 'date', 'sequence', 'id']


def clean_value(name):
    # Move anything in leading brackets to the end
    #name = re.sub(r'^\[(.*?)\](.*)', r'\2 \1', name)

    for char in '[]()_,.':
        name = name.replace(char, ' ')

    # if there are no spaces
    if name.find(' ') == -1:
        name = name.replace('-', ' ')

    # remove unwanted words (imax, ..)
    #self.remove_words(data, self.remove)

    #MovieParser.strip_spaces
    name = ' '.join(name.split())
    return name


class ParseWarning(Warning):
    def __init__(self, parsed, value, **kwargs):
        self.value = value
        self.parsed = parsed
        self.kwargs = kwargs

    def __unicode__(self):
        return self.value

    def __str__(self):
        return self.__unicode__().encode('utf-8')

    def __repr__(self):
        return str('ParseWarning({}, **{})').format(self, repr(self.kwargs))


def old_assume_quality(guessed_quality, assumed_quality):
    if assumed_quality:
        if not guessed_quality:
            return assumed_quality
        if assumed_quality.resolution:
            guessed_quality.resolution = assumed_quality.resolution
        if assumed_quality.source:
            guessed_quality.source = assumed_quality.source
        if assumed_quality.codec:
            guessed_quality.codec = assumed_quality.codec
        if assumed_quality.audio:
            guessed_quality.audio = assumed_quality.audio
    return guessed_quality


default_ignore_prefixes = [
    '(?:\[[^\[\]]*\])',  # ignores group names before the name, eg [foobar] name
    '(?:HD.720p?:)',
    '(?:HD.1080p?:)']


def name_to_re(name, ignore_prefixes=None, parser=None):
    """Convert 'foo bar' to '^[^...]*foo[^...]*bar[^...]+"""
    if not ignore_prefixes:
        ignore_prefixes = default_ignore_prefixes
    parenthetical = None
    if name.endswith(')'):
        p_start = name.rfind('(')
        if p_start != -1:
            parenthetical = re.escape(name[p_start + 1:-1])
            name = name[:p_start - 1]
    # Blanks are any non word characters except & and _
    blank = r'(?:[^\w&]|_)'
    ignore = '(?:' + '|'.join(ignore_prefixes) + ')?'
    res = re.sub(re.compile(blank + '+', re.UNICODE), ' ', name)
    res = res.strip()
    # accept either '&' or 'and'
    res = re.sub(' (&|and) ', ' (?:and|&) ', res, re.UNICODE)
    res = re.sub(' +', blank + '*', res, re.UNICODE)
    if parenthetical:
        res += '(?:' + blank + '+' + parenthetical + ')?'
        # Turn on exact mode for series ending with a parenthetical,
        # so that 'Show (US)' is not accepted as 'Show (UK)'
        if parser:
            parser.strict_name = True
    res = '^' + ignore + blank + '*' + '(' + res + ')(?:\\b|_)' + blank + '*'
    return res


def remove_dirt(name):
    if name:
        name = re.sub(r'[_.,\[\]\(\): ]+', ' ', name).strip().lower()
    return name


def normalize_name(name):
    name = capwords(name)
    return name


class ParsedEntry(ABCMeta(str('ParsedEntryABCMeta'), (object,), {})):
    """
    A parsed entry, containing parsed data like name, year, episodeNumber and season.
    """

    def __init__(self, data, name, **kwargs):
        self._data = data
        self._name = name
        self._validated_name = None
        self._kwargs = kwargs

    @property
    def name_regexps(self):
        return self._kwargs['name_regexps'] if 'name_regexps' in self._kwargs else []

    @property
    def alternate_names(self):
        return self._kwargs['alternate_names'] if 'alternate_names' in self._kwargs else []

    @property
    def ignore_prefixes(self):
        return self._kwargs['ignore_prefixes'] if 'ignore_prefixes' in self._kwargs else default_ignore_prefixes

    @property
    def allow_groups(self):
        return self._kwargs['allow_groups'] if 'allow_groups' in self._kwargs else []

    @property
    def strict_name(self):
        return self._kwargs['strict_name'] if 'strict_name' in self._kwargs else False

    @abstractproperty
    def parsed_type(self):
        raise NotImplementedError

    @abstractproperty
    def type(self):
        raise NotImplementedError

    def _validate(self):
        validate_name = self._validate_name()
        if validate_name and self._validate_groups():
            return validate_name
        return None

    def _validate_name(self):
        # name end position
        name_start = 0
        name_end = 0

        # regexp name matching
        re_from_name = False
        name_regexps = ReList(self.name_regexps)
        if not name_regexps:
            # if we don't have name_regexps, generate one from the name
            name_regexps = ReList(name_to_re(name, self.ignore_prefixes, None) for name in [self.name] + self.alternate_names)
            # With auto regex generation, the first regex group captures the name
            re_from_name = True
        # try all specified regexps on this data
        for name_re in name_regexps:
            match = re.search(name_re, self.data)
            if match:
                match_start, match_end = match.span(1 if re_from_name else 0)
                # Always pick the longest matching regex
                if match_end > name_end:
                    name_start, name_end = match_start, match_end
                log.debug('NAME SUCCESS: %s matched to %s', name_re.pattern, self.data)
        if not name_end:
            # leave this invalid
            log.debug('FAIL: name regexps %s do not match %s',
                      [regexp.pattern for regexp in name_regexps], self.data)
            return ''
        return clean_value(self.data[name_start:name_end])

    def _validate_groups(self):
        if not self.allow_groups:
            return True
        if not self.group:
            return False
        return self.group.lower() in [x.lower() for x in self.allow_groups]

    @property
    def name(self):
        return self._name if self._name else self.parsed_name

    @name.setter
    def name(self, name):
        self._valid = None
        self._name = name

    @abstractproperty
    def parsed_name(self):
        raise NotImplementedError

    @property
    def data(self):
        return self._data

    @property
    def valid(self):
        if not self.parsed_name:
            return False
        if self.type != self.parsed_type:
            return False
        if not self._name:
            return True # Not False ???!
        if self._validated_name is None:
            self._validated_name = self._validate()
        if self.strict_name and self._validated_name != clean_value(self.parsed_name):
            return False
        return len(self._validated_name) > 0 if self._validated_name else False

    @property
    def proper(self):
        return self.proper_count > 0

    @abstractproperty
    def proper_count(self):
        raise NotImplementedError

    @property
    def is_series(self):
        return False

    @property
    def is_movie(self):
        return False

    @property
    def group(self):
        return self.parsed_group.lower() if self.parsed_group else None

    @abstractproperty
    def parsed_group(self):
        raise NotImplementedError

    @abstractproperty
    def properties(self):
        raise NotImplementedError

    def __str__(self):
        return "<%s(name=%s)>" % (self.__class__.__name__, self.name)


class ParsedVideo(ABCMeta(str('ParsedVideoABCMeta'), (ParsedEntry,), {})):
    _old_quality = None
    _assumed_quality = None

    @abstractproperty
    def year(self):
        raise NotImplementedError

    @abstractproperty
    def subtitle_languages(self):
        raise NotImplementedError

    @abstractproperty
    def languages(self):
        raise NotImplementedError

    @abstractproperty
    def quality2(self):
        raise NotImplementedError

    @abstractproperty
    def is_3d(self):
        raise NotImplementedError

    @property
    def type(self):
        return 'video'

    @property
    def quality(self):
        if not self._old_quality:
            self._old_quality = self.quality2.to_old_quality(self._assumed_quality)
        return self._old_quality

    def assume_quality(self, quality):
        self._assumed_quality = quality
        self._old_quality = None

    def assume_quality2(self, quality2):
        self._assumed_quality = quality2.to_old_quality()

    def __str__(self):
        return "<%s(name=%s,year=%s,quality=%s)>" % (self.__class__.__name__, self.name, self.year, self.quality)

    def __cmp__(self, other):
        """Compares quality of parsed, if quality is equal, compares proper_count."""
        return cmp((self.quality), (other.quality))

    def __eq__(self, other):
        return self is other


class ParsedVideoQuality(ABCMeta(str('ParsedVideoQualityABCMeta'), (object,), {})):
    @abstractproperty
    def screen_size(self):
        raise NotImplementedError

    @abstractproperty
    def source(self):
        raise NotImplementedError

    @abstractproperty
    def format(self):
        raise NotImplementedError

    @abstractproperty
    def video_codec(self):
        raise NotImplementedError

    @abstractproperty
    def video_profile(self):
        raise NotImplementedError

    @abstractproperty
    def audio_codec(self):
        raise NotImplementedError

    @abstractproperty
    def audio_profile(self):
        raise NotImplementedError

    @abstractproperty
    def audio_channels(self):
        raise NotImplementedError

    @abstractmethod
    def to_old_quality(self, assumed_quality=None):
        raise NotImplementedError

    def __str__(self):
        return "<%s(screen_size=%s,source=%s,video_codec=%s,audio_channels=%s)>" % (
        self.__class__.__name__, self.screen_size, self.source, self.video_codec, self.audio_channels)


class ParsedMovie(ABCMeta(str('ParsedMovieABCMeta'), (ParsedVideo,), {})):
    @property
    def parsed_name(self):
        return self.title

    @property
    def type(self):
        return 'movie'

    @property
    def title(self):
        raise NotImplementedError

    @property
    def is_movie(self):
        return True


class ParsedSerie(ABCMeta(str('ParsedSerieABCMeta'), (ParsedVideo,), {})):
    _valid_strict = None

    @property
    def allow_seasonless(self):
        return self._kwargs['allow_seasonless'] if 'allow_seasonless' in self._kwargs else False

    @property
    def identified_by(self):
        return self._kwargs['identified_by'] if 'identified_by' in self._kwargs else 'auto'

    @property
    def assume_special(self):
        return self._kwargs['assume_special'] if 'assume_special' in self._kwargs else False

    @property
    def prefer_specials(self):
        return self._kwargs['prefer_specials'] if 'prefer_specials' in self._kwargs else False

    @property
    def parsed_name(self):
        return self.series

    @property
    def type(self):
        return 'series'

    @abstractproperty
    def parsed_season(self):
        raise NotImplementedError

    @abstractproperty
    def country(self):
        raise NotImplementedError

    @abstractproperty
    def series(self):
        raise NotImplementedError

    @abstractproperty
    def title(self):
        raise NotImplementedError

    @abstractproperty
    def complete(self):
        raise NotImplementedError

    @property
    def is_series(self):
        return True

    @property
    def season(self):
        return 1 if not self.parsed_season and self.allow_seasonless else self.parsed_season

    @abstractproperty
    def episode(self):
        raise NotImplementedError

    @abstractproperty
    def episodes(self):
        raise NotImplementedError

    @abstractproperty
    def date(self):
        raise NotImplementedError

    @abstractproperty
    def episode_details(self):
        raise NotImplementedError

    @abstractproperty
    def special(self):
        raise NotImplementedError

    @abstractproperty
    def regexp_id(self):
        raise NotImplementedError

    @abstractproperty
    def valid_strict(self):
        raise NotImplementedError

    @property
    def valid(self):
        ret = super(ParsedVideo, self).valid
        if ret:
            if self.country and self.name.endswith(')'):
                p_start = self.name.rfind('(')
                if p_start != -1:
                    parenthetical = re.escape(self.name[p_start + 1:-1])
                    if parenthetical and parenthetical.lower() != self.country.lower():
                        return False
            if self.identified_by != 'auto' and self.identified_by != self.id_type:
                return False
            if self.complete or (self.identified_by in ['auto', 'ep'] and self.season is not None and self.episode is None):
                return False
            if self.identified_by in ['auto', 'ep'] and self.episodes > 3:
                return False
            if self.identified_by in ['ep', 'sequence'] and self.episode is None:
                return False
            if self.identified_by == 'ep' and (self.episode is None or (self.season is None and not self.allow_seasonless)):
                return False
            if self.identified_by == 'date' and not self.date:
                return False
            if self.special or self.assume_special:
                return True
            if self.regexp_id:
                return True
            if self.episode is not None or self.season is not None:
                return True
            if self.date:
                return True
            if self.episode is not None and not self.season:
                return True
        return False

    @property
    def id_type(self):
        id_type = None
        if self.regexp_id:
            id_type = 'id'
        elif self.episode is not None:
            if self.season is not None:
                id_type = 'ep'
            else:
                id_type = 'sequence'
        elif self.date:
            id_type = 'date'
        if self.special or self.assume_special:
            if not id_type or self.prefer_specials:
                id_type = 'special'
        return id_type

    @property
    def id(self):
        id = None
        if self.regexp_id:
            id = self.regexp_id
        elif self.episode is not None:
            id = self.episode
        elif self.date:
            id = self.date
        if self.special or self.assume_special:
            if not id or self.prefer_specials:
                id = self.title if self.title else 'special'
        return id

    @property
    def identifiers(self):
        """Return all identifiers this parser represents. (for packs)"""
        # Currently 'ep' is the only id type that supports packs
        if not self.valid:
            raise Exception('Series flagged invalid')
        if self.id_type == 'ep':
            return ['S%02dE%02d' % (self.season, self.episode + x) for x in xrange(self.episodes)]
        elif self.id_type == 'date':
            return [self.id.strftime('%Y-%m-%d')]
        if self.id is None:
            raise Exception('Series is missing identifier')
        else:
            return [self.id]

    @property
    def identifier(self):
        """Return String identifier for parsed episode, eg. S01E02
        (will be the first identifier if this is a pack)
        """
        return self.identifiers[0]

    @property
    def pack_identifier(self):
        """Return a combined identifier for the whole pack if this has more than one episode."""
        # Currently only supports ep mode
        if self.id_type == 'ep' and self.episodes > 1:
            return 'S%02dE%02d-E%02d' % (self.season, self.episode, self.episode + self.episodes - 1)
        else:
            return self.identifier

    def __str__(self):
        return "<%s(name=%s,id=%s,season=%s,episode=%s,quality=%s,proper=%s,status=%s)>" % \
               (self.__class__.__name__, self.name, self.id, self.season, self.episode,
                self.quality, self.proper_count, 'OK' if self.valid else 'INVALID')

    def __cmp__(self, other):
        """Compares quality of parsers, if quality is equal, compares proper_count."""
        return cmp((self.quality, self.episodes, self.proper_count),
                   (other.quality, other.episodes, other.proper_count))

    def __eq__(self, other):
        return self is other


