# -*- coding: utf-8 -*-
#
# Copyright (c) 2011,2012 RMIT e-Research Office
#   (RMIT University, Australia)
# All rights reserved.
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
#    *  Redistributions of source code must retain the above copyright
#       notice, this list of conditions and the following disclaimer.
#    *  Redistributions in binary form must reproduce the above copyright
#       notice, this list of conditions and the following disclaimer in the
#       documentation and/or other materials provided with the distribution.
#    *  Neither the name of RMIT University nor the
#       names of its contributors may be used to endorse or promote products
#       derived from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND ANY
# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE REGENTS AND CONTRIBUTORS BE LIABLE FOR ANY
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
"""
.. module:: ReposPlugin
   :synopsis: The API for plugins to put media objects into SMRA

.. moduleauthor:: Ian Thomas <Ian.Edward.Thomas@rmit.edu.au>


"""
import logging
from dateutil import parser
from datetime import date
import time

from django.conf import settings
from django.utils.importlib import import_module
from django.core.exceptions import ImproperlyConfigured
from django.core.exceptions import ObjectDoesNotExist

from smra.smra_portal.models import MediaObject
from smra.smra_portal.models import Schema
from smra.smra_portal.models import ParameterName
from smra.smra_portal.models import System
from smra.smra_portal.models import Repository
from smra.smra_portal.models import MediaObjectParameterSet
from smra.smra_portal.models import Key
from smra.smra_portal.models import MediaObjectParameter

logger = logging.getLogger(__name__)


class Timer():
    """ A seconds-based timer for parts of the system.
    """
    
    def __init__(self):
        """ Start the timer.
        """
        self.t = time.time()
    
    def get_start_time(self):
        """ Gets starting time
        """
        return self.t
    
    def get_current_time(self):
        """ Gets current time
        """        
        return time.time()
    
    def get_elapsed_time(self):
        """ Gets difference between current time and start time
        """ 
        return time.time() - self.t

    
class Error(Exception):
    """ Base class for ReposPlugin Errors"""
    pass


class InvalidMediaObject(Error):
    """ There is an error in the format of the media object definition"""
    pass

    
class InvalidConfigurationException(Error):
    """ Bad Configuration of the AFI plugin"""
    pass
   

class MissingIDException(Error):
    """ Missing or invalid ID from Mayer"""
    pass

