# ------------------------------------------------------------------------------
#
#  Copyright (c) 2008, Enthought, Inc.
#  All rights reserved.
#
#  This software is provided without warranty under the terms of the BSD
#  license included in enthought/LICENSE.txt and may be redistributed only
#  under the conditions described in the aforementioned license.  The license
#  is also available online at http://www.enthought.com/licenses/BSD.txt
#
#  Thanks for using Enthought open source!
#
#  Author:        David C. Morrill
#  Original Date: 04/01/2008
#
# ------------------------------------------------------------------------------

""" Defines the TraitValue class, used for creating special, dynamic trait
    values.
"""

# -------------------------------------------------------------------------------
#  Imports:
# -------------------------------------------------------------------------------

from __future__ import absolute_import

from .trait_base import Undefined
from .traits import CTrait
from .has_traits import HasTraits, HasPrivateTraits
from .trait_errors import TraitError
from .trait_types import Tuple, Dict, Any, Str, Instance, Event, Callable
from .trait_handlers import TraitType, _read_only, _write_only, _arg_count

# -------------------------------------------------------------------------------
#  'BaseTraitValue' class:
# -------------------------------------------------------------------------------


class BaseTraitValue(HasPrivateTraits):

    # Subclasses can define this trait as a property:
    # value = Property

    # -- Public Methods ---------------------------------------------------------

    def as_ctrait(self, original_trait):
        """ Returns the low-level C-based trait for this TraitValue.
        """
        notifiers = original_trait._notifiers(0)

        if self._ctrait is not None:
            if (notifiers is None) or (len(notifiers) == 0):
                return self._ctrait

            trait = CTrait(0)
            trait.clone(self._ctrait)
        else:
            trait = self._as_ctrait(original_trait)

        if (
            (trait is not None)
            and (notifiers is not None)
            and (len(notifiers) > 0)
        ):
            trait._notifiers(1).extend(notifiers)

        return trait

    # -- Private Methods --------------------------------------------------------

    def _as_ctrait(self, original_trait):
        """ Returns the low-level C-based trait for this TraitValue.
        """
        value_trait = self.trait("value")
        if value_trait is None:
            return None

        if value_trait.type != "property":
            raise TraitError("Invalid TraitValue specified.")

        metadata = {"type": "property", "_trait_value": self}

        getter, setter, validate = value_trait.property()
        read_only = getter is _read_only
        if not read_only:
            getter = self._getter
            metadata["transient"] = True

        if setter is not _write_only:
            if read_only:
                setter = self._read_only_setter
            else:
                setter = self._setter
            metadata["transient"] = True

        return self._property_trait(getter, setter, validate, metadata)

    def _property_trait(self, getter, setter, validate, metadata):
        """ Returns a properly constructed 'property' trait.
        """
        n = 0
        if validate is not None:
            n = _arg_count(validate)

        trait = CTrait(4)
        trait.property(
            getter, _arg_count(getter), setter, _arg_count(setter), validate, n
        )

        trait.value_allowed(True)
        trait.value_property(True)
        trait.__dict__ = metadata

        return trait

    def _getter(self, object, name):
        return self.value

    def _setter(self, object, name, value):
        old_value = self.value
        self.value = value
        new_value = self.value
        if new_value != old_value:
            object.trait_property_changed(name, old_value, new_value)

    def _read_only_setter(self, object, name, value):
        self.value = value
        object.trait_property_changed(name, Undefined, value)


# -------------------------------------------------------------------------------
#  'TraitValue' class:
# -------------------------------------------------------------------------------


class TraitValue(BaseTraitValue):

    #: The callable used to define a default value:
    default = Callable

    #: The positional arguments to pass to the callable default value:
    args = Tuple

    #: The keyword arguments to pass to the callable default value:
    kw = Dict

    #: The trait to use as the new trait type:
    type = Any

    #: The object to delegate the new value to:
    delegate = Instance(HasTraits)

    #: The name of the trait on the delegate object to get the new value from:
    name = Str

    # -- Private Methods --------------------------------------------------------

    def _as_ctrait(self, original_trait):
        """ Returns the low-level C-based trait for this TraitValue.
        """
        if self.default is not None:
            trait = CTrait(0)
            trait.clone(original_trait)
            if original_trait.__dict__ is not None:
                trait.__dict__ = original_trait.__dict__.copy()

            trait.default_value(7, (self.default, self.args, self.kw))

        elif self.type is not None:
            type = self.type
            try:
                rc = issubclass(type, TraitType)
            except:
                rc = False

            if rc:
                type = type(*self.args, **self.kw)

            if not isinstance(type, TraitType):
                raise TraitError(
                    (
                        "The 'type' attribute of a TraitValue "
                        "instance must be a TraitType instance or subclass, but a "
                        "value of %s was specified."
                    )
                    % self.trait
                )

            self._ctrait = trait = type.as_ctrait()
            trait.value_allowed(True)

        elif self.delegate is None:
            return None

        else:
            if self.name == "":
                raise TraitError(
                    "You must specify a non-empty string "
                    "value for the 'name' attribute when using the "
                    "'delegate' trait of a TraitValue instance."
                )

            metadata = {
                "type": "property",
                "_trait_value": self,
                "transient": True,
            }

            getter = self._delegate_getter
            setter = self._delegate_setter
            validate = None

            self.add_trait("value", Event())
            self.delegate.on_trait_change(self._delegate_modified, self.name)

            trait = self._property_trait(getter, setter, validate, metadata)

        return trait

    def _delegate_getter(self, object, name):
        return getattr(self.delegate, self.name)

    def _delegate_setter(self, object, name, value):
        setattr(self.delegate, self.name, value)

    # -- Traits Event Handlers --------------------------------------------------

    def _delegate_modified(self):
        self.value = True


# -- Helper Function Definitions ------------------------------------------------


def SyncValue(delegate, name):
    return TraitValue(delegate=delegate, name=name)


def TypeValue(type):
    return TraitValue(type=type)


def DefaultValue(default, args=(), kw={}):
    return TraitValue(default=default, args=args, kw=kw)


# -------------------------------------------------------------------------------
#  Tell the C-based traits module about the 'BaseTraitValue' class:
# -------------------------------------------------------------------------------

from . import ctraits

ctraits._value_class(BaseTraitValue)
