# -*- coding: utf-8 -*-
# Moovida - Home multimedia server
# Copyright (C) 2007-2009 Fluendo Embedded S.L. (www.fluendo.com).
# All rights reserved.
#
# This file is available under one of two license agreements.
#
# This file is licensed under the GPL version 3.
# See "LICENSE.GPL" in the root of this distribution including a special
# exception to use Moovida with Fluendo's plugins.
#
# The GPL part of Moovida is also available under a commercial licensing
# agreement from Fluendo.
# See "LICENSE.Moovida" in the root directory of this distribution package
# for details on that license.
#
# Author: Philippe Normand <philippe@fluendo.com>

from elisa.core.components.resource_provider import ResourceProvider, \
         ResourceNotFound
from elisa.core import media_uri
from elisa.extern.coherence import et

from elisa.plugins.http_client.http_client import ElisaAdvancedHttpClient
from twisted.web2 import responsecode
from twisted.web2.stream import BufferedStream

from twisted.internet import defer, task

from elisa.plugins.base.models.media import RawDataModel
from elisa.plugins.thetvdb.models import TvSeriesListModel, TvSeriesModel, \
     TvEpisodeModel, TvSeasonsListModel, TvSeasonModel
from elisa.plugins.thetvdb.constants import *

import re
import datetime

