#!/usr/bin/env python
# -*- coding: utf-8 -*-
# $Id: __init__.py 9439 2015-09-25 08:01:30Z Eric $
#
# Copyright (c) 2015 Nuwa Information Co., Ltd, All Rights Reserved.
#
# Licensed under the Proprietary License,
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at our web site.
#
# See the License for the specific language governing permissions and
# limitations under the License.
#
# $Author: Eric $ (last)
# $Date: 2015-09-25 16:01:30 +0800 (Fri, 25 Sep 2015) $
# $Revision: 9439 $

# Avoid conflict with Iuno.cloud.logging moudle.
from __future__ import absolute_import

import os
import time
import logging as pyLogging
import cPickle as pickle

import singletonmixin

from Iuppiter.Logging import createLogger

class LocalCacheProvider(singletonmixin.Singleton):
    """
    Local cache provider which controls all cache access in local file storage.
    """

    def __init__(self):
        """
        Constructor.
        """
        super(LocalCacheProvider, self).__init__()
        self._dir = None

    def getFilePath(self, file):
        """
        Get file path in cache directory.

        @param file File name.
        @return Absolute file path if cache directory valid.
        """
        return (os.path.abspath(os.path.join(self._dir, file)) if self._dir
                else None)

    def read(self, file, key):
        """
        Read data from cache file.

        @param file File name.
        @param key Data key.
        @return Data.
        """
        path = self.getFilePath(file)
        if not path:
            return None

        try:
            with open(path, "rb") as f:
                content = pickle.loads(f.read())

            data = content.get(key, None)
            expire = content.get('__%s__expire__' % key, None)

            if expire and time.time() > expire:
                return None

            return data
        except:
            return None

    def write(self, file, key, value, expire=None):
        """
        Write data to cache file.

        @param file File name.
        @param key Data key.
        @param value Data.
        @param expire Expire milliseconds, None if valid forever.
        @return True if succeeded.
        """
        path = self.getFilePath(file)
        if not path:
            return False

        try:
            with open(path, "rb") as f:
                content = pickle.loads(f.read())
        except:
            content = {}

        content[key] = value

        expireKey = '__%s__expire__' % key
        if expire:
            content[expireKey] = time.time() + expire
        else:
            content.pop(expireKey, None)

        try:
            with open(path, "wb") as f:
                f.write(pickle.dumps(content))
        except:
            return False

        return True

localCache = LocalCacheProvider.getInstance()

def attachSettings(settingsLocals, services=(
                    'queue', 'mail', 'logging', 'browser'),
                   serviceSettings=None,
                   loggingFile=None, loggingLevel=None,
                   cache=False, cacheDir=None):
    """
    Attach cloud related settings to settings.py.
    It must be called in settings by passing locals().

    Please note: You must define additional Celery related settings before
                 attach, or all settings that we attached will be overwritten
                 by your except you carefully do that.

    @param settingsLocals locals() in settings.py.
    @param services Services will be enabled, default: (
                        'queue', 'mail', 'logging', 'browser').
    @param serviceSettings The settings for services, for instance: {
                               'queue': {
                                   'backendClasses': ...,
                                   'preferred': ...,
                                   '...': ...,
                               },
                           }

                           Basic configuration keys:
                               'backendClassess': Backend classes, None to use
                                                  default (predefined) backends.
                               'preferred': Preferred backend implementations
                                            or machine, ex: {
                                                'Amazon': ('i-1ea2b35a',),
                                                'Nuwa': ('cloud.tracedig.com',
                                                         'naga.servehttp.com'),
                                            }
    @param loggingFile Logging file, None if you want to output to stdout.
    @param loggingLevel Logging level, string: 'INFO', 'DEBUG'...etc.
    @param cache Enable local cache or not.
    @param cacheDir Cache directory, only valid if cache = True.
    """
    if serviceSettings is None:
        serviceSettings = {}

    if cache and not cacheDir:
        settingsPath = settingsLocals.get('__file__', os.path.curdir)
        prjDir = os.path.dirname(os.path.abspath(settingsPath))
        cacheDir = os.path.join(prjDir, '.cloud')

    if cache and cacheDir:
        cacheDir = os.path.abspath(cacheDir)
        if not os.path.exists(cacheDir):
            os.mkdir(cacheDir)
        if not os.path.isdir(cacheDir):
            raise RuntimeError('cacheDir must be a directory.')

        localCache._dir = cacheDir

    logger = None        
        
    if loggingFile and not loggingLevel:
        loggingLevel = 'INFO'

    if loggingLevel:
        logger = createLogger(__name__, loggingFile,
                              pyLogging.getLevelName(loggingLevel))
                              
    if not logger:
        logger = pyLogging.getLogger(__name__)

    for service in services:
        try:
            if '.' in service:
                servicePackage = classForName(service)
            else: # Default Iuno.cloud services.
                servicePackage = classForName('Iuno.cloud.%s' % service)
        except ImportError, ex:
            logger.warn('%s can not be imported: %s' % (service, str(ex)))
            continue

        if hasattr(servicePackage, 'getDefaultConfig'):
            config = servicePackage.getDefaultConfig()
        else:
            config = {}

        config.update(serviceSettings.get(service, {
            'backendClasses': None,
            'preferred': None,
        }))

        backendClasses = config.get('backendClasses', None)
        preferred = config.get('preferred', None)

        backends = getBackends(servicePackage, backendClasses,
                               preferred=preferred.keys()
                                         if preferred else None,
                               context=settingsLocals,
                               config=config)

        servicePackage.attachSettings(settingsLocals, backends, preferred,
                                      config)

