# Copyright 2011-2014 Biomedical Imaging Group Rotterdam, Departments of
# Medical Informatics and Radiology, Erasmus MC, Rotterdam, The Netherlands
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.


from abc import ABCMeta, abstractmethod
from typing import Tuple, TypeVar

from sympy.core.symbol import Symbol

from .. import exceptions

SizeType = TypeVar('SizeType', int, Symbol)


class Dimension(object):
    """
    A class representing a dimension. It contains the name and size of the
    dimension.
    """
    __slots__ = ('_name', '_size')

    def __init__(self, name: str, size: SizeType):
        """
        The constructor for the dimension.

        :param str name: Name of the dimension
        :param size: Size fo the dimension
        :type size: int or Symbol
        """
        if not isinstance(name, str):
            raise exceptions.FastrTypeError("Dimension.name should be a str, "
                                            "found [{}] {}".format(type(name).__name__,
                                                                   size))

        self._name = name
        self._size = None
        self.size = size

    def __repr__(self) -> str:
        """
        String representation of a Dimension
        """
        return "<Dimension {d.name} ({d.size})>".format(d=self)

    def __eq__(self, other: 'Dimension') -> bool:
        """
        Dimension is the same if the name and size are the same
        """
        return self.name == other.name and self.size == other.size

    def __ne__(self, other: 'Dimension') -> bool:
        """
        The not equal test is simply the inverse of the equal test
        """
        return not self == other

    @property
    def name(self) -> str:
        return self._name

    @property
    def size(self) -> SizeType:
        return self._size

    @size.setter
    def size(self, value: SizeType):
        if not isinstance(value, (int, Symbol)):
            raise exceptions.FastrTypeError("Dimension.size should be an int or"
                                            " sympy.Symbol, found [{}] {}".format(type(value).__name__,
                                                                                  value))

        if isinstance(value, int) and value < 0:
            raise exceptions.FastrValueError("Dimension.size should be positive")

        self._size = value

    def update_size(self, value: SizeType):
        if self._size == value:
            # No effect
            return

        if not isinstance(value, int):
            raise exceptions.FastrTypeError("Dimension.size can only be updated by an int "
                                            " found [{}] {}".format(type(value).__name__,
                                                                    value))

        if value < 0:
            raise exceptions.FastrValueError("Dimension.size should be positive")

        if isinstance(self._size, Symbol):
            self._size = value
        else:
            self._size = max(self._size, value)

    def copy(self) -> 'Dimension':
        """
        Get a copy object of a Dimension
        """
        return Dimension(self.name, self.size)


class HasDimensions(object, metaclass=ABCMeta):
    """
    A Mixin class for any object that has a notion of dimensions and size. It
    uses the dimension property to expose the dimension name and size.
    """

    @property
    @abstractmethod
    def dimensions(self) -> Tuple['Dimension', ...]:
        """
        The dimensions has to be implemented by any subclass. It has to provide
        a tuple of Dimensions.

        :return: dimensions
        :rtype: tuple
        """

    @property
    def dimnames(self) -> Tuple[str]:
        """
        A tuple containing the dimension names of this object. All items of the
        tuple are of type str.
        """
        return tuple(x.name for x in self.dimensions)

    @property
    def size(self) -> Tuple[SizeType]:
        """
        A tuple containing the size of this object. All items of the
        tuple are of type int or Symbol.
        """
        return tuple(x.size for x in self.dimensions)

    @property
    def ndims(self) -> int:
        """
        The number of dimensions in this object
        """
        return len(self.dimensions)


class ForwardsDimensions(HasDimensions):
    """
    Class of objects that have dimensions not because they contain data with
    dimensions but forward them (optionally with changes via combine_dimensions)
    """
    @property
    @abstractmethod
    def source(self) -> HasDimensions:
        """
        The source object from which the dimensions are forwarded

        :return: the object from which the dimensions are forwarded
        :rtype: HasDimensions
        """

    @abstractmethod
    def combine_dimensions(self, dimensions) -> Tuple[Dimension, ...]:
        """
        Method to combine/manipulate the dimensions

        :param dimensions: the input dimensions from the source
        :return: dimensions manipulated for this object
        :rtype: tuple of dimensions
        """

    @property
    def dimensions(self) -> Tuple[Dimension, ...]:
        """
        The dimensions of the object based on the forwarding
        """
        return self.combine_dimensions(self.source.dimensions)