class TheTvDBResourceProvider(ResourceProvider):
    """
    Provide access to resources served by TheTvDB.com over HTTP.
    """

    log_category="thetvdb_resource_provider"
    # Queries to the API
    api_uri = 'http://%s/api/(.*)' % API_SERVER
    api_re = re.compile(api_uri)
    # Queries to the image server
    img_uri = 'http://%s/(.*)' % BANNERS_SERVER
    img_re = re.compile(img_uri)

    supported_uri = api_uri + '|' + img_uri

    def initialize(self):
        self._api_client = ElisaAdvancedHttpClient(host=API_SERVER)
        self._img_client = ElisaAdvancedHttpClient(host=BANNERS_SERVER)
        return super(TheTvDBResourceProvider, self).initialize()

    def clean(self):
        """
        Close all the open HTTP connections.
        """
        close_dfrs = [self._api_client.close(), self._img_client.close()]
        dfr = defer.DeferredList(close_dfrs, consumeErrors=True)

        def parent_clean(result):
            return super(TheTvDBResourceProvider, self).clean()

        dfr.addCallback(parent_clean)
        return dfr

    def get(self, uri, context_model=None):
        """
        Link to API docs: http://www.thetvdb.com/wiki/index.php?title=Programmers_API

        This resource_provider can:

        - lookup for TV shows given a search term and return results
          in the I{series} attribute of a
          L{elisa.plugins.thetvdb.models.TvSeriesListModel}

        - retrieve extended informations about a TV show, given its id
          in the uri (cf online API docs) and a
          L{elisa.plugins.thetvdb.models.TvSeriesModel}
          context_model. This context_model will be completed and
          returned.

        - retrieve all seasons/episodes informations about a TV show,
          given its id in the uri (cf online API docs) and a
          L{elisa.plugins.thetvdb.models.TvSeasonsListModel} instance
          as context_model. Fills the model and returns it.

        - retrieve extended informations about a sepecific episode,
          passed as I{context_model}, in a specific season of a TV
          show and return them in the updated
          L{elisa.plugins.thetvdb.models.TvEpisodeModel} instance

        Posters, banners and fanart hosted on thetvdb.com can also be retrieved
        and stored to L{elisa.plugins.base.models.media.RawDataModel}
        instances. Images are in jpeg format.

        @param uri:           URI pointing to the resource
        @type uri:            L{elisa.core.media_uri.MediaUri}
        @param context_model: optional series model instance to fill
        @type context_model:  L{elisa.plugins.thetvdb.models.TvSeriesModel} or
                              C{None}

        @return:              a new model and a deferred fired when the model
                              is filled with the requested resource's data
        @rtype:               tuple of L{elisa.core.components.model.Model}
                              L{elisa.core.utils.defer.Deferred}
        """
        url = str(uri)
        self.debug("GET %s", url)

        http_client = self._api_client
        if context_model:
            result_model = context_model
        elif self.api_re.match(url):
            result_model = TvSeriesListModel()
        else:
            result_model = RawDataModel()
            http_client = self._img_client

        dfr = http_client.request(url)
        dfr.addCallback(self._request_done, result_model, url)
        return result_model, dfr

    def _node_value(self, node, node_name):
        text = node.find('.//%s' % node_name).text
        if text is None:
            text = ''
        return unicode(text)

    def _set_optional_value_from_xml(self, model, attr_name, xml_node,
                                     node_name, trans_func=None):
        elem = xml_node.find('.//%s' % node_name)
        if elem is not None:
            value = elem.text
            if value is not None:
                value = unicode(value)
                if trans_func is not None:
                    value = trans_func(value)
            setattr(model, attr_name, value)

    def _pipe_unsplit(self, text):
        return [v.strip() for v in text.split('|') if v]

    def _timestamp_to_datetime(self, text):
        return datetime.datetime.fromtimestamp(int(text))

    def _img_path_to_url(self, text):
        return media_uri.MediaUri('http://%s/banners/%s' % (BANNERS_SERVER,
                                                            text))

    def _year_month_day_to_date(self, text):
        year, month, day = [int(i) for i in text.split('-')]
        return datetime.date(year, month, day)

    def _fill_series_model(self, serie, model=None):
        if not model:
            model = TvSeriesModel()

        model.id = int(self._node_value(serie, 'id'))
        model.name = self._node_value(serie, 'SeriesName')
        self._set_optional_value_from_xml(model, 'overview', serie, 'Overview')
        self._set_optional_value_from_xml(model, 'language', serie, 'language')
        self._set_optional_value_from_xml(model, 'first_aired_date', serie,
                                          'FirstAired',
                                          self._year_month_day_to_date)

        self._set_optional_value_from_xml(model, 'banner_url', serie, 'banner',
                                          self._img_path_to_url)

        self._set_optional_value_from_xml(model, 'imdbid', serie, 'IMDB_ID')
        self._set_optional_value_from_xml(model, 'zap2itid', serie, 'zap2it_id')
        self._set_optional_value_from_xml(model, 'runtime', serie, 'Runtime',
                                          int)

        # |-separated
        self._set_optional_value_from_xml(model, 'actors', serie, 'Actors',
                                          self._pipe_unsplit)

        self._set_optional_value_from_xml(model, 'airs_day_of_week', serie,
                                          'Airs_DayOfWeek')
        self._set_optional_value_from_xml(model, 'airs_time', serie,
                                          'Airs_Time')

        # |-separated
        self._set_optional_value_from_xml(model, 'genres', serie, 'Genre',
                                          self._pipe_unsplit)

        self._set_optional_value_from_xml(model, 'network', serie, 'Network')
        self._set_optional_value_from_xml(model, 'rating', serie, 'Rating',
                                          float)
        self._set_optional_value_from_xml(model, 'status', serie, 'Status')
        self._set_optional_value_from_xml(model, 'last_updated', serie,
                                          'lastupdated',
                                          self._timestamp_to_datetime)

        self._set_optional_value_from_xml(model, 'fanart_url', serie, 'fanart',
                                          self._img_path_to_url)
        self._set_optional_value_from_xml(model, 'poster_url', serie, 'poster',
                                          self._img_path_to_url)

        return defer.succeed(model)

    def _fill_episode_model(self, episode, model):

        model.id = self._node_value(episode, 'id')
        model.imdbid = self._node_value(episode, 'IMDB_ID')
        model.seriesid = int(self._node_value(episode, 'seriesid'))
        model.seasonid = int(self._node_value(episode, 'seasonid'))

        self._set_optional_value_from_xml(model, 'last_updated', episode,
                                          'lastupdate',
                                          self._timestamp_to_datetime)
        model.season_number = int(self._node_value(episode,
                                                   'SeasonNumber'))
        model.episode_number = int(self._node_value(episode,
                                                    'EpisodeNumber'))
        model.director = self._node_value(episode, 'Director')
        model.name = self._node_value(episode, 'EpisodeName')

        # |-separated
        self._set_optional_value_from_xml(model, 'guest_stars', episode,
                                          'GuestStars', self._pipe_unsplit)

        model.overview = self._node_value(episode, 'Overview')
        model.language = self._node_value(episode, 'Language')
        self._set_optional_value_from_xml(model, 'rating', episode, 'Rating',
                                          float)
        model.writer = self._node_value(episode, 'Writer')

        self._set_optional_value_from_xml(model, 'poster_url', episode,
                                          'filename', self._img_path_to_url)

        return defer.succeed(model)

    def _fill_banners(self, banners, model):
        for banner in banners:
            banner_type = self._node_value(banner, 'BannerType')
            banner_type2 = self._node_value(banner, 'BannerType2')
            if banner_type == banner_type2 == 'season':
                lang = self._node_value(banner, 'Language')
                season = int(self._node_value(banner, 'Season'))
                banner_url = self._img_path_to_url(self._node_value(banner,
                                                                    'BannerPath'))
                if season not in model.season_banners:
                    model.season_banners[season] = {}
                try:
                    model.season_banners[season][lang].append(banner_url)
                except KeyError:
                    model.season_banners[season][lang] = [banner_url,]

        return defer.succeed(model)

    def _response_read(self, response, result_model):
        # Parse the response and populate the model accordingly
        dfr = None
        if isinstance(result_model, TvSeriesListModel):
            xml_tree = et.parse_xml(response)
            series = list(xml_tree.findall(".//Series"))

            def series_filled(model):
                result_model.series.append(model)

            def iterate_series():
                for serie in series:
                    dfr = self._fill_series_model(serie)
                    dfr.addCallback(series_filled)
                    yield dfr

            def all_done(result):
                return result_model

            if series:
                dfr = task.coiterate(iterate_series())
                dfr.addCallback(all_done)
        elif isinstance(result_model, TvSeasonsListModel):
            xml_tree = et.parse_xml(response)
            episodes = list(xml_tree.findall(".//Episode"))
            tvshow_name = xml_tree.find('Series').find('SeriesName').text

            def episode_filled(episode_model):
                season_number = episode_model.season_number
                if season_number in result_model.seasons:
                    result_model.seasons[season_number].episodes.append(episode_model)
                else:
                    season = TvSeasonModel()
                    season.number = season_number
                    season.id = episode_model.seasonid
                    season.tvshow_id = episode_model.seriesid
                    season.tvshow_name = tvshow_name
                    season.episodes.append(episode_model)
                    result_model.seasons[season_number] = season

            def iterate_episodes():
                for episode in episodes:
                    model = TvEpisodeModel()
                    dfr = self._fill_episode_model(episode, model)
                    dfr.addCallback(episode_filled)
                    yield dfr

            def all_done(result):
                return result_model

            if episodes:
                dfr = task.coiterate(iterate_episodes())
                dfr.addCallback(all_done)
        elif isinstance(result_model, TvEpisodeModel):
            xml_tree = et.parse_xml(response)
            episode = xml_tree.find(".//Episode")

            def all_done(result):
                return result_model

            if episode:
                dfr = self._fill_episode_model(episode, result_model)
                dfr.addCallback(all_done)

        elif isinstance(result_model, TvSeriesModel):
            xml_tree = et.parse_xml(response)
            series = xml_tree.find('.//Series')
            banners = list(xml_tree.findall('.//Banner'))
            if series:
                dfr = self._fill_series_model(series, result_model)
            elif banners:
                dfr = self._fill_banners(banners, result_model)

        elif isinstance(result_model, RawDataModel):
            result_model.data = response
            result_model.size = len(response)
            dfr = defer.succeed(result_model)
        return dfr

    def _request_done(self, response, model, url):
        if response.code == responsecode.OK:
            # Read the response stream
            read_dfr = BufferedStream(response.stream).readExactly()
            read_dfr.addCallback(self._response_read, model)
            return read_dfr
        elif response.code == responsecode.NOT_FOUND:
            # 404 error code: resource not found
            raise ResourceNotFound(url)
        else:
            # Other HTTP response code
            raise Exception('Received an %d HTTP response code' % response.code)
