#!/usr/bin/env python

__author__ = 'Adam R. Smith, Michael Meisinger'


import collections
import datetime
import string
import time
import simplejson
import base64
import uuid
import os
import re
from types import NoneType
from copy import deepcopy
from mi.core.time_tools import timegm_to_float

DICT_LOCKING_ATTR = "__locked__"

class DotNotationGetItem(object):
    """ Drive the behavior for DotList and DotDict lookups by dot notation, JSON-style. """

    def _convert(self, val):
        """ Convert the type if necessary and return if a conversion happened. """
        if isinstance(val, dict) and not isinstance(val, DotDict):
            return DotDict(val), True
        elif isinstance(val, list) and not isinstance(val, DotList):
            return DotList(val), True

        return val, False

    def __getitem__(self, key):
        val = super(DotNotationGetItem, self).__getitem__(key)
        val, converted = self._convert(val)
        if converted: self[key] = val

        return val

    def __contains__(self, item):
        return hasattr(self, item)

class DotList(DotNotationGetItem, list):
    """ Partner class for DotDict; see that for docs. Both are needed to fully support JSON/YAML blocks. """

    def __iter__(self):
        for val in super(DotList, self).__iter__():
            val, converted = self._convert(val)
            yield val

class DotDict(DotNotationGetItem, dict):
    """
    Subclass of dict that will recursively look up attributes with dot notation.
    This is primarily for working with JSON-style data in a cleaner way like javascript.
    Note that this will instantiate a number of child DotDicts when you first access attributes;
    do not use in performance-critical parts of your code.
    """

    def __dir__(self):
        return [k for k in self.__dict__.keys() + self.keys() if k != DICT_LOCKING_ATTR]

    def __getattr__(self, key):
        """ Make attempts to lookup by nonexistent attributes also attempt key lookups. """
        if self.has_key(key):
            return self[key]

        if not self.__dict__.has_key(DICT_LOCKING_ATTR):
            import sys
            import dis
            frame = sys._getframe(1)
            if '\x00%c' % dis.opmap['STORE_ATTR'] in frame.f_code.co_code:
                self[key] = DotDict()
                return self[key]

        raise AttributeError(key)

    def __setattr__(self, key, value):
        if key in dir(dict):
            raise AttributeError('%s conflicts with builtin.' % key)
        if self.__dict__.has_key(DICT_LOCKING_ATTR):
            raise AttributeError('Setting %s on a locked DotDict' % key)
        if isinstance(value, dict):
            self[key] = DotDict(value)
        else:
            self[key] = value

    def copy(self):
        return deepcopy(self)

    def get_safe(self, qual_key, default=None):
        """
        @brief Returns value of qualified key, such as "system.name" or None if not exists.
                If default is given, returns the default. No exception thrown.
        """
        value = get_safe(self, qual_key)
        if value is None:
            value = default
        return value

    def lock(self):
        self.__dict__[DICT_LOCKING_ATTR] = True

    def clear(self):
        if self.__dict__.has_key(DICT_LOCKING_ATTR):
            del self.__dict__[DICT_LOCKING_ATTR]

        super(DotDict, self).clear()

    def pop(self, *args, **kwargs):
        if self.__dict__.has_key(DICT_LOCKING_ATTR):
            raise AttributeError('Cannot pop on a locked DotDict')
        return super(DotDict, self).pop(*args, **kwargs)

    def popitem(self):
        if self.__dict__.has_key(DICT_LOCKING_ATTR):
            raise AttributeError('Cannot popitem on a locked DotDict')
        return super(DotDict, self).popitem()

    def as_dict(self):
        return simple_deepcopy(self)


    @classmethod
    def fromkeys(cls, seq, value=None):
        return DotDict(dict.fromkeys(seq, value))


class DictDiffer(object):
    """
    Calculate the difference between two dictionaries as:
    (1) items added
    (2) items removed
    (3) keys same in both but changed values
    (4) keys same in both and unchanged values
    """
    def __init__(self, current_dict, past_dict):
        self.current_dict, self.past_dict = current_dict, past_dict
        self.set_current, self.set_past = set(current_dict.keys()), set(past_dict.keys())
        self.intersect = self.set_current.intersection(self.set_past)
    def added(self):
        return self.set_current - self.intersect
    def removed(self):
        return self.set_past - self.intersect
    def changed(self):
        return set(o for o in self.intersect if self.past_dict[o] != self.current_dict[o])
    def unchanged(self):
        return set(o for o in self.intersect if self.past_dict[o] == self.current_dict[o])


