#!/usr/bin/env python
'''
@author Luke Campbell <LCampbell at ASAScience dot com>
@file ion/services/dm/utility/types.py
@date Thu Jan 17 15:51:16 EST 2013
'''
from pyon.container.cc import Container
from pyon.core.exception import BadRequest, NotFound
from pyon.public import CFG, RT
from pyon.util.memoize import memoize_lru
from pyon.util.log import log

from ion.services.dm.inventory.dataset_management_service import DatasetManagementService

from coverage_model.parameter_types import QuantityType, ArrayType, TextType
from coverage_model.parameter_types import RecordType, CategoryType 
from coverage_model.parameter_types import ConstantType, ConstantRangeType, RaggedArrayType
from coverage_model.parameter_functions import AbstractFunction, ExternalFunction
from coverage_model import ParameterFunctionType, ParameterContext, SparseConstantType, ConstantType

from copy import deepcopy
from udunitspy.udunits2 import Unit, System
from interface.objects import ParameterContext as ParameterContextResource

import ast
import numpy as np
import re
from uuid import uuid4


class TypesManager(object):
    system = System(path=CFG.get_safe('units', 'res/config/units/udunits2.xml'))

    def __init__(self, dataset_management_client, resource_ids, resource_objs):
        self.dataset_management = dataset_management_client
        self.resource_ids = resource_ids
        self.resource_objs = resource_objs

    def get_array_type(self,parameter_type=None, encoding=None):
        if encoding in ('string', 'char', 'str', '', 'opaque'):
            return self.get_string_type()
        if 'int' in encoding:
            fill = -9999
        elif 'float' in encoding:
            fill = np.nan
        else:
            raise TypeError("Unknown encoding for array types %s" % encoding)
        return ArrayType(inner_encoding=encoding, inner_fill_value=fill)

    def get_boolean_type(self):
        return QuantityType(value_encoding = np.dtype('int8'))

    def get_category_type(self, parameter_type, encoding, code_set):
        groups = re.match(r'(category)(<)(.*)(:)(.*)(>)', parameter_type).groups()
        dtype = np.dtype(groups[2])
        try:
            if isinstance(code_set, basestring):
                codestr = code_set.replace('\\', '')
                code_set = ast.literal_eval(codestr)
                for k in code_set.keys():
                    v = code_set[k]
                    del code_set[k]
                    code_set[dtype.type(k)] = v
        except:
            raise TypeError('Invalid Code Set: %s' % code_set)
        return CategoryType(categories=code_set)

    def get_constant_type(self,parameter_type, encoding, code_set):
        groups = re.match(r'(constant)(<)(.*)(>)', parameter_type).groups()
        if groups[2] == 'str' or groups[2] == 'string':
            if encoding[0] != 'S' or len(encoding) < 2:
                raise TypeError('Constant strings need to be specified with a valid length (e.g. S8): %s' % encoding)
            slen = encoding[1:]
            try:
                int(slen)
            except ValueError:
                raise TypeError('Improper String Length: %s' % slen)
            parameter_type = QuantityType(value_encoding = np.dtype('|%s' % encoding))
        else:
            parameter_type = self.get_parameter_type(groups[2], encoding, code_set)
        return ConstantType(parameter_type)

    def get_fill_value(self, val, encoding, ptype=None):

        if isinstance(ptype,ConstantRangeType):
            matches = re.match(r'\((-?\d+(\.\d*)?), ?(-?\d+(\.\d*)?)\)', val)
            if matches:
                groups = matches.groups()
                return (self.get_fill_value(groups[0], encoding), self.get_fill_value(groups[2], encoding))
            else:
                retval = self.get_fill_value(val,encoding)
                if retval is not None:
                    raise TypeError('Invalid range fill value: %s' % val)

        if 'float' in encoding and val == '':
            return -9999999.0
        if 'int' in encoding and val == '':
            return -9999999
        if val == '':
            return None
        if isinstance(val, basestring) and val.lower() == 'none':
            return None
        if isinstance(val, basestring) and val.lower() == 'empty':
            return ''
        if isinstance(val, basestring) and val.lower() == 'false':
            return 0
        if isinstance(val, basestring) and val.lower() == 'true':
            return 1
        if 'float' in encoding:
            return float(val)
        if 'int' in encoding:
            return int(val)
        if isinstance(val, basestring) and encoding.lower()[0] == 's':
            return val
        if isinstance(val, basestring) and encoding.lower() == 'opaque':
            raise TypeError('Fill value for opaque must be None, not: %s' % val)
        if 'category' in encoding.lower() and isinstance(val, (int, float)):
            return val
        else:
            raise TypeError('Invalid Fill Value: %s' % val) # May never be called

    def get_parameter_type(self,parameter_type, encoding, code_set=None, pfid=None, pmap=None, target_info=None):
        if parameter_type == 'quantity':
            return self.get_quantity_type(parameter_type,encoding)
        elif re.match(r'array<?.*>?', parameter_type):
            return self.get_array_type(parameter_type, encoding)
        elif re.match(r'category<.*>', parameter_type):
            return self.get_category_type(parameter_type, encoding, code_set)
        elif re.match(r'str(ing)?', parameter_type):
            return self.get_string_type()
        elif re.match(r'constant<.*>', parameter_type):
            return self.get_constant_type(parameter_type, encoding, code_set)
        elif parameter_type == 'boolean':
            return self.get_boolean_type()
        elif re.match(r'range<.*>', parameter_type):
            return self.get_array_type(parameter_type, encoding)
        elif re.match(r'record<.*>', parameter_type):
            return self.get_record_type()
        elif parameter_type == 'function':
            return self.get_function_type(parameter_type, encoding, pfid, pmap)
        elif parameter_type == 'sparse':
            return self.get_sparse_type(parameter_type, encoding)
        elif parameter_type == 'ragged':
            return self.get_ragged_type(parameter_type, encoding)
        elif parameter_type == 'external':
            return self.get_external_type(parameter_type, encoding, target_info)
        else:
            raise TypeError( 'Invalid Parameter Type: %s' % parameter_type)


    def get_param_name(self, pdid):
        try:
            param_name = self.resource_objs[pdid].name
        except KeyError:
            raise KeyError('Parameter %s was not loaded' % pdid)
        return param_name

    def get_pfunc(self,pfid):
        # Preload Case
        if not pfid:
            raise TypeError('No parameter function id specified')
        if pfid.startswith('PFID'):
            if pfid not in self.resource_objs: 
                raise KeyError('Function %s was not loaded' % pfid)

            pf = self.resource_objs[pfid]
            func = DatasetManagementService.get_coverage_function(pf)
            return func
        # System Case
        else:
            pf = Container.instance.resource_registry.read(pfid)
            func = DatasetManagementService.get_coverage_function(pf)
            return func

    def _placeholder(self, value, pc_callback):
        placeholder = value.replace('LV_','')
        document_key = ''
        if '||' in placeholder:
            document_key, placeholder = placeholder.split('||')
        document_val = placeholder
        placeholder = '%s_%s' % (placeholder, uuid4().hex)
        pc = pc_callback(placeholder)
        pc.lookup_value = document_val
        pc.document_key = document_key
        pc.uom = '1'
        pc.visible = False
        ctxt_id = self.dataset_management.create_parameter_context(name=placeholder, parameter_context=pc.dump())
        return ctxt_id, placeholder


    def get_lookup_value(self,value):
        return self._placeholder(value, lambda placeholder : ParameterContext(name=placeholder, param_type=SparseConstantType(base_type=ConstantType(value_encoding='float64'), fill_value=-9999.)))
    
    def get_array_lookup_value(self,value):
        return self._placeholder(value, lambda placeholder : ParameterContext(name=placeholder, param_type=SparseConstantType(base_type=ArrayType(inner_encoding='float64', inner_fill_value=-9999.))))

    def get_string_array_lookup_value(self, value):
        return self._placeholder(value, lambda placeholder : ParameterContext(name=placeholder, param_type=SparseConstantType(base_type=ArrayType())))

    def get_cc_value(self, value):
        placeholder = value.lower()
        # Check to see if this coefficient exists already

        hits, _ = Container.instance.resource_registry.find_resources(name=placeholder, restype=RT.ParameterContext, id_only=True)
        if hits:
            return hits[0], placeholder

        pc = ParameterContext(name=placeholder, param_type=SparseConstantType(value_encoding='float64'), fill_value=-9999.)
        pc.uom = '1'
        pc.display_name = 'Calibration Coefficient ' + placeholder
        ctxt_id = self.dataset_management.create_parameter_context(name=placeholder, parameter_context=pc.dump())
        return ctxt_id, placeholder


    def has_lookup_value(self, context):
        if isinstance(context.param_type, ParameterFunctionType) and hasattr(context.function,'lookup_values'):
            return True
        return False

    def get_lookup_value_ids(self, context):
        if isinstance(context.param_type, ParameterFunctionType):
            return getattr(context.function,'lookup_values',[])
        return []

    def get_cc_value_ids(self, context):
        if isinstance(context.param_type, ParameterFunctionType):
            return getattr(context.function,'coefficients',[])
        return []

    def evaluate_pmap(self,pfid, pmap):
        lookup_values = []
        coefficients = []
        for k,v in pmap.iteritems():
            if isinstance(v, dict):
                pfid_,pmap_ = v.popitem()
                pmap[k] = self.evaluate_pmap(pfid_, pmap_)
            if isinstance(v, basestring) and 'PD' in v:
                pmap[k] = self.get_param_name(v)
            if isinstance(v, basestring) and 'LV' in v:
                ctxt_id, placeholder = self.get_lookup_value(v)
                pmap[k] = placeholder
                lookup_values.append(ctxt_id)
            if isinstance(v, basestring) and 'CC' in v:
                ctxt_id, placeholder = self.get_cc_value(v)
                pmap[k] = placeholder
                coefficients.append(ctxt_id)

        func = self.get_pfunc(pfid)
        func.param_map = pmap
        if lookup_values:
            func.lookup_values = lookup_values
        if coefficients:
            func.coefficients = coefficients
        return func

    def evaluate_qc(self):
        pass

        
    @memoize_lru(maxsize=100)
    def find_function(self,name):
        res_obj, _ = Container.instance.resource_registry.find_resources(name=name, restype=RT.ParameterFunction, id_only=False)

        if res_obj:
            return res_obj[0]._id, DatasetManagementService.get_coverage_function(res_obj[0])
        else:
            raise KeyError('%s was never loaded' % name)

    def find_grt(self):
        return self.find_function('global_range_test')
    
    def find_spike(self):
        return self.find_function('dataqc_spiketest')
    
    def find_stuck_value(self):
        return self.find_function("dataqc_stuckvaluetest")

    def find_trend_test(self):
        return self.find_function("dataqc_polytrendtest")

    def find_propagate_test(self):
        return self.find_function("dataqc_propagateflags")

    def find_gradient_test(self):