# -*- coding: utf-8 -*-
# Copyright (C) 2000-2015 Mag. Christian Tanzer. All rights reserved
# Glasauergasse 32, A--1130 Wien, Austria. tanzer@swing.co.at
# ****************************************************************************
#
# This module is licensed under the terms of the BSD 3-Clause License
# <http://www.c-tanzer.at/license/bsd_3c.html>.
# ****************************************************************************
#
#++
# Name
#    TFL.Record
#
# Purpose
#    Class emulating a struct/record (but dynamically)
#
# Revision Dates
#    14-Jun-2000 (CT) Creation
#    11-Sep-2000 (CT) `quote' added to `str'
#    21-Jan-2001 (CT) `__getattr__' uses `try/except' instead of `has_key'
#    16-Apr-2003 (CT) `copy` added
#    21-Jan-2006 (MG)  Moved into `TFL` package
#    20-Mar-2006 (CT)  `__getitem__` added
#    31-May-2006 (WPR) `__iter__` added
#    19-Oct-2007 (PGO) self.kw needs name-mangling, otherwise a stored dict
#                      named `kw` will be silently modified
#     8-Nov-2007 (CT)  Use `_kw` instead of `__kw` (and modernized)
#     8-Nov-2007 (CT)  `assert` statements added to avoid silent errors
#    23-Jan-2008 (CT)  `Record_S` added
#     1-Feb-2008 (MG)  `__nonzero__` added
#    27-Feb-2009 (CT)  `__setitem__` added
#     9-Dec-2009 (CT)  `__delattr__` added
#     9-Dec-2009 (CT) `__repr__` and `_formatted_kw` changed (use `%r`
#                     instead of explicitly quoted `%s`)
#    21-Dec-2009 (CT) `__getstate__` and `__setstate__` added
#    20-Feb-2010 (CT) `__contains__` added
#    20-Jul-2011 (CT) `_properties` added to allow subclasses to define
#                     property setters that actually work
#    10-Oct-2014 (CT) Use `portable_repr`
#    15-Oct-2014 (CT) Add `_portable_repr_Record`, protect against recursion
#    23-Jan-2015 (CT) Add support for dotted names to `__setattr__`
#    13-Apr-2015 (CT) Add `_import_cb_json_dump`
#     6-May-2015 (CT) Use `TFL.json_dump.jsonified`
#    29-May-2015 (CT) Add `* ds` to `Record.__init__`
#    16-Oct-2015 (CT) Add `__future__` imports
#    ««revision-date»»···
#--

from   __future__  import absolute_import
from   __future__  import division
from   __future__  import print_function
from   __future__  import unicode_literals

from   _TFL                import TFL

from   _TFL.portable_repr  import portable_repr
from   _TFL.predicate      import sorted
from   _TFL.pyk            import pyk

import _TFL._Meta.Object

