#!/usr/bin/env python2
# -*- coding: utf-8 -*-
#
# Copyright (C) 2011 Loic Jaquemet loic.jaquemet+python@gmail.com
#

import logging
import numbers
import sys

from haystack.outputters import Outputter
from haystack import types
from haystack import basicmodel


log = logging.getLogger('python')


class PythonOutputter(Outputter):

    """ Parse a self._ctypes structure and outputs a pure python object."""

    def parse(self, obj, prefix='', depth=50):
        """
        Returns a Plain Old python object as a perfect copy of this self._ctypes object.
        array would be lists, pointers, inner allocators, and circular
        reference should be handled nicely.
        """
        # get self class.
        try:
            obj_module_name = obj.__class__.__module__
            obj_class_name = obj.__class__.__name__
            try:
                obj_module = self._model.get_pythoned_module(obj_module_name)
            except KeyError:
                # FIXME - ctypes modules should not be in sys.modules. what about reloading?
                self._model.build_python_class_clones(sys.modules[obj_module_name])
                obj_module = self._model.get_pythoned_module(obj_module_name)
            my_class = getattr(obj_module, "%s_py" % obj_class_name)
        except AttributeError as e:
            log.warning('did you forget to register your python allocators ?')
            raise
        my_self = my_class()
        my_address = self._ctypes.addressof(obj)
        # keep ref of the POPO too.
        if self._memory_handler.hasRef(my_class, my_address):
            return self._memory_handler.getRef(my_class, my_address)
        # save our POPO in a partially resolved state, to keep from loops.
        self._memory_handler.keepRef(my_self, my_class, my_address)
        for field, typ in basicmodel.get_fields(obj):
            attr = getattr(obj, field)
            try:
                member = self._attrToPyObject(attr, field, typ)
            except NameError as e:
                raise NameError('%s %s\n%s' % (field, typ, e))

            setattr(my_self, field, member)
        # save the original type (me) and the field
        setattr(my_self, '_ctype_', type(obj))
        return my_self

    def _attrToPyObject(self, attr, field, attrtype):
        if self._ctypes.is_basic_type(attrtype):
            if self._ctypes.is_basic_ctype(type(attr)):
                obj = attr.value
            else:
                obj = attr
        elif self._ctypes.is_struct_type(attrtype) or self._ctypes.is_union_type(attrtype):
            attr._mappings_ = self._memory_handler
            obj = self.parse(attr)
        elif self._ctypes.is_array_of_basic_type(attrtype):
            # return a list of int, float, or a char[] to str
            obj = self._utils.ctypes_to_python_array(attr)
        elif self._ctypes.is_array_type(attrtype):
            # array of something else than int/byte
            obj = []
            eltyp = type(attr[0])
            for i in range(0, len(attr)):
                obj.append(self._attrToPyObject(attr[i], i, eltyp))
        elif self._ctypes.is_cstring_type(attrtype):
            obj = self._memory_handler.getRef(
                self._ctypes.CString,
                self._utils.get_pointee_address(
                    attr.ptr))
        elif self._ctypes.is_function_type(attrtype):
            obj = repr(attr)
        elif self._ctypes.is_pointer_type(attrtype):
            # get the cached Value of the LP.
            _subtype = self._utils.get_subtype(attrtype)
            _address = self._utils.get_pointee_address(attr)
            #if field == 'ProcessHeaps':
            #    import code
            #    code.interact(local=locals())
            if _address == 0:
                # Null pointer
                obj = None
            elif self._ctypes.is_pointer_to_void_type(attrtype):
                # TODO: make a prototype for c_void_p loading
                # void types a rereturned as None
                obj = None
            elif self._ctypes.is_array_of_basic_type(attrtype):
                log.error('basic Type array - %s' % (field))
                obj = 'BasicType array'
            else:
                # FIXME we should NOT recurse
                # get the cached Value of the LP.
                cache = self._memory_handler.getRef(_subtype, _address)
                if cache is not None:  # struct, union...
                    obj = self._attrToPyObject(cache, field, _subtype)
                else:
                    # you got here because your pointer is not loaded:
                    #  did you ignore it in expectedValues ?
                    #  is it in the middle of a struct ?
                    #  is that a linked list ?
                    #  is it a invalid instance ?
                    log.debug('Pointer for field:%s %s/%s not in cache '
                              '0x%x' % (field, attrtype, self._utils.get_subtype(attrtype),
                                        _address))
                    return (None, None)
        elif isinstance(attr, numbers.Number):
            # case for int, long. But needs to be after c_void_p pointers case
            obj = attr
        else:
            log.error('toPyObj default to return attr %s' % (type(attr)))
            obj = attr
        return obj


def json_encode_pyobj(obj):
    if hasattr(obj, '_ctype_'):
        return obj.__dict__
    elif type(obj).__name__ == 'int':
        log.warning('found an int')
        return str(obj)
    else:
        return obj