import pkgutil

from Iuppiter.Util import classForName
from Iuppiter.Util import preferredSort

def getBackendClasses(service, preferred=None, context=None):
    """
    Get all cloud implementation backend classes by service.

    @param service Service module or its locals dict.
    @param preferred Preferred backends.
    @param context Settings context.
    @return Backend classes.
    """
    if isinstance(service, dict):
        getDefaultPreferred = service.get('getDefaultPreferred', None)
    elif hasattr(service, 'getDefaultPreferred'):
        getDefaultPreferred = service.getDefaultPreferred
    else:
        getDefaultPreferred = None
    
    if not preferred and getDefaultPreferred:
        preferred = getDefaultPreferred(context=context)

    if isinstance(service, dict):
        servicePath = os.path.abspath(os.path.dirname(service['__file__']))
    else:
        servicePath = os.path.abspath(os.path.dirname(service.__file__))
        
    backendsPath = os.path.join(servicePath, 'backend')

    if not os.path.exists(backendsPath):
        return [] # Without backends.

    backendNames = [name for _, name, _ in pkgutil.iter_modules([backendsPath])]

    if preferred:
        backendNames = preferredSort(backendNames, preferred)

    backendClasses = []
    for name in backendNames:
        className = '%s.backend.%s.Backend' % (
            service.__name__ if not isinstance(service, dict) 
                             else service['__name__'], 
            name)
        try:
            clz = classForName(className)
        except ImportError:
            pyLogging.error('Unable to load backend: %s' % className)
            continue

        backendClasses.append(clz)

    return backendClasses

def getBackends(service, classes=None, preferred=None, context=None,
                config=None):
    """
    Get all cloud implementation backends by service.

    @param service Service module or its locals dict.
    @param classes Backend classes.
    @param preferred Preferred backends.
    @param context Settings context.
    @param config Service settings.
    @return Backend instances.
    """
    if not classes:
        classes = getBackendClasses(service, preferred, context=context)

    return [clz(context, config) for clz in classes]

class HostInfo(object):
    """
    Wrapper class to hold return value of tryGet.
    """
    def __init__(self, backend, info):
        """
        Constructor.

        @param backend Backend.
        @param info Information.
        """
        self.backend = backend
        self.info = info

def tryGet(methodName, backends, preferred=None, *args, **kws):
    """
    Try get information from backends with preferred by given method name.

    @param methodName Backend class method name.
    @param backends Backend instances.
    @param preferred Preffered backends.
    @param *args Arguments which will pass to method.
    @param **kws Keyword arguments which will pass to method.
    @return (Backend, Object to represents information)
    """
    if preferred:
        values = dict([(b.name, b) for b in backends])
        names = preferredSort(values.keys(), preferred)
        backends = [values[n] for n in names]

    for backend in backends:
        preferredMachine = (preferred.get(backend.name, None) if preferred else
                            None)
        info = getattr(backend, methodName)(preferredMachine, *args, **kws)
        if info:
            return HostInfo(backend, info)
    return None
