# -*- coding: utf-8 -*-
# Copyright (c) 2019 Frootlab Developers
#
# This file is part of Pandora, https://frootlab.github.io/pandora
#
#  Pandora is free software: you can redistribute it and/or modify it under the
#  terms of the GNU General Public License as published by the Free Software
#  Foundation, either version 3 of the License, or (at your option) any later
#  version.
#
#  Pandora is distributed in the hope that it will be useful, but WITHOUT ANY
#  WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
#  A PARTICULAR PURPOSE. See the GNU General Public License for more details.
#  You should have received a copy of the GNU General Public License along with
#  Pandora. If not, see <http://www.gnu.org/licenses/>.
#
"""Record Class."""

__author__ = 'Patrick Michl'
__email__ = 'frootlab@gmail.com'
__license__ = 'GPLv3'
__docformat__ = 'google'

import abc
import contextlib
import itertools
import dataclasses
from typing import Any, Union, Tuple, Type
from flib.base import check
from flib.errors import InvalidTypeError
from flib.base.types import StrDict, StrList, void, OptOp, TypeHint

#
# Structural Types
#

Field = dataclasses.Field
FieldTuple = Tuple[Field, ...]
ColDefA = str # Column definition: name
ColDefB = Tuple[str, type] # Column definition: name and type
ColDefC = Tuple[str, type, StrDict] # Column definition: name, type, constraints
ColDefD = Tuple[str, type, Field] # Column definition: name, type, constraints
ColDef = Union[ColDefA, ColDefB, ColDefC, ColDefD] # Column definition

#
# Constants
#

STATE_CREATE = 0b0001
STATE_UPDATE = 0b0010
STATE_DELETE = 0b0100

#
# Record Base Class
#

class Record(abc.ABC):
    """Abstract base class for :mod:`dataclasses` based records.

    Args:
        *args: Arguments, that are valid with respect to the column definitions
            of derived :mod:'dataclasses'.
        **kwds: Keyword arguments, that are valid with respect to the column
            definitions of derived :mod:'dataclasses'.

    """
    __slots__ = ['_id', '_name', '_state']

    _id: int
    _name: str
    _state: int

    @abc.abstractmethod
    def __init__(self, *args: Any) -> None:
        raise NotImplementedError()

    def __post_init__(self, *args: Any, **kwds: Any) -> None:
        self._validate()
        self._id = self._get_newid()
        self._state = STATE_CREATE

    def _validate(self) -> None:
        """Check validity of the field types."""
        fields = getattr(self, '__dataclass_fields__', {})
        for name, field in fields.items():
            if isinstance(field.type, str):
                continue # Do not type check structural types like 'typing.Any'
            value = getattr(self, name)
            check.has_type(f"field '{name}'", value, field.type)

    def _delete(self) -> None:
        """Mark record as deleted and remove it's ID from index."""
        if not self._state & STATE_DELETE:
            self._state |= STATE_DELETE
            self._delete_hook(self._id)

    def _update(self, **kwds: Any) -> None:
        """Mark record as updated and write the update to diff table."""
        if not self._state & STATE_UPDATE:
            self._state |= STATE_UPDATE
            self._update_hook(self._id, **kwds)

    def _restore(self) -> None:
        """Mark record as not deleted and append it's ID to index."""
        if self._state & STATE_DELETE:
            self._state &= ~STATE_DELETE
            self._restore_hook(self._id)

    def _revoke(self) -> None:
        """Mark record as not updated and remove the update from diff table."""
        if self._state & STATE_UPDATE:
            self._state &= ~STATE_UPDATE
            self._revoke_hook(self._id)

    @abc.abstractmethod
    def _get_newid(self) -> int:
        raise NotImplementedError()

    @abc.abstractmethod
    def _delete_hook(self, rowid: int) -> None:
        raise NotImplementedError()

    @abc.abstractmethod
    def _restore_hook(self, rowid: int) -> None:
        raise NotImplementedError()

    @abc.abstractmethod
    def _update_hook(self, rowid: int, **kwds: Any) -> None:
        raise NotImplementedError()

    @abc.abstractmethod
    def _revoke_hook(self, rowid: int) -> None:
        raise NotImplementedError()

