# -*- coding: utf-8 -*-
#
# Copyright 2017 Simone Campagna
#
# 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.
#

"""
Place/Transition classes.
"""

import abc
import collections
import itertools

from .arc import (
    InputArc, OutputArc
)
from .entity import Entity
from .expression import (
    Condition, TRUE, merge_conditions, parse_condition
)
from .odict import ODict, Binding


__all__ = [
    'Node',
    'Place',
    'Transition',
]


class Node(Entity):
    """Base node class.

       Attributes
       ----------
       name: str
           The node name.
    """

    def __init__(self, name):
        super().__init__(name=name)
        self._input_arcs = ODict()
        self._output_arcs = ODict()
        self._fusion_nodes = {self}

    def input_arcs(self):
        """Yield input arcs.

           Yields
           ------
           InputArc
               An input arc.
        """
        yield from self._input_arcs.values()

    def output_arcs(self):
        """Yield output arcs.

           Yields
           ------
           OutputArc
               An output arc.
        """
        yield from self._output_arcs.values()

    def connect_input(self, arc):
        """Connects to an input arc.

           Parameters
           ----------
           arc: InputArc
               The input arc.
        """
        if arc.name in self._input_arcs:
            raise ValueError("{} {}: input arc {} already defined".format(
                type(self).__name__, self.name, arc.name))
        self._check_input_arc_type(arc)
        self._check_arc_node(arc)
        self._input_arcs[arc.name] = arc

    def connect_output(self, arc):
        """Connects to an output arc.

           Parameters
           ----------
           arc: OutputArc
               The output arc.
        """
        if arc.name in self._output_arcs:
            raise ValueError("{} {}: input arc {} already defined".format(
                type(self).__name__, self.name, arc.name))
        self._check_output_arc_type(arc)
        self._check_arc_node(arc)
        self._output_arcs[arc.name] = arc

    def fusion_nodes(self):
        """Yields fusion nodes.

           Yields
           ------
           Node
               A node fused with this.
        """
        yield from self._fusion_nodes

    @abc.abstractmethod
    def _check_input_arc_type(self, arc):
        """Checks if the input arc has the correct type

           Parameters
           ----------
           arc: Arc
               The input arc.
        """
        raise NotImplementedError()

    @abc.abstractmethod
    def _check_output_arc_type(self, arc):
        """Checks if the output arc has the correct type

           Parameters
           ----------
           arc: Arc
               The output arc.
        """
        raise NotImplementedError()

    @abc.abstractmethod
    def _check_arc_node(self, arc):
        """Checks if the arc refers to this node.

           Parameters
           ----------
           arc: Arc
               The arc.
        """
        raise NotImplementedError()

    def fuse(self, node):
        """Node fusion.

           node: Node
               A node (same class).
        """
        if node not in self._fusion_nodes:
            if not isinstance(node, type(self)):
                raise TypeError("{} {}: cannot fuse with {} {}".format(
                    type(self).__name__, self.name, type(node).__name__, node.name))
            self._fusion_nodes.update(node.fusion_nodes())
            node._fusion_nodes = self._fusion_nodes  # pylint: disable=protected-access
            for arc in node.input_arcs():
                self.connect_input(arc)
            node._input_arcs = self._input_arcs  # pylint: disable=protected-access
            for arc in node.output_arcs():
                self.connect_output(arc)
            node._output_arcs = self._output_arcs  # pylint: disable=protected-access
            self._fuse_hook(node)

    @abc.abstractmethod
    def _fuse_hook(self, node):
        """Node fusion hook.

           node: Node
               A node (same class).
        """
        raise NotImplementedError()


