# -*- 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: Olivier Tilloy <olivier@fluendo.com>

# Some portions of code largely inspired by Ross Burton's flickrpc
# (see http://burtonini.com/bzr/flickrpc/)

"""
Constants and helper functions for access to the Flickr API (see
http://www.flickr.com/services/api for details).
"""

from elisa.core.media_uri import MediaUri
from elisa.core.default_config import CONFIG_DIR
from elisa.core.utils import defer

from elisa.plugins.base.models.image import ImageModel

try:
    from hashlib import md5
except ImportError:
    # hashlib is new in Python 2.5
    from md5 import md5

from xml.dom import minidom
import os


API_SERVER = 'api.flickr.com'
IMG_SERVER = 'static.flickr.com'


# Flickr API key and secret for Elisa (registered under oSoMoN's account).
# These 'key' and 'secret' tokens are required by Flickr to allow API calls.
_key = 'ef2cc311ac8916c794b436bf482c5de5'
_secret = '27987243a912133a'


BASE_API_URI = 'http://%s/services/rest/?' % API_SERVER


def sign_arguments(arguments):
    """
    Compute the 'api_sig' argument for methods that need signing (see
    http://www.flickr.com/services/api/auth.spec.html#signing for details).

    This method updates the dictionary of arguments passed as a parameter,
    setting the value of the 'api_sig' argument.

    @param arguments: the arguments passed to the method that need signing
    @type arguments:  C{dict} of {str}
    """
    signature = _secret + \
                ''.join([arg + arguments[arg] for arg in sorted(arguments)])
    arguments['api_sig'] = md5(signature).hexdigest()


def get_token_file():
    """
    Return the filename that contains the authentication token for the API key.

    @return: the filename where to store the authentication token
    @rtype:  C{str}
    """
    return os.path.join(CONFIG_DIR, 'flickr', 'auth.xml')


def _read_token(response):
    """
    Extract the authentication token from an XML response to
    flickr.auth.getToken.

    @param response: the response to flickr.auth.getToken
    @type response:  C{str} or C{unicode}

    @return: the authentication token
    @rtype:  C{str}
    """
    dom = minidom.parseString(response)
    return dom.getElementsByTagName('token')[0].firstChild.nodeValue.strip()


def _save_token(token):
    """
    Save the retrieved authentication token to the local cache.

    @param token: the authentication token as returned by flickr.auth.getToken
    @type token:  C{str}
    """
    token_file = get_token_file()
    token_path = os.path.dirname(token_file)
    if not os.path.exists(token_path):
        os.makedirs(token_path, 0700)
    f = file(token_file, 'w')
    f.write(token)
    f.close()


def get_cached_token():
    """
    Return the authentication token that has been locally cached.

    @return:        the authentication token
    @rtype:         C{str}

    @raise IOError: if the token cache file cannot be found
    """
    token_file = get_token_file()
    if not os.path.exists(token_file):
        raise IOError('No cached token file!')
    f = file(token_file)
    token = _read_token(f.read())
    f.close()
    return token


def generate_call_uri(base_uri=BASE_API_URI, method=None, arguments={},
                      authenticated=False, sign=False):
    """
    Generate the URI to use for a Flickr API call using an HTTP GET method.

    The 'method' and 'api_key' arguments are automatically added if not
    present. If authentication is required, the 'auth_token' argument will be
    automatically added. If signing is required, the signature will be computed
    and the 'api_sig' argument added.

    @param base_uri:      the base URI of the request
    @type base_uri:       C{str}
    @param method:        the Flickr API method name
    @type method:         C{str}
    @param arguments:     the arguments (key-value) of the method
    @type arguments:      C{dict} of C{str}
    @param authenticated: whether the method requires authentication
    @type authenticated:  C{bool}
    @param sign:          whether the method requires signing
    @type sign:           C{bool}

    @return:              the complete URI of the request to send
    @rtype:               C{str}
    """
    full_arguments = arguments.copy()

    if method is not None:
        full_arguments['method'] = method

    # the API key is always required
    full_arguments['api_key'] = _key

    if authenticated:
        sign = True # all authenticated calls must be signed
        full_arguments['auth_token'] = get_cached_token()

    if sign:
        sign_arguments(full_arguments)

    argvalues = [key + '=' + value for key, value in full_arguments.items()]
    uri = base_uri + '&'.join(argvalues)
    return uri


