# Copyright (C) 2007 Alexandre Conrad, alexandre (dot) conrad (at) gmail (dot) com
#
# This module is part of FormAlchemy and is released under
# the MIT License: http://www.opensource.org/licenses/mit-license.php

import cgi
import warnings
import logging
logger = logging.getLogger('formalchemy.' + __name__)

MIN_SA_VERSION = '0.4.5'
from sqlalchemy import __version__
if __version__.split('.') < MIN_SA_VERSION.split('.'):
    raise ImportError('Version %s or later of SQLAlchemy required' % MIN_SA_VERSION)

from sqlalchemy.orm.attributes import InstrumentedAttribute
from sqlalchemy.orm.properties import SynonymProperty
from sqlalchemy.orm import compile_mappers, object_session, class_mapper
from sqlalchemy.orm.session import Session
from sqlalchemy.orm.scoping import ScopedSession
from sqlalchemy.orm.dynamic import DynamicAttributeImpl
from sqlalchemy.util import OrderedDict

from formalchemy import fields
from formalchemy import renderers
from formalchemy import fatypes

compile_mappers() # initializes InstrumentedAttributes


try:
    # 0.5
    from sqlalchemy.orm.attributes import manager_of_class
    def _get_attribute(cls, p):
        manager = manager_of_class(cls)
        return manager[p.key]
except ImportError:
    # 0.4
    def _get_attribute(cls, p):
        return getattr(cls, p.key)
    

def prettify(text):
    """
    Turn an attribute name into something prettier, for a default label where none is given.

    >>> prettify("my_column_name")
    'My column name'
    """
    return text.replace("_", " ").capitalize()



class SimpleMultiDict(dict):
    """
    Adds `getone`, `getall` methods to dict.  Assumes that values are either
    a string or a list of strings.
    """
    def getone(self, key):
        if key not in self:
            raise KeyError(key)
        v = dict.get(self, key)
        if v is None or isinstance(v, basestring) or isinstance(v, cgi.FieldStorage):
            return v
        return v[0]
    def getall(self, key):
        v = dict.get(self, key)
        if v is None:
            return []
        elif isinstance(v, basestring):
            return [v]
        return v


class ModelRenderer(object):
    """
    The `ModelRenderer` class is the superclass for all classes needing to deal 
    with `model` access and supporting rendering capabilities.
    """
    prettify = staticmethod(prettify)

    def __init__(self, model, session=None, data=None, prefix=None):
        """ 
        - `model`: 
              a SQLAlchemy mapped class or instance.  New object creation
              should be done by passing the class, which will need a default
              (no-parameter) constructor.  After construction or binding of
              the :class:`~formalchemy.forms.FieldSet`, the instantiated object will be available as
              the `.model` attribute.

        - `session=None`: 
              the session to use for queries (for relations). If `model` is associated
              with a session, that will be used by default. (Objects mapped with a
              `scoped_session
              <http://www.sqlalchemy.org/docs/05/session.html#contextual-thread-local-sessions>`_
              will always have a session. Other objects will
              also have a session if they were loaded by a Query.)

        - `data=None`: 
              dictionary-like object of user-submitted data to validate and/or
              sync to the `model`. Scalar attributes should have a single
              value in the dictionary; multi-valued relations should have a
              list, even if there are zero or one values submitted.  Currently,
              pylons request.params() objects and plain dictionaries are known
              to work.

        - `prefix=None`:
              the prefix to prepend to html name attributes. This is useful to avoid 
              field name conflicts when there are two fieldsets creating objects
              from the same model in one html page.  (This is not needed when
              editing existing objects, since the object primary key is used as part
              of the field name.)


        Only the `model` parameter is required.

        After binding, :class:`~formalchemy.forms.FieldSet`'s `model` attribute will always be an instance.
        If you bound to a class, `FormAlchemy` will call its constructor with no
        arguments to create an appropriate instance. 

        .. NOTE::

          This instance will not be added to the current session, even if you are using `Session.mapper`.

        All of these parameters may be overridden by the `bind` or `rebind`
        methods.  The `bind` method returns a new instance bound as specified,
        while `rebind` modifies the current :class:`~formalchemy.forms.FieldSet` and has
        no return value. (You may not `bind` to a different type of SQLAlchemy
        model than the initial one -- if you initially bind to a `User`, you
        must subsequently bind `User`'s to that :class:`~formalchemy.forms.FieldSet`.)

        Typically, you will configure a :class:`~formalchemy.forms.FieldSet` once in
        your common form library, then `bind` specific instances later for editing. (The
        `bind` method is thread-safe; `rebind` is not.)  Thus:

        load stuff:

        >>> from formalchemy.tests import FieldSet, User, session

        now, in `library.py`

        >>> fs = FieldSet(User)
        >>> fs.configure(options=[]) # put all configuration stuff here
        
        and in `controller.py`
        
        >>> from library import fs
        >>> user = session.query(User).first()
        >>> fs2 = fs.bind(user)
        >>> html = fs2.render()

        The `render_fields` attribute is an OrderedDict of all the `Field`'s
        that have been configured, keyed by name. The order of the fields
        is the order in `include`, or the order they were declared
        in the SQLAlchemy model class if no `include` is specified.

        The `_fields` attribute is an OrderedDict of all the `Field`'s
        the ModelRenderer knows about, keyed by name, in their
        unconfigured state.  You should not normally need to access
        `_fields` directly.
        
        (Note that although equivalent `Field`'s (fields referring to
        the same attribute on the SQLAlchemy model) will equate with
        the == operator, they are NOT necessarily the same `Field`
        instance.  Stick to referencing `Field`'s from their parent
        `FieldSet` to always get the "right" instance.)
        """
        self._fields = OrderedDict()
        self._render_fields = OrderedDict()
        self.model = self.session = None
        self.prefix = prefix

        if not model:
            raise Exception('model parameter may not be None')
        ModelRenderer.rebind(self, model, session, data)

        cls = isinstance(self.model, type) and self.model or type(self.model)
        try:
            class_mapper(cls)
        except:
            # this class is not managed by SA.  extract any raw Fields defined on it.
            keys = cls.__dict__.keys()
            keys.sort(lambda a, b: cmp(a.lower(), b.lower())) # 2.3 support
            for key in keys:
                field = cls.__dict__[key]
                if isinstance(field, fields.Field):
                    if field.name and field.name != key:
                        raise Exception('Fields in a non-mapped class have the same name as their attribute.  Do not manually give them a name.')
                    field.name = field.key = key
                    self.append(field)
            if not self._fields:
                raise Exception("not bound to a SA instance, and no manual Field definitions found")
        else:
            # SA class.
            # load synonyms so we can ignore them
            synonyms = set(p for p in class_mapper(cls).iterate_properties 
                           if isinstance(p, SynonymProperty))
            # attributes we're interested in
            attrs = []
            for p in class_mapper(cls).iterate_properties:
                attr = _get_attribute(cls, p)
                if ((isinstance(p, SynonymProperty) or attr.property.key not in (s.name for s in synonyms))
                    and not isinstance(attr.impl, DynamicAttributeImpl)):
                    attrs.append(attr)
            # sort relations last before storing in the OrderedDict
            L = [fields.AttributeField(attr, self) for attr in attrs]
            L.sort(lambda a, b: cmp(a.is_relation, b.is_relation)) # note, key= not used for 2.3 support
            self._fields.update((field.key, field) for field in L)