@pyk.adapt__bool__
class Record (TFL.Meta.Object) :
    """Class emulating a struct/record (but dynamically).

    >>> r = Record (x = "y", kw = dict (foo = 42))
    >>> print (r.x)
    y
    >>> r.kw
    {'foo': 42}

    >>> bool (r)
    True

    >>> bool (Record ())
    False

    >>> r
    Record (kw = {'foo' : 42}, x = 'y')

    >>> from _TFL.json_dump import to_string as jsonified
    >>> jsonified (r)
    '{"kw": {"foo": 42}, "x": "y"}'

    """

    _properties = ()

    def __init__ (self, * ds, ** kw) :
        _kw = dict ()
        for d in ds :
            _kw.update (d)
        _kw.update (kw)
        assert "_kw"           not in _kw
        assert "copy"          not in _kw
        assert "_formatted_kw" not in _kw
        assert "_properties"   not in _kw
        assert not any (p in _kw for p in self._properties)
        self.__dict__ ["_kw"] = _kw
    # end def __init__

    def copy (self, ** kw) :
        result = self.__class__ (** self._kw)
        result._kw.update (kw)
        return result
    # end def copy

    def _formatted_kw (self, seen = None) :
        if seen is None :
            seen = set ([id (self)])
        return ", ".join \
            ( (   "%s = %s" % (k, portable_repr.call (v, seen))
              for (k, v) in sorted (pyk.iteritems (self._kw))
              )
            )
    # end def _formatted_kw

    def __bool__ (self) :
        return bool (self._kw)
    # end def __bool__

    def __contains__ (self, item) :
        return item in self._kw
    # end def __contains__

    def __delattr__ (self, name) :
        del self._kw [name]
    # end def __delattr__

    def __getattr__ (self, name) :
        try :
            return self._kw [name]
        except KeyError :
            raise AttributeError (name)
    # end def __getattr__

    def __getitem__ (self, key) :
        return self._kw [key]
    # end def __getitem__

    def __getstate__ (self) :
        return self._kw
    # end def __getstate__

    def __iter__ (self) :
        return iter (self._kw)
    # end def __iter__

    def __len__ (self) :
        return len (self._kw)
    # end def __len__

    def __repr__ (self) :
        return portable_repr (self)
    # end def __repr__

    def __setattr__ (self, name, value) :
        if name in self._properties :
            self.__super.__setattr__ (name, value)
        elif "." in name :
            this  = self
            names = name.split (".")
            for name in names [:-1] :
                nested = this.__class__ ()
                setattr (this, name, nested)
                this   = nested
            setattr (this, names [-1], value)
        else :
            self._kw [name] = value
    # end def __setattr__

    def __setitem__ (self, name, value) :
        if name in self._properties :
            self.__super.__setattr__ (name, value)
        else :
            self._kw [name] = value
    # end def __setitem__

    def __setstate__ (self, state) :
        self.__dict__ ["_kw"] = state
    # end def __setstate__

    def __str__ (self) :
        return "(%s)" % (self._formatted_kw (), )
    # end def __str__

# end class Record

class Record_S (Record) :
    """Record usable as dict for %-interpolation with nested attributes.

    >>> c = Record_S (x = 1)
    >>> o = Record_S (a = 42, b = Record_S (a = 137, b = "foo", c = c))
    >>> print ("o.a = %(a)s, o.b.a = %(b.a)s, o.b.c.x = %(b.c.x)s" % o)
    o.a = 42, o.b.a = 137, o.b.c.x = 1

    >>> c
    Record_S (x = 1)

    >>> o
    Record_S (a = 42, b = Record_S (a = 137, b = 'foo', c = Record_S (x = 1)))

    >>> from _TFL.json_dump import to_string as jsonified
    >>> jsonified (o)
    '{"a": 42, "b": {"a": 137, "b": "foo", "c": {"x": 1}}}'

    >>> c.y = o

    >>> c
    Record_S (x = 1, y = Record_S (a = 42, b = Record_S (a = 137, b = 'foo', c = Record_S (...))))

    >>> o
    Record_S (a = 42, b = Record_S (a = 137, b = 'foo', c = Record_S (x = 1, y = Record_S (...))))

    >>> jsonified (o)
    Traceback (most recent call last):
    ...
    ValueError: Circular reference detected

    """

    def __getitem__ (self, key) :
        try :
            return self.__super.__getitem__ (key)
        except KeyError :
            o = self
            for k in key.split (".") :
                try :
                    o = getattr (o, k)
                except AttributeError :
                    raise KeyError (key)
            return o
    # end def __getitem__

# end class Record_S

@portable_repr.add_type (Record)
@portable_repr.recurses
def _portable_repr_Record (obj, seen) :
    return "%s (%s)" % (obj.__class__.__name__, obj._formatted_kw (seen))
# end def _portable_repr_Record

@portable_repr.recursion_repr.add_type (Record)
def _recursion_repr_Record (obj) :
    return "%s (...)" % (obj.__class__.__name__, )
# end def _recursion_repr_Record

@TFL._Add_Import_Callback ("_TFL.json_dump")
def _import_cb_json_dump (module) :
    @module.default.add_type (Record)
    def json_encode_record (o) :
        return o._kw
# end def _import_cb_json_dump

if __name__ != "__main__" :
    TFL._Export ("*")
### __END__ TFL.Record