class Place(Node, collections.Sequence):
    # pylint: disable=too-many-ancestors
    """Place class.

       Attributes
       ----------
       name: str
           The node name.
       tokens: iterable, optional
           Initialization tokens.
    """
    def __init__(self, name, *, tokens=()):
        super().__init__(name=name)
        self._tokens = list(tokens)

    def _fuse_hook(self, node):
        self._tokens.extend(node)
        node._tokens = self._tokens  # pylint: disable=protected-access

    def __iter__(self):
        yield from self._tokens

    def __len__(self):
        return len(self._tokens)

    def __getitem__(self, index):
        return self._tokens[index]

    def push(self, token):
        """Pushes a token.

           Parameters
           ----------
           token: object
               The token.
        """
        self._tokens.append(token)

    def pop(self):
        """Pops a token.

           Returns
           -------
           object
               A token.
        """
        return self._tokens.pop(0)

    def del_tokens(self, tokens):
        """Removes tokens.

           Parameters
           ----------
           tokens: iterable
               The tokens to be removed.
        """
        saved = self._tokens[:]
        try:
            for token in tokens:
                self._tokens.remove(token)
        except:
            self._tokens = saved
            raise

    def set_tokens(self, tokens):
        """Sets tokens.

           Parameters
           ----------
           tokens: iterable
               The tokens.
        """
        self._tokens = list(tokens)

    def _check_input_arc_type(self, arc):
        if not isinstance(arc, OutputArc):
            raise ValueError("{} {}: {!r} is not an OutputArc".format(
                type(self).__name__, self.name, arc))

    def _check_output_arc_type(self, arc):
        if not isinstance(arc, InputArc):
            raise ValueError("{} {}: {!r} is not an InputArc".format(
                type(self).__name__, self.name, arc))

    def _check_arc_node(self, arc):
        if arc.place not in self._fusion_nodes:
            raise ValueError("{} {}: {!r} is bound to a different place".format(
                type(self).__name__, self.name, arc))

    def __repr__(self):
        return "{}({!r}, tokens={!r})".format(
            type(self).__name__,
            self.name,
            self._tokens)


class Transition(Node):
    """Transition class

       Attributes
       ----------
       name: str
           The node name.
       condition: str, optional
           The transition firing condition (defaults to True).
    """
    def __init__(self, name, *, condition=TRUE):
        super().__init__(name=name)
        self._condition = condition

    def _fuse_hook(self, node):
        self._condition = merge_conditions(self._condition, node.condition)
        node._condition = self._condition  # pylint: disable=protected-access

    @property
    def condition(self):
        """Gets the condition attribute"""
        return self._condition

    @condition.setter
    def condition(self, condition):
        """Sets the condition attribute"""
        if isinstance(condition, str):
            condition = parse_condition(condition, free_variables=self.input_variables())
        elif not isinstance(condition, Condition):
            raise TypeError("{!r} is not a valid condition".format(condition))
        self._condition = condition

    def _check_input_arc_type(self, arc):
        if not isinstance(arc, InputArc):
            raise ValueError("{} {}: {!r} is not an InputArc".format(
                type(self).__name__, self.name, arc))

    def _check_output_arc_type(self, arc):
        if not isinstance(arc, OutputArc):
            raise ValueError("{} {}: {!r} is not an OutputArc".format(
                type(self).__name__, self.name, arc))

    def _check_arc_node(self, arc):
        if arc.transition not in self._fusion_nodes:
            raise ValueError("{} {}: {!r} is bound to a different transition".format(
                type(self).__name__, self.name, arc))

    def input_variables(self):
        """Yields input variables.

           Yields
           ------
           str
               Input variable.
        """
        for arc in self.input_arcs():
            yield from arc.variables()

    def fire(self, binding):
        """Fire transition.

           Parameters
           ----------
           binding: Binding
               A valid binding.
        """
        for input_arc in self.input_arcs():
            input_arc.fire(binding)
        for output_arc in self.output_arcs():
            output_arc.fire(binding)

    def __repr__(self):
        return "{}({!r}, condition={!r})".format(
            type(self).__name__,
            self.name,
            self._condition)

    def enabled_bindings(self):
        """Yields enabled bindings.

           Yields
           ------
           Binding
               Enabled binding.
        """
        partial_binding_iterators = [input_arc.iter_bindings() for input_arc in self.input_arcs()]
        for partial_bindings in itertools.product(*partial_binding_iterators):
            binding = Binding()
            for partial_binding in partial_bindings:
                binding.update(partial_binding)
            if self.condition(binding):
                yield binding

