"""
ExtGen --- Python Extension module Generator.

Defines Component and Container classes.
"""

import os
import re
import sys
import time

class ComponentMetaClass(type):

    classnamespace = {}

    def __new__(mcls, *args, **kws):
        cls = type.__new__(mcls, *args, **kws)
        n = cls.__name__
        c = ComponentMetaClass.classnamespace.get(n)
        if c is None:
            ComponentMetaClass.classnamespace[n] = cls
        else:
            if not c.__module__=='__main__':
                sys.stderr.write('ComponentMetaClass: returning %s as %s\n'\
                                 % (cls, c))
            ComponentMetaClass.classnamespace[n] = c
            cls = c
        return cls

    def __getattr__(cls, name):
        try: return ComponentMetaClass.classnamespace[name]
        except KeyError: pass
        raise AttributeError("'%s' object has no attribute '%s'"%
                             (cls.__name__, name))

class Component(object):

    __metaclass__ = ComponentMetaClass

    container_options = dict()
    component_container_map = dict()
    default_container_label = None
    default_component_class_name = 'Code'
    template = ''

    def __new__(cls, *args, **kws):
        obj = object.__new__(cls)
        obj._provides = kws.get('provides', None)
        obj.parent = None
        obj.containers = {} # holds containers for named string lists
        obj._components = [] # holds pairs (<Component subclass instance>, <container name or None>)
        obj._generate_components = {} # temporary copy of components used for finalize and generate methods.
        obj = obj.initialize(*args, **kws)    # initialize from constructor arguments
        return obj

    def components(self):
        if Component._running_generate:
            try:
                return self._generate_components[Component._running_generate_id]
            except KeyError:
                pass
            while self._generate_components: # clean up old cache
                self._generate_components.popitem()
            self._generate_components[Component._running_generate_id] = l = list(self._components)
            return l
        return self._components
    components = property(components)

    def initialize(self, *components, **options):
        """
        Set additional attributes, add components to instance, etc.
        """
        # self.myattr = ..
        # map(self.add, components)
        return self

    def finalize(self):
        """
        Set components after all components are added.
        """
        return

    def __repr__(self):
        return '%s(%s)' % (self.__class__.__name__, ', '.join([repr(c) for (c,l) in self.components]))

    def provides(self):
        """
        Return a code idiom name that the current class defines.

        Used in avoiding redefinitions of functions and variables.
        """
        if self._provides is None:
            return '%s_%s' % (self.__class__.__name__, id(self))
        return self._provides
    provides = property(provides)

    def warning(message):
        #raise RuntimeError('extgen:' + message)
        print >> sys.stderr, 'extgen:',message
    warning = staticmethod(warning)

    def info(message):
        print >> sys.stderr, message
    info = staticmethod(info)

    def __getattr__(self, attr):
        if attr.startswith('container_'): # convenience feature
            return self.get_container(attr[10:])
        if attr.startswith('component_'): # convenience feature
            return self.get_component(attr[10:])
        raise AttributeError('%s instance has no attribute %r' % (self.__class__.__name__, attr))

    def __add__(self, other): # convenience method
        self.add(other)
        return self
    __iadd__ = __add__

    def _get_class_names(cls):
        if not issubclass(cls, Component):
            return [cls]
        r = [cls]
        for b in cls.__bases__:
            r += Component._get_class_names(b)
        return r
    _get_class_names = staticmethod(_get_class_names)

    def add(self, component, container_label=None):
        """
        Append component and its target container label to components list.
        """
        if isinstance(component, tuple) and len(component)==2 and isinstance(component[0], Component):
            assert container_label is None, `container_label`
            component, container_label = component
        if not isinstance(component, Component) and self.default_component_class_name!=component.__class__.__name__:
            clsname = self.default_component_class_name
            if clsname is not None:
                component = getattr(Component, clsname)(component)
            else:
                raise ValueError('%s.add requires Component instance but got %r' \
                                 % (self.__class__.__name__, component.__class__.__name__))
        if container_label is None:
            container_label = self.default_container_label
            for n in self._get_class_names(component.__class__):
                try:
                    container_label = self.component_container_map[n.__name__]
                    break
                except KeyError:
                    pass
        if container_label is None:
            container_label = component.__class__.__name__
        self.components.append((component, container_label))
        component.update_parent(self)
        return

    def update_parent(self, parent):
        pass

    def get_path(self, *paths):
        if not hasattr(self, 'path'):
            if paths:
                return os.path.join(*paths)
            return ''
        if not self.parent:
            return os.path.join(*((self.path,) + paths))
        return os.path.join(*((self.parent.get_path(), self.path)+paths))

    def get_component(self, cls):
        if isinstance(cls, str):
            cls = getattr(Component, cls)
        if isinstance(self, cls):
            return self
        if self.parent:
            return self.parent.get_component(cls)
        self.warning('could not find %r parent component %s, returning self'\
                  % (self.__class__.__name__, cls.__name__))
        return self

    _running_generate = False
    _running_generate_id = 0
    _generate_dry_run = True

    def generate(self, dry_run=True):
        old_dry_run = Component._generate_dry_run
        Component._generate_dry_run = dry_run
        Component._running_generate_id += 1
        Component._running_generate = True
        self._finalize()
        result = self._generate()
        Component._running_generate = False
        Component._generate_dry_run = old_dry_run
        return result

    def _finalize(self):
        # recursively finalize all components.
        for component, container_key in self.components:
            old_parent = component.parent
            component.parent = self
            component._finalize()
            component.parent = old_parent
        self.finalize()

    def _generate(self):
        """
        Generate code idioms (saved in containers) and
        return evaluated template strings.
        """
        #self.finalize()

        # clean up containers
        self.containers = {}
        for n in dir(self):
            if n.startswith('container_') and isinstance(getattr(self, n), Container):
                delattr(self, n)

        # create containers
        for k,kwargs in self.container_options.items():
            self.containers[k] = Container(**kwargs)

        # initialize code idioms
        self.init_containers()

        # generate component code idioms
        for component, container_key in self.components:
            if not isinstance(component, Component):
                result = str(component)
                if container_key == '<IGNORE>':
                    pass
                elif container_key is not None:
                    self.get_container(container_key).add(result)
                else:
                    self.warning('%s: no container label specified for component %r'\
                                 % (self.__class__.__name__,component))
                continue
            old_parent = component.parent
            component.parent = self
            result = component._generate()
            if container_key == '<IGNORE>':
                pass
            elif container_key is not None:
                if isinstance(container_key, tuple):
                    assert len(result)==len(container_key),`len(result),container_key`
                    results = result
                    keys = container_key
                else:
                    assert isinstance(result, str) and isinstance(container_key, str), `result, container_key`
                    results = result,
                    keys = container_key,
                for r,k in zip(results, keys):
                    container = component.get_container(k)
                    container.add(r, component.provides)
            else:

                self.warning('%s: no container label specified for component providing %r'\
                                 % (self.__class__.__name__,component.provides))
            component.parent = old_parent

        # update code idioms
        self.update_containers()

        # fill templates with code idioms
        templates = self.get_templates()
        if isinstance(templates, str):
            result = self.evaluate(templates)
        else:
            assert isinstance(templates, (tuple, list)),`type(templates)`
            result = tuple(map(self.evaluate, templates))
        return result

    def init_containers(self):
        """
        Update containers before processing components.
        """
        # container = self.get_container(<key>)
        # container.add(<string>, label=None)
        return

    def update_containers(self):
        """
        Update containers after processing components.
        """
        # container = self.get_container(<key>)
        # container.add(<string>, label=None)
        return

    def get_container(self, name):
        """ Return named container.

        Rules for returning containers:
        (1) return local container if exists
        (2) return parent container if exists
        (3) create local container and return it with warning
        """
        # local container
        try:
            return self.containers[name]
        except KeyError:
            pass

        # parent container
        parent = self.parent
        while parent is not None:
            try:
                return parent.containers[name]
            except KeyError:
                parent = parent.parent
                continue

        # create local container
        self.warning('Created container for %r with name %r, define it in'\
                     ' parent .container_options mapping to get rid of this warning' \
                     % (self.__class__.__name__, name))
        c = self.containers[name] = Container()
        return c

    def get_templates(self):
        """
        Return instance templates.
        """
        return self.template

    def evaluate(self, template, **attrs):
        """
        Evaluate template using instance attributes and code
        idioms from containers.
        """
        d = self.containers.copy()
        for n in dir(self):
            if n in ['show', 'build'] or n.startswith('_'):
                continue
            v = getattr(self, n)
            if isinstance(v, str):
                d[n] = v
        d.update(attrs)
        for label, container in self.containers.items():
            if not container.use_indent:
                continue
            replace_list = set(re.findall(r'[ ]*%\('+label+r'\)s', template))
            for s in replace_list:
                old_indent = container.indent_offset
                container.indent_offset = old_indent + len(s) - len(s.lstrip())
                i = template.index(s)
                template = template[:i] + str(container) + template[i+len(s):]
                container.indent_offset = old_indent
        try:
            template = template % d
        except KeyError, msg:
            raise KeyError('%s.container_options needs %s item' % (self.__class__.__name__, msg))
        return re.sub(r'.*[<]KILLLINE[>].*(\n|$)','', template)


    _registered_components_map = {}

    def register(*components):
        """
        Register components so that component classes can use
        predefined components via `.get(<provides>)` method.
        """
        d = Component._registered_components_map
        for component in components:
            provides = component.provides
            if provides in d:
                Component.warning('component that provides %r is already registered, ignoring.' % (provides))
            else:
                d[provides] = component
        return
    register = staticmethod(register)

    def get(provides):
        """
        Return predefined component with given provides property..
        """
        try:
            return Component._registered_components_map[provides]
        except KeyError:
            pass
        raise KeyError('no registered component provides %r' % (provides))
    get = staticmethod(get)

    def numpy_version(self):
        import numpy
        return numpy.__version__
    numpy_version = property(numpy_version)