class ReposPlugin(object):
    """ Handles the interface to repos ingestion plugins
     for different databases"""
      
    def __init__(self):
        self.sys = None
        self.errors = []
    
    def ingest_media_objects(self, system):
        """Given a system, dispatch each ingestion plugin 
        to populate media objects and associated metadata"""
        try:
            self.sys = System.objects.get(name=system)
        except System.DoesNotExist:
            self.sys = System()
            self.sys.name = system
            self.sys.save()            
        result = {}
        for repos in settings.REPOS:
            path = repos
            try:
                dot = repos.rindex('.') 
            except ValueError:
                raise ImproperlyConfigured('%s isn\'t a repos module' % path)
            module, classname = path[:dot], path[dot + 1:]
            try:
                mod = import_module(module)
            except ImportError, ex:
                raise ImproperlyConfigured('Error importing repos %s: "%s"' % 
                                       (module, ex))
            try:
                repos_class = getattr(mod, classname)
            except AttributeError:
                raise ImproperlyConfigured('repos module "%s" does not '
                                           'define a "%s" class' % 
                                       (module, classname))
            instance = repos_class()
            result[path] = instance.ingest(self,system)            
        return result

    def magic_encode(self,s):
        from django.utils.encoding import smart_unicode
        try:
            #FIXME: I think I want to decode here to convert raw bytestrings
            #into unicode. I believe I am effectivly removing converting any
            # unicode into byte strings, which are then cast when needed back to 
            # unicode. Yuck.
            res = s.encode('ascii','ignore')
        except UnicodeDecodeError:
            # but here I then try to convert BONZA into unicode
            res = smart_unicode(s,encoding="cp1252")
        return res
        
    def _process_desc(self, obj, id):
        # If we have updated description then update object.
        if id :
            desc = self.magic_encode(id)
        else :
            desc = ''       
        if obj:
            obj.description = desc
            obj.save()
        return obj
                         
    def _process_ident(self, id,repos):
        # If key is found, then grab associated object, else make new
        #  key and default object
        if not repos:
            raise InvalidConfigurationException("no repository found for this id")
        if id :
            #ident = self.magic_encode(id)
            ident = ("%s" % id).encode('ascii','ignore')
        else :
            ident = ''      
        try:                
            key = Key.objects.get(ident=repr(ident), repos=repos)
            obj = key.media_object                
        except ObjectDoesNotExist:
            obj = MediaObject(description='', repos=repos) # will be updated
            obj.save() 
            key = Key(ident=repr(ident), media_object=obj, repos=repos)
            key.save()
        return (obj, key)
        
    def _process_metadata(self, obj, id, repos_name):
        rank = 100
        logger.debug("id=%s" % list(id)) 
        # If we have meta data, then pull apart and create parameters.
        for metadata in id:
            
            logger.debug("metadata=%s" % list(metadata)) 
            (schema_namespace, schema_name) = metadata[0]
            meta = metadata[1]
            
            logger.debug("trying %s %s" % (schema_namespace, schema_name) ) 
            
            try:
                schemadef = Schema.objects.get(namespace
                                               =schema_namespace,
                                               name
                                               =schema_name)
            except ObjectDoesNotExist:
                # no such schema
                logger.debug("no such schema")
                continue          
            try:
                mop = MediaObjectParameterSet.objects.get(
                                                    media_object=obj,
                                                    schema=schemadef)
            except ObjectDoesNotExist:
                mop = MediaObjectParameterSet(media_object=obj,
                                               schema=schemadef,
                                               ranking=rank)
                rank -= 10
                mop.save()
                
            #FIXME: there is no current way to remove old metadata schemas
            # if they change at the source.
                
            logger.debug("mop=%s" % mop)
            
            logger.debug('schemadef = %s' % schemadef)
            logger.debug('sch = %s/%s' % (schema_namespace,
                                           schema_name))
            for paramkey in meta:
                logger.debug("paramkey = %s" % paramkey)
                
                try:
                    param = ParameterName.objects.get(schema=schemadef, 
                                                  name=paramkey)
                except ObjectDoesNotExist:  
                    # if not matching parameter in schema then we skip
                    logger.debug("no maching parameter")
                    continue
                
                except ObjectDoesNotExist:  
                    # if not matching parameter in schema then we skip
                    logger.debug("no maching parameter")
                    continue
                
                in_val = meta[paramkey]
                logger.debug("in_val = %s" % in_val)
                new_val = ''
                from django.utils.encoding import smart_unicode 
                if (param.type == ParameterName.STRING
                     or param.type == ParameterName.LINK):
                    if in_val :
                        new_val = self.magic_encode(in_val)
                    else :
                        new_val = '' 

                # Incoming dates can be encoded in usual formats,
                # but are stored
                # in SMRA in DATE_FORMAT fixed form because we store dates as 
                # strings in MediaObjectParameter.value. This is the format 
                # used directly used in searches
                # in SMRA  
                if param.type == ParameterName.DATE:
                    if in_val :
                        try:
                            new_val = parser.parse(in_val).date().strftime(
                                                    ParameterName.DATE_FORMAT)
                        except ValueError:
                            error_message = "Bad parsing of %s (date) %s " \
                                         "in object number %s (%s)" \
                                         % (param.name, in_val, obj.id, 
                                            repos_name)
                            logger.error(error_message)
                            self.errors.append(error_message)
                            new_val = in_val                  
                if param.type == ParameterName.YEAR:
                    try:   
                        new_val = int(in_val)
                    except TypeError:
                        new_val = 0
                    except ValueError:
                        error_message = "Bad parsing of %s (year) %s " \
                                     "in object number %s (%s)" \
                                     % (param.name, in_val, obj.id, repos_name)
                        logger.error(error_message)
                        self.errors.append(error_message)
                        new_val = in_val
                if param.type == ParameterName.NUMERIC:
                    try: 
                        new_val = int(in_val)
                    except TypeError:
                        new_val = 0
                    except ValueError:
                        error_message = "Bad parsing of %s (number) %s " \
                                     "in object number %s (%s)" \
                                     % (param.name, in_val, obj.id, repos_name)
                                     
                        logger.error(error_message)
                        self.errors.append(error_message)
                        new_val = ""
                logger.debug("new_val=%s" % new_val)
                
                # TODO: need to have some way of signalling deletion
                # of parameter sets (if gone on source) and deletion
                # of media objcts (if gone on source)
                
                try:
                    mops = MediaObjectParameter.objects.get(name=param, 
                                                        paramset=mop)
                    mops.value = new_val
                    mops.save()
                except ObjectDoesNotExist:
                    mops = MediaObjectParameter(name=param, 
                                                paramset=mop, 
                                                value=new_val)
                    mops.save()
                    
        return obj

    def create_schema(self,ns,name,desc,parameters):
        """
        Creates a schema with given parameters if it does not exist.  Will
        not update any existing matching schema
        """
        try:
            sch = Schema.objects.get(namespace=ns, name=name)
        except Schema.DoesNotExist:
            sch = Schema()
            sch.namespace = ns
            sch.name = name
            sch.description = desc
            sch.save()
            for param in parameters:
                mops = ParameterName(schema=sch,
                                     name=param[0],
                                     type=param[1],
                                     ranking=param[2],
                                     initial=param[3])
                mops.save()
        except Schema.MultipleObjectsReturned:
            return
        else:
            return
        return
            
    def set_last_ingested_metadata(self,obj,metadata,repos_name):
        """
        Sets metadata for an existing object
        """
        obj = self._process_metadata(obj,metadata,repos_name)
        return obj
        
    def create_media_object(self, repos_name, ident, description, metadata):
        """Create a media object and available meta data based on ingesting
         a repository record. This function is a hook for the ingestion plugins
          to access models (indirectly). Returns the media_object.
         """
        logger.debug('create_media_object called')
        repos = None
        if not repos_name:
            raise InvalidConfigurationException(
                            "No repository specified for ingestion")
        try:
            repos = Repository.objects.get(name=repos_name, 
                                           system=self.sys)
        except Repository.DoesNotExist:
            raise InvalidConfigurationException(
                            "Repository %s does not exist" % repos_name)
        except Repository.MultipleObjectsReturned:
            raise InvalidConfigurationException(
                            "Repository %s found multiple times "
                            "in database" % repos_name)

        (obj, key) = self._process_ident(ident,repos)                     
        obj = self._process_desc(obj, description)
        obj =  self._process_metadata(obj, metadata, repos_name)
        if obj:
            return obj
        else:
            raise InvalidMediaObject("object missing description")   
