"""In programming, a factory is a function that returns an object.

Functions are easy to understand because they have clear inputs and outputs.
Most gdsfactory functions take some inputs and return a Component object.
Some of these inputs parameters are also functions.

- Component: Object with.
    - name.
    - references: to other components (x, y, rotation).
    - polygons in different layers.
    - ports dict.


Specs:

- ComponentSpec: Component, function, string or dict
    (component=mzi, settings=dict(delta_length=20)).
- LayerSpec: (3, 0), 3 (assumes 0 as datatype) or string.

"""

from __future__ import annotations

import pathlib
from collections.abc import Callable, Generator, Sequence
from enum import IntEnum
from functools import partial
from typing import Any, Literal, ParamSpec, Protocol, TypeAlias, TypedDict, TypeVar

import kfactory as kf
import klayout.db as kdb
import numpy as np
import numpy.typing as npt
from kfactory.layer import LayerEnum

STEP_DIRECTIVES = {
    "x",
    "y",
    "dx",
    "dy",
}

STEP_DIRECTIVES_ALL_ANGLE = {
    "x",
    "y",
    "dx",
    "dy",
    "ds",
    "exit_angle",
    "cross_section",
    "connector",
    "separation",
}


class Step(TypedDict, total=False):
    """Manhattan Step.

    Parameters:
        x: absolute.
        y: absolute.
        dx: x-displacement.
        dy: y-displacement.

    """

    x: float
    y: float
    dx: Delta
    dy: Delta


class PixelBufferOptions(TypedDict, total=False):
    """Options for KLayout's get_pixels_with_options method.

    Parameters:
        width: The width of the image to render in pixels.
        height: The height of the image to render in pixels.
        linewidth: The width of a line in pixels (usually 1) or 0 for default.
        oversampling: The oversampling factor (1..3) or 0 for default.
        resolution: The resolution (pixel size compared to a screen pixel size, i.e 1/oversampling) or 0 for default.
        target_box: The box to draw or an empty box for default (DBox).

    """

    width: int
    height: int
    linewidth: int
    oversampling: int
    resolution: float


Anchor: TypeAlias = Literal[
    "ce",
    "cw",
    "nc",
    "ne",
    "nw",
    "sc",
    "se",
    "sw",
    "center",
    "cc",
]
Axis: TypeAlias = Literal["x", "y"]
NSEW: TypeAlias = Literal["N", "S", "E", "W"]
WidthTypes: TypeAlias = Literal["sine", "linear", "parabolic"]

Float2: TypeAlias = tuple[float, float]
Float3: TypeAlias = tuple[float, float, float]
Floats: TypeAlias = Sequence[float]
Strs: TypeAlias = Sequence[str]
Int2: TypeAlias = tuple[int, int]
Int3: TypeAlias = tuple[int, int, int]
Ints: TypeAlias = tuple[int, ...] | list[int]

BoundingBox: TypeAlias = tuple[float, float, float, float]
BoundingBoxes: TypeAlias = Sequence[BoundingBox]
Size: TypeAlias = tuple[float, float]
Spacing: TypeAlias = tuple[float, float]
Position: TypeAlias = tuple[float, float]
Radius: TypeAlias = float

Delta: TypeAlias = float
AngleInDegrees: TypeAlias = float

Layer: TypeAlias = tuple[int, int]
Layers: TypeAlias = Sequence[Layer]
LayerSpec: TypeAlias = Layer | str | int | LayerEnum
LayerSpecs: TypeAlias = Sequence[LayerSpec]

Number: TypeAlias = float | int
Coordinate: TypeAlias = tuple[float, float]
Coordinates: TypeAlias = Sequence[Coordinate]
WayPoints: TypeAlias = Sequence[Coordinate | kdb.DPoint]

MaterialSpec: TypeAlias = (
    str | float | tuple[float, float] | Callable[..., Any] | npt.NDArray[np.float64]
)

WidthFunction: TypeAlias = Callable[..., npt.NDArray[np.floating[Any]]]
OffsetFunction: TypeAlias = Callable[[float], float]

PathType: TypeAlias = str | pathlib.Path
PathTypes: TypeAlias = Sequence[PathType]
Metadata: TypeAlias = dict[str, int | float | str]

Port: TypeAlias = kf.DPort
TPort = TypeVar("TPort", bound=Port)
IOPorts: TypeAlias = tuple[str, str]
PortFactory: TypeAlias = Callable[..., Port]
PortsFactory: TypeAlias = Callable[..., Sequence[Port]]
PortSymmetries: TypeAlias = dict[str, Sequence[str]]
PortDict: TypeAlias = dict[str, Port]
Ports: TypeAlias = kf.DPorts | Sequence[Port] | kf.DInstancePorts
SelectPorts: TypeAlias = Callable[..., Sequence[Port]]

PortType: TypeAlias = str
PortName: TypeAlias = str

PortTypes: TypeAlias = Sequence[PortType]
PortNames: TypeAlias = Sequence[PortName]

PortsDict: TypeAlias = dict[str, list[Port]]
PortsDictGeneric: TypeAlias = dict[str, list[TPort]]

ConductorConductorName: TypeAlias = tuple[str, str]
ConductorViaConductorName: TypeAlias = tuple[str, str, str] | ConductorConductorName
ConnectivitySpec: TypeAlias = ConductorConductorName | ConductorViaConductorName

