import os
import tempfile
from typing import List, Optional, Type

from tuco import FSM
from tuco.exceptions import TucoEmptyFSMError
from tuco.properties import Error, Event, FinalState, State, Timeout


class GraphBuilder:
    """Parse state machine's states and events to generate graphs."""

    TIMEOUT_ATTRIBUTES = {'color': 'lightgray', 'fontcolor': 'lightgray'}
    FINAL_STATE_ATTRIBUTES = {'shape': 'box', 'fillcolor': 'lightgray', 'style': 'filled'}
    EVENT_ATTRIBUTES = {'color': 'green', 'fontcolor': 'green'}
    ERROR_ATTRIBUTES = {'color': 'red', 'fontcolor': 'red'}

    def __init__(self, fsm_class: Type[FSM], file_format) -> None:
        try:
            from graphviz import Digraph
        except ImportError as e:
            raise RuntimeError('Graphviz could not be found, make sure you installed tuco with '
                               'optional graph support.') from e
        states = fsm_class._states
        if not states:
            raise TucoEmptyFSMError()

        self.fsm_class = fsm_class
        self.states = states.items()
        self.file_format = file_format

        comment = self.fsm_class.__doc__ or ''
        # Replace line endings for multi line comments.
        comment = comment.replace('\n', '\n//')

        self.dot = Digraph(comment=comment, format=file_format, graph_attr={'overlap': 'false'})

    @classmethod
    def generate_from_class(cls, fsm_class: Type[FSM], file_format) -> str:
        """Generate a SVG graph from a tuco fsm class."""
        generation = cls(fsm_class, file_format)
        generation.iterate_states()
        return generation.render_graph()

    def iterate_states(self) -> None:
        """Iterate over all states sent and add nodes to graphviz."""
        for state_name, state in self.states:
            if isinstance(state, FinalState):
                self.add_final_state_node(state_name)
            else:
                self.add_node(state, state_name)

    def iterate_events(self, parent_state_name: str, events: List[Event]) -> None:
        """Iterate over all events of a state and add nodes to graphviz."""
        for event in events:
            self.dot.node(event.target_state)
            self.dot.edge(parent_state_name, event.target_state, label='Event: {}'.format(event.event_name),
                          _attributes=self.EVENT_ATTRIBUTES)

            self.add_error_node(parent_state_name, event.error)

    def add_node(self, state: State, state_name: str) -> None:
        """Add a node with its events and timeouts."""
        self.dot.node(state_name)

        self.add_error_node(state_name, state.error)
        self.add_timeout_node(state_name, state.timeout)

        self.iterate_events(state_name, state.events)

    def add_final_state_node(self, state_name: str) -> None:
        """Add a final node."""
        self.dot.node(state_name, _attributes=self.FINAL_STATE_ATTRIBUTES)

    def add_error_node(self, parent_state_name: str, error: Optional[Error]) -> None:
        """Render an error node if present."""
        if not error:
            return

        target_state = error.target_state
        self.dot.node(target_state)
        self.dot.edge(parent_state_name, target_state, label='Error', _attributes=self.ERROR_ATTRIBUTES)

    def add_timeout_node(self, parent_state_name: str, timeout: Optional[Timeout]) -> None:
        """Render a timeout node if present."""
        if not timeout:
            return
        self.dot.node(timeout.target_state)
        self.dot.edge(parent_state_name, timeout.target_state, label='Timeout', _attributes=self.TIMEOUT_ATTRIBUTES)

    def render_graph(self) -> str:
        """Render graph in a file and return its content."""
        temp_file = tempfile.NamedTemporaryFile(prefix='tuco', delete=False)
        temp_file.close()
        filename = temp_file.name
        graph_file = '{}.{}'.format(filename, self.file_format)

        try:
            self.dot.render(os.path.basename(filename), os.path.dirname(filename))
            return open(graph_file).read()
        finally:
            for name in (filename, graph_file):
                try:
                    os.unlink(name)
                except OSError:
                    pass


generate_from_class = GraphBuilder.generate_from_class
