from dataclasses import dataclass, field
from datetime import datetime
from enum import Enum
from math import atan2, pi
from typing import NamedTuple


class DiagramKey(Enum):
    TERM_ID = "term_id"
    LABEL = "label"


# TODO: move to config.ini and parse with a function in preprocessing
SHAPE_WIDTH = 200
SHAPE_HEIGHT = 80
FILL_COLOR = "#f2f3f4"
x_padding = 10
y_padding = 20


def get_timestamp_str():
    return f"{datetime.now():%Y-%m-%dT%H:%M:%S.%fZ}"


class DiagramObject:
    pass


@dataclass
class DiagramInfo(DiagramObject):
    diagram_name: int
    diagram_id: int
    modify_date: str = field(default_factory=get_timestamp_str)
    grid_dx: int = 1600
    grid_dy: int = 850
    grid_size: int = 10
    page_width: int = 1100
    page_height: int = 850
    diagram_content: int = None
    template_key: str = "scaffold"


class ConnectorType(Enum):
    RANK_CONNECTOR = "rank"
    PROPERTY_CONNECTOR = "property"


@dataclass
class Connector(DiagramObject):
    connector_id: str
    source_id: str
    target_id: str
    connector_label_id: str
    connector_val: str
    rel_x_pos: float = 0
    rel_y_pos: float = 0
    start_pos_x: float = 0.5
    start_pos_y: float = 1
    end_pos_x: float = 0.5
    end_pos_y: float = 0
    is_dashed: bool = 0
    is_curved: bool = 0
    template_key: str = "connector"

    @staticmethod
    def compute_dynamic_position(
        source_shape_x: float,
        source_shape_y: float,
        target_shape_x: float,
        target_shape_y: float,
    ) -> tuple[float, float, float, float]:
        crit_angle = abs(atan2(SHAPE_HEIGHT, SHAPE_WIDTH))
        angle = atan2(target_shape_y - source_shape_y, target_shape_x - source_shape_x)

        if -crit_angle <= angle <= crit_angle:
            return (1, 0.5, 0, 0.5)
        elif crit_angle < angle <= pi - crit_angle:
            return (0.5, 1, 0.5, 0)
        elif angle > pi - crit_angle or angle < -(pi - crit_angle):
            return (0, 0.5, 1, 0.5)
            # return (1, 0.5, 0, 0.5)
        elif -(pi - crit_angle) <= angle < -crit_angle:
            return (0.5, 0, 0.5, 1)
        else:
            return (0, 0, 0, 0)

    def resolve_position(
        self,
        source_shape_pos: tuple[float, float],
        target_shape_pos: tuple[float, float],
        classes_only: bool = False,
        horizontal_tree: bool = False,
    ) -> None:

        if not classes_only:
            self.start_pos_x, self.start_pos_y, self.end_pos_x, self.end_pos_y = (
                Connector.compute_dynamic_position(*source_shape_pos, *target_shape_pos)
            )

        if horizontal_tree:
            temp = self.start_pos_x
            self.start_pos_x = self.start_pos_y
            self.start_pos_y = temp

            temp = self.end_pos_x
            self.end_pos_x = self.end_pos_y
            self.end_pos_y = temp


@dataclass
class GhostConnector(Connector):

    is_curved: bool = 1
    is_dashed: bool = 1


@dataclass
class Shape:
    shape_id: str
    shape_content: str
    fill_color: str
    x_pos: float
    y_pos: float
    shape_width: int
    shape_height: int
    template_key: str = "shape"


class NxEdge(NamedTuple):
    subj: any
    obj: any
    pred: any


class NxStringEdge(NxEdge):
    subj: str
    obj: str
    pred: str


# Errors


class WrongFileFormatError(Exception):
    def __init__(self, message="Wrong file format provided"):
        self.message = message
        super().__init__(self.message)


class BadDiagramError(Exception):
    def __init__(self, output_file_path):
        self.message = f"The diagram has several syntax issues that needs to be resolved. Please refer to generated file at {output_file_path}. Problematic elements are in red! Refer to earlier output for list of errors."
        super().__init__(self.message)


class DisconnectedTermError(Exception):
    def __init__(self, term_id, term_content):
        if not term_content.strip():
            self.message = f"Term with id {term_id} is not connected to any other term."
        else:
            self.message = f"Term with content: {term_content}, is not connected to any other term."
        super().__init__(self.message)


class DisconnectedEdgeError(Exception):
    def __init__(self, message):
        super().__init__(message)


class MissingParentEdgeError(DisconnectedEdgeError):
    def __init__(self, edge_id, edge_content):
        if not edge_content.strip():
            self.message = (
                f"Edge with id {edge_id} does not have a source (parent) connected."
            )
        else:
            self.message = f"Edge with content: {edge_content}, does not have a source (parent) connected."
        super().__init__(self.message)


class MissingChildEdgeError(DisconnectedEdgeError):
    def __init__(self, edge_id, edge_content):
        if not edge_content.strip():
            self.message = (
                f"Edge with id {edge_id} does not have a target (child) connected."
            )
        else:
            self.message = f"Edge with content: {edge_content}, does not have a target (child) connected."
        super().__init__(self.message)


class FloatingEdgeError(DisconnectedEdgeError):
    def __init__(self, edge_id, edge_content):
        if not edge_content:
            self.message = f"Edge with id {edge_id} does not have anything connected."
        else:
            self.message = (
                f"Edge with content: {edge_content}, does not have anything connected."
            )
        super().__init__(self.message)


class CircularEdgeError(DisconnectedEdgeError):
    def __init__(self, edge_id, edge_content):
        if not edge_content:
            self.message = (
                f"Edge with id {edge_id} is only connected to itself. Please ignore."
            )
        else:
            self.message = f"Edge with content: {edge_content}, is only connected to itself. Please ignore."
        super().__init__(self.message)


class BlankLabelError(Exception):
    def __init__(self, message):
        super().__init__(message)


class BlankTermLabelError(BlankLabelError):
    def __init__(self, term_id):
        self.message = f"Term with id {term_id} does not have a label."
        super().__init__(self.message)


class BlankEdgeLabelError(BlankLabelError):
    def __init__(self, edge_id, connected_terms):
        if not connected_terms:
            self.message = f"Edge with id {edge_id} does not have a label."
        else:
            self.message = f"Edge connected to {' and '.join(connected_terms)}, does not have a label."
        super().__init__(self.message)