Sparameters: TypeAlias = dict[str, npt.NDArray[np.float64]]

Route: TypeAlias = (
    kf.routing.generic.ManhattanRoute | kf.routing.aa.optical.OpticalAllAngleRoute
)
RoutingStrategy: TypeAlias = Callable[..., Sequence[Route]]
RoutingStrategies: TypeAlias = dict[str, RoutingStrategy]


from gdsfactory.cross_section import CrossSectionFactory, CrossSectionSpec  # noqa: E402

MultiCrossSectionAngleSpec: TypeAlias = Sequence[
    tuple[CrossSectionSpec, tuple[int, ...]]
]

from gdsfactory import component  # noqa: E402

AnyComponent: TypeAlias = component.Component | component.ComponentAllAngle
AnyComponentT = TypeVar("AnyComponentT", bound=AnyComponent)
AnyComponentFactory: TypeAlias = Callable[..., AnyComponent]
AnyComponentPostProcess: TypeAlias = (
    Callable[[component.Component], None]
    | Callable[[component.ComponentAllAngle], None]
)

ComponentParams = ParamSpec("ComponentParams")
ComponentFactory: TypeAlias = Callable[..., component.Component]
ComponentAllAngleFactory: TypeAlias = Callable[..., component.ComponentAllAngle]
ComponentBaseFactory: TypeAlias = Callable[..., component.ComponentBase]
ComponentFactoryDict: TypeAlias = dict[str, ComponentFactory]
ComponentFactories: TypeAlias = Sequence[ComponentFactory]

ComponentSpec: TypeAlias = (
    str | ComponentFactory | dict[str, Any] | kf.DKCell | partial[component.Component]
)
ComponentAllAngleSpec: TypeAlias = (
    str | ComponentAllAngleFactory | dict[str, Any] | component.ComponentAllAngle
)
ComponentSpecOrComponent: TypeAlias = ComponentSpec | component.Component
ComponentSpecs: TypeAlias = Sequence[ComponentSpec]
ComponentSpecsOrComponents: TypeAlias = Sequence[ComponentSpecOrComponent]


class _PostProcess(Protocol):
    def __call__(self, component: component.Component, **kwargs: Any) -> Any: ...


PostProcess: TypeAlias = (
    _PostProcess | Callable[[component.Component], None] | partial[component.Component]
)
PostProcesses: TypeAlias = Sequence[PostProcess]

Instance: TypeAlias = component.ComponentReference
InstanceOrVInstance: TypeAlias = component.ComponentReference | kf.VInstance
ComponentOrPath: TypeAlias = PathType | component.Component
ComponentOrReference: TypeAlias = component.Component | component.ComponentReference
NameToFunctionDict: TypeAlias = dict[str, ComponentFactory]


ComponentSpecOrList: TypeAlias = ComponentSpec | list[ComponentSpec]
CellSpec: TypeAlias = (
    str | ComponentFactory | dict[str, Any]  # PCell function, function name or dict
)
CellAllAngleSpec: TypeAlias = str | ComponentAllAngleFactory | dict[str, Any]
ComponentSpecDict: TypeAlias = dict[str, ComponentSpec]

LayerTransitions: TypeAlias = dict[
    LayerSpec | tuple[LayerEnum | Layer, LayerEnum | Layer], ComponentSpec
]


class TypedArray(np.ndarray[Any, np.dtype[Any]]):
    """based on https://github.com/samuelcolvin/pydantic/issues/380."""

    @classmethod
    def __get_validators__(
        cls,
    ) -> Generator[Callable[[Any, Any], npt.NDArray[np.float64]], Any, None]:
        yield cls.validate_type  # pragma: no cover

    @classmethod
    def validate_type(cls, val: Any, _info: Any) -> npt.NDArray[np.float64]:
        return np.array(val, dtype=cls.inner_type)  # type: ignore[attr-defined]


class ArrayMeta(type):
    def __getitem__(cls, t: np.dtype[Any]) -> type[npt.NDArray[Any]]:
        return type("Array", (TypedArray,), {"inner_type": t})  # pragma: no cover


class Array(np.ndarray[Any, np.dtype[Any]], metaclass=ArrayMeta): ...


class CornerMode(IntEnum):
    diamond_limit = 0
    octagon_limit = 1
    square_limit = 2  # The GDSFactory default and klayout default
    acute_limit = 3
    no_limit = 4


__all__ = (
    "AngleInDegrees",
    "ComponentSpec",
    "Coordinate",
    "Coordinates",
    "CornerMode",
    "CrossSectionFactory",
    "CrossSectionSpec",
    "Delta",
    "Float2",
    "Float3",
    "Floats",
    "Instance",
    "Int2",
    "Int3",
    "Ints",
    "Layer",
    "LayerSpec",
    "LayerSpecs",
    "LayerTransitions",
    "Layers",
    "Number",
    "PathType",
    "PathTypes",
    "Port",
    "PortDict",
    "PortName",
    "PortNames",
    "PortType",
    "PortTypes",
    "Ports",
    "PortsDict",
    "PortsDictGeneric",
    "Position",
    "PostProcesses",
    "Radius",
    "RoutingStrategies",
    "SelectPorts",
    "Size",
    "Spacing",
    "Strs",
    "TPort",
    "WayPoints",
    "WidthTypes",
)
