#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""
| This file is part of the web2py Web Framework
| Copyrighted by Massimo Di Pierro <mdipierro@cs.depaul.edu>
| License: LGPLv3 (http://www.gnu.org/licenses/lgpl.html)

Provides:

- List; like list but returns None instead of IndexOutOfBounds
- Storage; like dictionary allowing also for `obj.foo` for `obj['foo']`
"""

from pydal.contrib import portalocker
from gluon._compat import copyreg, pickle, PY2

__all__ = ['List', 'Storage', 'Settings', 'Messages',
           'StorageList', 'load_storage', 'save_storage']

DEFAULT = lambda: 0


class Storage(dict):
    """
    A Storage object is like a dictionary except `obj.foo` can be used
    in addition to `obj['foo']`, and setting obj.foo = None deletes item foo.

    Example::

        >>> o = Storage(a=1)
        >>> print o.a
        1

        >>> o['a']
        1

        >>> o.a = 2
        >>> print o['a']
        2

        >>> del o.a
        >>> print o.a
        None

    """
    __slots__ = ()
    __setattr__ = dict.__setitem__
    __delattr__ = dict.__delitem__
    __getitem__ = dict.get
    __getattr__ = dict.get
    __getnewargs__ = lambda self: getattr(dict,self).__getnewargs__(self)
    __repr__ = lambda self: '<Storage %s>' % dict.__repr__(self)
    # http://stackoverflow.com/questions/5247250/why-does-pickle-getstate-accept-as-a-return-value-the-very-instance-it-requi
    __getstate__ = lambda self: None
    __copy__ = lambda self: Storage(self)

    def getlist(self, key):
        """
        Returns a Storage value as a list.

        If the value is a list it will be returned as-is.
        If object is None, an empty list will be returned.
        Otherwise, `[value]` will be returned.

        Example output for a query string of `?x=abc&y=abc&y=def`::

            >>> request = Storage()
            >>> request.vars = Storage()
            >>> request.vars.x = 'abc'
            >>> request.vars.y = ['abc', 'def']
            >>> request.vars.getlist('x')
            ['abc']
            >>> request.vars.getlist('y')
            ['abc', 'def']
            >>> request.vars.getlist('z')
            []

        """
        value = self.get(key, [])
        if value is None or isinstance(value, (list, tuple)):
            return value
        else:
            return [value]

    def getfirst(self, key, default=None):
        """
        Returns the first value of a list or the value itself when given a
        `request.vars` style key.

        If the value is a list, its first item will be returned;
        otherwise, the value will be returned as-is.

        Example output for a query string of `?x=abc&y=abc&y=def`::

            >>> request = Storage()
            >>> request.vars = Storage()
            >>> request.vars.x = 'abc'
            >>> request.vars.y = ['abc', 'def']
            >>> request.vars.getfirst('x')
            'abc'
            >>> request.vars.getfirst('y')
            'abc'
            >>> request.vars.getfirst('z')

        """
        values = self.getlist(key)
        return values[0] if values else default

    def getlast(self, key, default=None):
        """
        Returns the last value of a list or value itself when given a
        `request.vars` style key.

        If the value is a list, the last item will be returned;
        otherwise, the value will be returned as-is.

        Simulated output with a query string of `?x=abc&y=abc&y=def`::

            >>> request = Storage()
            >>> request.vars = Storage()
            >>> request.vars.x = 'abc'
            >>> request.vars.y = ['abc', 'def']
            >>> request.vars.getlast('x')
            'abc'
            >>> request.vars.getlast('y')
            'def'
            >>> request.vars.getlast('z')

        """
        values = self.getlist(key)
        return values[-1] if values else default


def pickle_storage(s):
    return Storage, (dict(s),)

copyreg.pickle(Storage, pickle_storage)
if PY2:
    PICKABLE = (str, int, long, float, bool, list, dict, tuple, set)
else:
    PICKABLE = (str, int, float, bool, list, dict, tuple, set)

class StorageList(Storage):
    """
    Behaves like Storage but missing elements defaults to [] instead of None
    """
    def __getitem__(self, key):
        return self.__getattr__(key)

    def __getattr__(self, key):
        if key in self:
            return self.get(key)
        else:
            r = []
            self[key] = r
            return r


def load_storage(filename):
    fp = None
    try:
        fp = portalocker.LockedFile(filename, 'rb')
        storage = pickle.load(fp)
    finally:
        if fp:
            fp.close()
    return Storage(storage)


def save_storage(storage, filename):
    fp = None
    try:
        fp = portalocker.LockedFile(filename, 'wb')
        pickle.dump(dict(storage), fp)
    finally:
        if fp:
            fp.close()


class Settings(Storage):
    def __setattr__(self, key, value):
        if key != 'lock_keys' and self['lock_keys'] and key not in self:
            raise SyntaxError('setting key \'%s\' does not exist' % key)
        if key != 'lock_values' and self['lock_values']:
            raise SyntaxError('setting value cannot be changed: %s' % key)
        self[key] = value


class Messages(Settings):
    def __init__(self, T):
        Storage.__init__(self, T=T)

    def __getattr__(self, key):
        value = self[key]
        if isinstance(value, str):
            return self.T(value)
        return value


class FastStorage(dict):
    """
    Eventually this should replace class Storage but causes memory leak
    because of http://bugs.python.org/issue1469629

        >>> s = FastStorage()
        >>> s.a = 1
        >>> s.a
        1
        >>> s['a']
        1
        >>> s.b
        >>> s['b']
        >>> s['b']=2
        >>> s['b']
        2
        >>> s.b
        2
        >>> isinstance(s,dict)
        True
        >>> dict(s)
        {'a': 1, 'b': 2}
        >>> dict(FastStorage(s))
        {'a': 1, 'b': 2}
        >>> import pickle
        >>> s = pickle.loads(pickle.dumps(s))
        >>> dict(s)
        {'a': 1, 'b': 2}
        >>> del s.b
        >>> del s.a
        >>> s.a
        >>> s.b
        >>> s['a']
        >>> s['b']

    """
    def __init__(self, *args, **kwargs):
        dict.__init__(self, *args, **kwargs)
        self.__dict__ = self

    def __getattr__(self, key):
        return getattr(self, key) if key in self else None

    def __getitem__(self, key):
        return dict.get(self, key, None)

    def copy(self):
        self.__dict__ = {}
        s = FastStorage(self)
        self.__dict__ = self
        return s

    def __repr__(self):
        return '<Storage %s>' % dict.__repr__(self)

    def __getstate__(self):
        return dict(self)

    def __setstate__(self, sdict):
        dict.__init__(self, sdict)
        self.__dict__ = self

    def update(self, *args, **kwargs):
        dict.__init__(self, *args, **kwargs)
        self.__dict__ = self


class List(list):

    """
        Like a regular python list but callable.
        When  a(i) is called if i is out of bounds returns None
        instead of `IndexError`.
    """

    def __call__(self, i, default=DEFAULT, cast=None, otherwise=None):
        """Allows to use a special syntax for fast-check of
        `request.args()` validity.
        :params:
            i: index
            default: use this value if arg not found
            cast: type cast
            otherwise:
                will be executed when:
                    - casts fail
                    - value not found, dont have default and otherwise is
                    especified
                can be:
                    - None: results in a 404
                    - str: redirect to this address
                    - callable: calls the function (nothing is passed)
        Example:
            You can use::
                request.args(0,default=0,cast=int,otherwise='http://error_url')
                request.args(0,default=0,cast=int,otherwise=lambda:...)
        """
        n = len(self)
        if 0 <= i < n or -n <= i < 0:
            value = self[i]
        elif default is DEFAULT:
            value = None
        else:
            value, cast, otherwise = default, False, False
        try:
            if cast:
                value = cast(value)
            if not value and otherwise:
                raise ValueError('Otherwise will raised.')
        except (ValueError, TypeError):
            from gluon.http import HTTP, redirect
            if otherwise is None:
                raise HTTP(404)
            elif isinstance(otherwise, str):
                redirect(otherwise)
            elif callable(otherwise):
                return otherwise()
            else:
                raise RuntimeError("invalid otherwise")
        return value

if __name__ == '__main__':
    import doctest
    doctest.testmod()