def simple_deepcopy(coll):
    """Performs a recursive deep copy on given collection, only using dict, list and set
    collection types and not checking for cycles."""
    if isinstance(coll, dict):
        return {k: simple_deepcopy(v) for k, v in coll.iteritems()}
    elif isinstance(coll, set):
        return {simple_deepcopy(v) for v in coll}
    elif hasattr(coll, "__iter__"):
        return [simple_deepcopy(v) for v in coll]
    else:
        return coll


def quacks_like_dict(object):
    """Check if object is dict-like"""
    return isinstance(object, collections.Mapping)

def dict_merge(base, upd, inplace=False):
    """Merge two deep dicts non-destructively.
    Uses a stack to avoid maximum recursion depth exceptions.
    @param base the dict to merge into
    @param upd the content to merge
    @param inplace change base if True, otherwise deepcopy base
    @retval the merged dict (base if inplace else a merged deepcopy)
    """
    assert quacks_like_dict(base), quacks_like_dict(upd)
    dst = base if inplace else deepcopy(base)

    stack = [(dst, upd)]
    while stack:
        current_dst, current_src = stack.pop()
        for key in current_src:
            if key not in current_dst:
                current_dst[key] = current_src[key]
            else:
                if quacks_like_dict(current_src[key]) and quacks_like_dict(current_dst[key]) :
                    stack.append((current_dst[key], current_src[key]))
                else:
                    current_dst[key] = current_src[key]
    return dst

def get_safe(dict_instance, keypath, default=None):
    """
    Returns a value with in a nested dict structure from a dot separated
    path expression such as "system.server.host" or a list of key entries
    @retval Value if found or None
    """
    try:
        obj = dict_instance
        keylist = keypath if type(keypath) is list else keypath.split('.')
        for key in keylist:
            obj = obj[key]
        return obj
    except Exception, ex:
        return default

def named_any(name):
    """
    Retrieve a Python object by its fully qualified name from the global Python
    module namespace.  The first part of the name, that describes a module,
    will be discovered and imported.  Each subsequent part of the name is
    treated as the name of an attribute of the object specified by all of the
    name which came before it.
    @param name: The name of the object to return.
    @return: the Python object identified by 'name'.
    """
    assert name, 'Empty module name'
    names = name.split('.')

    topLevelPackage = None
    moduleNames = names[:]
    while not topLevelPackage:
        if moduleNames:
            trialname = '.'.join(moduleNames)
            try:
                topLevelPackage = __import__(trialname)
            except Exception, ex:
                moduleNames.pop()
        else:
            if len(names) == 1:
                raise Exception("No module named %r" % (name,))
            else:
                raise Exception('%r does not name an object' % (name,))

    obj = topLevelPackage
    for n in names[1:]:
        obj = getattr(obj, n)

    return obj

def for_name(modpath, classname):
    '''
    Returns a class of "classname" from module "modname".
    '''
    module = __import__(modpath, fromlist=[classname])
    classobj = getattr(module, classname)
    return classobj()

def current_time_millis():
    return int(round(time.time() * 1000))

get_ion_ts_millis = current_time_millis

def get_ion_ts():
    """
    Returns standard ION representation of a global timestamp.
    It is defined as a str representing an integer number, the millis in UNIX epoch
    """
    return str(current_time_millis())

def get_datetime(ts, local_time=True):
    """
    Returns a datatime instance in either local time or GMT time based on the given ION
    timestamp
    @param ts  ION timestamp (str with millis in epoch)
    @param local_time  if True, returns local time (default), otherwise GMT
    @retval  datetime instance
    """
    tsf = float(ts) / 1000
    timev = time.localtime(tsf) if local_time else time.gmtime(tsf)
    dt = datetime.datetime.fromtimestamp(timegm_to_float(timev))
    return dt

def get_datetime_str(ts, show_millis=False, local_time=True):
    """
    Returns a string with date and time representation from an ION timestamp
    @param ts  ION timestamp (str with millis in epoch)
    @param show_millis  If True, appends the milli seconds
    @param local_time  if True, returns local time (default), otherwise GMT
    @retval  str with ION standard date and time representation
    """
    dt = get_datetime(ts, local_time)
    dts = str(dt)
    if show_millis:
        dts += "." + ts[-3:]
    return dts