def authenticate_1(provider):
    """
    First step of the authentication to the Flickr API: request a Frob (see
    http://www.flickr.com/services/api/flickr.auth.getFrob.html for details)
    and generate a login URL that the application should open in a browser
    window to let the user allow Elisa to access his content. Once the user has
    allowed the application to access his account, call the authenticate_2
    method with the generated frob to get an authentication token.

    If the application is already authenticated (authentication token locally
    cached), return the token. No subsequent call to authenticate_2 is needed.

    @param provider: the Flickr resource provider
    @type provider:  L{elisa.plugins.flickr.resource_provider.FlickrResourceProvider}

    @return: a deferred that when fired returns a dictionary containing either
             the login URL and the frob, or the token if already authenticated.
    @rtype:  L{twisted.internet.defer.Deferred}
    """
    try:
        token = get_cached_token()
        # The authentication token has already been locally cached
        return defer.succeed({'token': token})
    except IOError:
        pass

    def got_frob(result_model):
        dom = minidom.parseString(result_model.data)
        frob = dom.getElementsByTagName('frob')[0].firstChild.nodeValue
        # Build the login URL
        perms = 'delete'
        login = generate_call_uri('http://flickr.com/services/auth/?',
                                  arguments={'perms': perms, 'frob': frob},
                                  sign=True)
        return {'url': login, 'frob': frob}

    uri = generate_call_uri(method='flickr.auth.getFrob', sign=True)
    result_model, request_defer = provider.get(uri)
    request_defer.addCallback(got_frob)
    return request_defer


def authenticate_2(provider, frob):
    """
    Second step of the authentication to the Flickr API: return the auth token
    for the given frob, if one has been attached.

    @param provider: the Flickr resource provider
    @type provider:  L{elisa.plugins.flickr.resource_provider.FlickrResourceProvider}
    @param frob:     the frob
    @type frob:      C{str}

    @return:         the authentication token if existing, C{None} otherwise
    @rtype:          C{str}
    """
    def got_token(result_model):
        dom = minidom.parseString(result_model.data)
        token = dom.getElementsByTagName('token')[0].firstChild.nodeValue
        _save_token(dom.toprettyxml())
        return token

    uri = generate_call_uri(method='flickr.auth.getToken',
                            arguments={'frob': frob}, sign=True)
    result_model, request_defer = provider.get(uri)
    request_defer.addCallback(got_token)
    return request_defer


def build_photo_url(model):
    """
    Build the real URL corresponding to a photo model.

    See http://www.flickr.com/services/api/misc.urls.html for details on how to
    build the URL of a photo.
    Once built this URL is cached in the model itself for faster subsequent
    accesses.

    @param model: a photo model
    @type model:  L{elisa.plugins.flickr.models.FlickrPhotoModel}
    """
    # Thumbnail:
    # http://farm{farm-id}.static.flickr.com/{server-id}/{id}_{secret}_t.jpg
    # Small photo:
    # http://farm{farm-id}.static.flickr.com/{server-id}/{id}_{secret}_m.jpg
    # Medium photo:
    # http://farm{farm-id}.static.flickr.com/{server-id}/{id}_{secret}.jpg
    # Large photo:
    # http://farm{farm-id}.static.flickr.com/{server-id}/{id}_{secret}_b.jpg
    url = 'http://farm%d.static.flickr.com/%d/%s_%s'
    url %= (model.farm, model.server, model.flickr_id, model.secret)
    thumbnail = MediaUri(url + '_t.jpg')
    small = MediaUri(url + '_m.jpg')
    medium = MediaUri(url + '.jpg')
    large = MediaUri(url + '_b.jpg')
    model.images = ImageModel()
    model.images.references = [thumbnail, small, medium, large]