#
# Builder, Constructor and helper functions
#

def build(*args: ColDef, newid: OptOp = None, **kwds: Any) -> Type[Record]:
    """Create a new subclass of the Record class.

    Args:
        *args: *column definitions*. All column definitions independent from
            each other can be given in one of the following formats: (1) In
            order to only specify the name of the column, without further
            information, the colum definition has to be given as a string
            `<name>`. Thereby the choice of `<name>` is restricted to valid
            identifiers, described by [UAX31]_. (2) If, additionally to the
            name, also the data type of the column shall be specified, then the
            column definition has to be given as a tuple `(<name>, <type>)`.
            Thereby `<type>` is required to by a valid :class:`type`, like like
            :class:`str`, :class:`int`, :class:`float` or :class:`Date
            <datetime.datetime>`. (3) Finally the column definition may also
            contain supplementary constraints and metadata. In this case the
            definition has to be given as a tuple `(<name>, <type>, <dict>)`,
            where `<dict>` is dictionary which comprises any items, documented
            by the function :func:`dataclasses.fields`.
        newid: Optional reference to a method, which returns the current ID of
            a new instance of the Record class. By default the Record class
            uses an internal Iterator.
        **kwds: Optional references to methods, which are bound to specific
            events of the new Record class. These events are: 'delete',
            'restore', 'update' and 'revoke'. By default no events are hooked.

    Returns:
        New subclass of the Record class.

    """
    # Check column defnitions and convert them to field descriptors, as required
    # by dataclasses.make_dataclass()
    fields: list = []
    names: StrList = []
    for arg in args:
        if isinstance(arg, str):
            fields.append(arg)
            names.append(arg)
            continue
        check.has_type(f'column {arg}', arg, tuple)
        check.has_size(f'column {arg}', arg, min_size=2, max_size=3)
        check.has_type('first argument', arg[0], str)
        check.has_type('second argument', arg[1], TypeHint)
        if len(arg) == 2:
            fields.append(arg)
            names.append(arg[0])
            continue
        check.has_type('third argument', arg[2], (Field, dict))
        if isinstance(arg[2], Field):
            fields.append(arg)
            names.append(arg[0])
            continue
        field = dataclasses.field(**arg[2])
        names.append(arg[0])
        fields.append(arg[:2] + (field,))

    # Dynamically create a dataclass, which is inherited from Record class.
    # Thereby create an ampty '__slots__' attribute to avoid collision with
    # default values (which in dataclasses are stored as class variables),
    # while avoiding the creation of a '__dict__' attribute
    namespace: StrDict = {}
    if newid and callable(newid):
        namespace['_get_newid'] = newid
    else:
        counter = itertools.count() # Infinite iterator
        namespace['_get_newid'] = lambda obj: next(counter)
    hooks = {
        'delete': '_delete_hook', 'restore': '_restore_hook',
        'update': '_update_hook', 'revoke': '_revoke_hook'}
    for key in hooks:
        namespace[hooks[key]] = kwds.get(key, void)
    namespace['__slots__'] = tuple()
    dataclass = dataclasses.make_dataclass(
        Record.__name__, fields, bases=(Record, ), namespace=namespace)

    # Dynamically create a new class, which is inherited from dataclass,
    # with corrected __slots__ attribute.
    return type(dataclass.__name__, (dataclass,), {'__slots__': names})

def create_from(record: Record, **changes: Any) -> Record:
    """Create new record instance, using an existing record."""
    if not is_record(record):
        raise InvalidTypeError('record', record, Record)
    newrec = dataclasses.replace(record, **changes)
    with contextlib.suppress(AttributeError):
        newrec._id = record._id # pylint: disable=W0212
    with contextlib.suppress(AttributeError):
        newrec._state = record._state # pylint: disable=W0212
    with contextlib.suppress(AttributeError):
        newrec._name = record._name # pylint: disable=W0212
    return newrec

def is_record(record: object) -> bool:
    """Check if a given object is a record."""
    if not isinstance(record, Record):
        return False
    if not dataclasses.is_dataclass(record):
        return False
    return True

def get_fields(record: Record) -> FieldTuple:
    """Get field definitions of record."""
    return dataclasses.fields(record)