class pyObj(object):

    """
    Base class for a plain old python object.
    all haystack/ctypes classes will be translated in this format before pickling.

    Operations :
        - toString(self, prefix):    print a nicely formatted data structure
                :param prefix: str to insert before each line (\t after that)
        - findCtypes(self) : checks if a self._ctypes is to be found somewhere is the object.
                                            Useful to check if the object can be pickled.
    """

    def toString(self, prefix='', maxDepth=10):
        if maxDepth < 0:
            return '#(- not printed by Excessive recursion - )'
        s = '{\n'
        if hasattr(self, '_ctype_'):
            items = [n for n, t in basicmodel.get_record_type_fields(self._ctype_)]
        else:
            log.warning('no _ctype_')
            items = [n for n in self.__dict__.keys() if n != '_ctype_']
        for attrname in items:
            attr = getattr(self, attrname)
            typ = type(attr)
            s += "%s%s: %s\n" % (prefix,
                                 attrname,
                                 self._attrToString(
                                     attr,
                                     attrname,
                                     typ,
                                     prefix + '\t',
                                     maxDepth=maxDepth - 1))
        s += '}'
        return s

    def _attrToString(self, attr, attrname, typ, prefix, maxDepth):
        s = ''
        if isinstance(attr, tuple) or isinstance(attr, list):
            for i in xrange(0, len(attr)):
                s += '%s,' % (self._attrToString(attr[i],
                                                 i,
                                                 None,
                                                 prefix + '\t',
                                                 maxDepth))
            s = "[%s]," % (s)
        elif not hasattr(attr, '__dict__'):
            s = '%s,' % (repr(attr))
        elif isinstance(attr, pyObj):
            s = '%s,' % (attr.toString(prefix, maxDepth))
        else:
            s = '%s,' % (repr(attr))
        return s

    def __len__(self):
        return self._len_

    def findCtypes(self, cache=None):
        """ recurse on members to check for self._ctypes object. """
        if cache is None:
            cache = set()
        ret = False
        for attrname, attr in self.__dict__.items():
            if id(attr) in cache:  # do not recurse in already parsed
                continue
            # ignore _ctype_, it's a ctype class type, we know that.
            if attrname == '_ctype_':
                cache.add(id(attr))
                continue
            typ = type(attr)
            attr = getattr(self, attrname)
            log.debug('findCtypes on attr %s' % attrname)
            if self._attrFindCtypes(attr, attrname, typ, cache):
                log.warning('Found a self._ctypes in %s' % (attrname))
                ret = True
        return ret

    def _attrFindCtypes(self, attr, attrname, typ, cache):
        ret = False
        cache.add(id(attr))
        if hasattr(attr, '_ctype_'):  # a pyobj
            return attr.findCtypes(cache)
        elif isinstance(attr, tuple) or isinstance(attr, list):
            for el in attr:
                if self._attrFindCtypes(el, 'element', None, cache):
                    log.warning('Found a self._ctypes in array/tuple')
                    return True
        elif types.is_ctypes_instance(attr):
            log.warning('Found a self._ctypes in self %s' % (attr))
            return True
        else:  # int, long, str ...
            ret = False
        return ret

    def __iter__(self):
        """ iterate on a instance's type's _fields_ members following the original type field order """
        for k, typ in basicmodel.get_fields(self._ctype_):
            v = getattr(self, k)
            yield (k, v, typ)
        pass

    # the python cannot contain a ref to a ctypes
    def __getstate__(self):
        d = self.__dict__.copy()
        if '_ctype_' in d:
            d['_ctype_'] = d['_ctype_'].__class__.__name__
        return d

    def __reduce__(self):
        """Explains how to rebuild this class once pickled."""
        state = self.__dict__.copy()
        if '_ctype_' in state:
            state['_ctype_'] = state['_ctype_'].__class__.__name__
        name = self.__class__.__name__
        modulename = self.__module__
        return (_pyObjBuilder(), # __call__
                (modulename, name), # arg for builder
                state
                )


class _pyObjBuilder:
    """Builder of pickled instance of pyObj into properly named POPO"""
    def __call__(self, modulename, classname):
        # make a simple object which has no complex __init__ (this one will do)
        obj = pyObj()
        # class = getattr(containing_class, class_name)
        # kpy = type('%s.%s' % (modulename, classname), (pyObj,), {})
        kpy = type(classname, (pyObj,), {})
        obj.__class__ = kpy
        return obj


def findCtypesInPyObj(memory_handler, obj):
    """ check function to help in unpickling errors correction """
    if hasattr(obj, 'findCtypes'):
        if obj.findCtypes():
            log.warning('Found a self._ctypes in array/tuple')
            return True
    elif isinstance(obj, tuple) or isinstance(obj, list):
        for el in obj:
            if findCtypesInPyObj(memory_handler, el):
                log.warning('Found a self._ctypes in array/tuple')
                return True
    elif types.is_ctypes_instance(obj):
        return True
    return False