def parse_ion_ts(ts):
    return float(ts) / 1000.0


def is_valid_ts(s):

    if not is_string(s):
        return False

    #Checking for only numeric characters and length of 13
    #TODO - will need to change this length to 14 in the year 2286!!!  :)
    if not re.match("^[0-9]*$", s) or len(s) < 13 or len(s) > 14:
        return False
    try:
        val = int(s)
        if val < 0:
            return False

        return True
    except ValueError:
        return False

def is_string(obj):
    try:
        return isinstance(obj, basestring)
    except NameError:
        return isinstance(obj, str)


def itersubclasses(cls, _seen=None):
    """
    itersubclasses(cls)
    http://code.activestate.com/recipes/576949-find-all-subclasses-of-a-given-class/

    Generator over all subclasses of a given class, in depth first order.
    """

    if not isinstance(cls, type):
        raise TypeError('itersubclasses must be called with '
                        'new-style classes, not %.100r' % cls)
    if _seen is None: _seen = set()
    try:
        subs = cls.__subclasses__()
    except TypeError: # fails only when cls is type
        subs = cls.__subclasses__(cls)
    for sub in subs:
        if sub not in _seen:
            _seen.add(sub)
            yield sub
            for sub in itersubclasses(sub, _seen):
                yield sub

def getleafsubclasses(cls):
    """
    Returns all subclasses that have no further subclasses, for the given class
    """
    scls = itersubclasses(cls)
    return [s for s in scls if not s.__subclasses__()]

# _abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789
BASIC_VALID = "_%s%s" % (string.ascii_letters, string.digits)
# -_.()abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789
NORMAL_VALID = "-_.() %s%s" % (string.ascii_letters, string.digits)

def create_valid_identifier(name, valid_chars=BASIC_VALID, dot_sub=None, ws_sub=None):
    if dot_sub:
        name = name.replace('.', dot_sub)
    if ws_sub:
        name = name.replace(' ', ws_sub)
    return ''.join(c for c in name if c in valid_chars)

def create_basic_identifier(name):
    return create_valid_identifier(name, dot_sub='_', ws_sub='_')

def is_basic_identifier(name):
    return name == create_basic_identifier(name)

def is_valid_identifier(name, valid_chars=BASIC_VALID, dot_sub=None, ws_sub=None):
    return name == create_valid_identifier(name, valid_chars=valid_chars, dot_sub=dot_sub, ws_sub=ws_sub)

#Used by json encoder
def ion_object_encoder(obj):
    return obj.__dict__

def make_json(data):
    result = simplejson.dumps(data, default=ion_object_encoder, indent=2)
    return result


#Global utility functions for generating unique names and UUIDs
# get a UUID - URL safe, Base64
def get_a_Uuid():
    r_uuid = base64.urlsafe_b64encode(uuid.uuid4().bytes)
    return r_uuid.replace('=', '')

# generate a unique identifier based on a UUID and optional information
def create_unique_identifier(prefix=''):
    return prefix + '_' + get_a_Uuid()

def get_default_sysname():
    return 'ion_%s' % os.uname()[1].replace('.', '_')

def get_default_container_id():
    return string.replace('%s_%d' % (os.uname()[1], os.getpid()), ".", "_")


BASIC_TYPE_SET = {str, bool, int, float, long, NoneType}

def recursive_encode(obj, encoding="utf8"):
    """Recursively walks a dict/list collection and in-place encodes any unicode keys and values in
    dicts and lists to UTF-8 encoded str"""
    if isinstance(obj, dict):
        fix_list = None
        for k, v in obj.iteritems():
            if type(k) is unicode:
                if fix_list is None:
                    fix_list = []
                fix_list.append(k)
            if type(v) in BASIC_TYPE_SET:
                continue
            if type(v) is unicode:
                obj[k] = v.encode(encoding)
                continue
            recursive_encode(v, encoding=encoding)
        if fix_list:
            for k in fix_list:
                v = obj.pop(k)
                newk = k.encode(encoding)
                obj[newk] = v
    elif isinstance(obj, list):
        for i, v in enumerate(obj):
            if type(v) in BASIC_TYPE_SET:
                continue
            if type(v) is unicode:
                obj[i] = v.encode(encoding)
                continue
            recursive_encode(v, encoding=encoding)
    else:
        raise RuntimeError("unknown type: %s" % type(obj))

    return obj