class Container(object):
    """
    Container of a list of named strings.

    >>> c = Container(separator=', ', prefix='"', suffix='"')
    >>> c.add('hey',1)
    >>> c.add('hoo',2)
    >>> print c
    "hey, hoo"
    >>> c.add('hey',1)
    >>> c.add('hey2',1)
    Traceback (most recent call last):
    ...
    ValueError: Container item 1 exists with different value

    >>> c2 = Container()
    >>> c2.add('bar')
    >>> c += c2
    >>> print c
    "hey, hoo, bar"

    """
    __metaclass__ = ComponentMetaClass

    def __init__(self,
                 separator='\n', prefix='', suffix='',
                 skip_prefix_when_empty=False,
                 skip_suffix_when_empty=False,
                 default = '', reverse=False,
                 user_defined_str = None,
                 use_indent = False,
                 indent_offset = 0,
                 use_firstline_indent = False, # implies use_indent
                 replace_map = {},
                 ignore_empty_content = False,
                 skip_prefix_suffix_when_single = False
                 ):
        self.list = []
        self.label_map = {}

        self.separator = separator
        self.prefix = prefix
        self.suffix = suffix
        self.skip_prefix = skip_prefix_when_empty
        self.skip_suffix = skip_suffix_when_empty
        self.default = default
        self.reverse = reverse
        self.user_str = user_defined_str
        self.use_indent = use_indent or use_firstline_indent
        self.indent_offset = indent_offset
        self.use_firstline_indent = use_firstline_indent
        self.replace_map = replace_map
        self.ignore_empty_content = ignore_empty_content
        self.skip_prefix_suffix_when_single = skip_prefix_suffix_when_single

    def __nonzero__(self):
        return bool(self.list)

    def has(self, label):
        return label in self.label_map

    def get(self, label):
        return self.list[self.label_map[label]]

    def __add__(self, other):
        if isinstance(other, Container):
            lst = [(i,l) for (l,i) in other.label_map.items()]
            lst.sort()
            for i,l in lst:
                self.add(other.list[i], l)
        else:
            self.add(other)
        return self
    __iadd__ = __add__

    def add(self, content, label=None):
        """ Add content to container using label.
        If label is None, an unique label will be generated using time.time().
        """
        if content is None:
            return
        if content=='' and self.ignore_empty_content:
            return
        assert isinstance(content, str),`type(content)`
        if label is None:
            label = time.time()
        if self.has(label):
            d = self.get(label)
            if d!=content:
                raise ValueError("Container item %r exists with different value" % (label))
            return
        for old, new in self.replace_map.items():
            content = content.replace(old, new)
        self.list.append(content)
        self.label_map[label] = len(self.list)-1
        return

    def __str__(self):
        if self.user_str is not None:
            return self.user_str(self)
        if self.list:
            l = self.list
            if self.reverse:
                l = l[:]
                l.reverse()
            if self.use_firstline_indent:
                new_l = []
                for l1 in l:
                    lines = l1.split('\\n')
                    i = len(lines[0]) - len(lines[0].lstrip())
                    indent = i * ' '
                    new_l.append(lines[0])
                    new_l.extend([indent + l2 for l2 in lines[1:]])
                l = new_l
            r = self.separator.join(l)
            if not (len(self.list)==1 and self.skip_prefix_suffix_when_single):
                r = self.prefix + r
                r = r + self.suffix
        else:
            r = self.default
            if not self.skip_prefix:
                r = self.prefix + r
            if not self.skip_suffix:
                r = r + self.suffix
        if r and self.use_indent:
            lines = r.splitlines(True)
            indent = self.indent_offset * ' '
            r = ''.join([indent + line for line in lines])
        return r

    def copy(self, mapping=None, **extra_options):
        options = dict(separator=self.separator, prefix=self.prefix, suffix=self.suffix,
                       skip_prefix_when_empty=self.skip_prefix,
                       skip_suffix_when_empty=self.skip_suffix,
                       default = self.default, reverse=self.reverse,
                       user_defined_str = self.user_str,
                       use_indent = self.use_indent,
                       indent_offset = self.indent_offset,
                       use_firstline_indent = self.use_firstline_indent,
                       replace_map = self.replace_map,
                       ignore_empty_content = self.ignore_empty_content,
                       skip_prefix_suffix_when_single = self.skip_prefix_suffix_when_single
                       )
        options.update(extra_options)
        cpy = Container(**options)
        if mapping is None:
            cpy += self
        else:
            lst = [(i,l) for (l,i) in self.label_map.items()]
            lst.sort()
            for i,l in lst:
                cpy.add(mapping(other.list[i]), l)
        return cpy

def _test():
    import doctest
    doctest.testmod()

if __name__ == "__main__":
    _test()
