#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Copyright 2022 Jan Sebastian Götte <gerbonara@jaseg.de>
#
# 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.
#

import math
import itertools

from dataclasses import dataclass, replace

from .utils import *

prec = lambda x: f'{float(x):.6}'


@dataclass(frozen=True)
class GraphicPrimitive:

    # hackety hack: Work around python < 3.10 not having dataclasses.KW_ONLY.
    # 
    # For details, refer to graphic_objects.py
    def __init_subclass__(cls):
        cls.polarity_dark = True

        d = {'polarity_dark': bool}
        if hasattr(cls, '__annotations__'):
            cls.__annotations__.update(d)
        else:
            cls.__annotations__ = d

    def bounding_box(self):
        """ Return the axis-aligned bounding box of this feature.

        :returns: ``((min_x, min_Y), (max_x, max_y))``
        :rtype: tuple
        """

        raise NotImplementedError()

    def to_svg(self, fg='black', bg='white', tag=Tag):
        """ Render this primitive into its SVG representation.

        :param str fg: Foreground color. Must be an SVG color name.
        :param str bg: Background color. Must be an SVG color name.
        :param function tag: Tag constructor to use.

        :rtype: str
        """

        raise NotImplementedError()


@dataclass(frozen=True)
class Circle(GraphicPrimitive):
    #: Center X coordinate
    x : float
    #: Center y coordinate
    y : float
    #: Radius, not diameter like in :py:class:`.apertures.CircleAperture`
    r : float # Here, we use radius as common in modern computer graphics, not diameter as gerber uses.

    def bounding_box(self):
        return ((self.x-self.r, self.y-self.r), (self.x+self.r, self.y+self.r))

    def to_svg(self, fg='black', bg='white', tag=Tag):
        color = fg if self.polarity_dark else bg
        return tag('circle', cx=prec(self.x), cy=prec(self.y), r=prec(self.r), fill=color)


@dataclass(frozen=True)
class ArcPoly(GraphicPrimitive):
    """ Polygon whose sides may be either straight lines or circular arcs. """

    #: list of (x : float, y : float) tuples. Describes closed outline, i.e. the first and last point are considered
    #: connected.
    outline : list
    #: Must be either None (all segments are straight lines) or same length as outline.
    #: Straight line segments have None entry.
    arc_centers : list = None

    @property
    def segments(self):
        """ Return an iterator through all *segments* of this polygon. For each outline segment (line or arc), this
        iterator will yield a ``(p1, p2, center)`` tuple. If the segment is a straight line, ``center`` will be
        ``None``.
        """
        ol = self.outline
        return itertools.zip_longest(ol, ol[1:] + [ol[0]], self.arc_centers or [])

    def bounding_box(self):
        bbox = (None, None), (None, None)
        for (x1, y1), (x2, y2), arc in self.segments:
            if arc:
                clockwise, (cx, cy) = arc
                bbox = add_bounds(bbox, arc_bounds(x1, y1, x2, y2, cx, cy, clockwise))

            else:
                line_bounds = (min(x1, x2), min(y1, y2)), (max(x1, x2), max(y1, y2))
                bbox = add_bounds(bbox, line_bounds)
        return bbox

    @classmethod
    def from_regular_polygon(kls, x:float, y:float, r:float, n:int, rotation:float=0, polarity_dark:bool=True):
        """ Convert an n-sided gerber polygon to a normal ArcPoly defined by outline """

        delta = 2*math.pi / n

        return kls([
                (x + math.cos(rotation + i*delta) * r,
                 y + math.sin(rotation + i*delta) * r)
                for i in range(n) ], polarity_dark=polarity_dark)

    def __len__(self):
        """ Return the number of points on this polygon's outline (which is also the number of segments because the
        polygon is closed). """
        return len(self.outline)

    def __bool__(self):
        """ Return ``True`` if this polygon has any outline points. """
        return bool(len(self))

    def path_d(self):
        if len(self.outline) == 0:
            return

        yield f'M {float(self.outline[0][0]):.6} {float(self.outline[0][1]):.6}'

        for old, new, arc in self.segments:
            if not arc:
                yield f'L {float(new[0]):.6} {float(new[1]):.6}'
            else:
                clockwise, center = arc
                yield svg_arc(old, new, center, clockwise)

    def to_svg(self, fg='black', bg='white', tag=Tag):
        color = fg if self.polarity_dark else bg
        return tag('path', d=' '.join(self.path_d()), fill=color)


@dataclass(frozen=True)
class Line(GraphicPrimitive):
    """ Straight line with round end caps. """
    #: Start X coordinate. As usual in modern graphics APIs, this is at the center of the half-circle capping off this
    #: line.
    x1 : float
    #: Start Y coordinate
    y1 : float
    #: End X coordinate
    x2 : float
    #: End Y coordinate
    y2 : float
    #: Line width
    width : float

    def flip(self):
        return replace(self, x1=self.x2, y1=self.y2, x2=self.x1, y2=self.y1)

    @classmethod
    def from_obround(kls, x:float, y:float, w:float, h:float, rotation:float=0, polarity_dark:bool=True):
        """ Convert a gerber obround into a :py:class:`~.graphic_primitives.Line`. """
        if w > h:
            w, a, b = h, w-h, 0
        else:
            w, a, b = w, 0, h-w

        return kls(
                *rotate_point(x-a/2, y-b/2, rotation, x, y),
                *rotate_point(x+a/2, y+b/2, rotation, x, y),
                w, polarity_dark=polarity_dark)

    def bounding_box(self):
        r = self.width / 2
        return add_bounds(Circle(self.x1, self.y1, r).bounding_box(), Circle(self.x2, self.y2, r).bounding_box())

    def to_svg(self, fg='black', bg='white', tag=Tag):
        color = fg if self.polarity_dark else bg
        width = f'{self.width:.6}' if not math.isclose(self.width, 0) else '0.01mm'
        return tag('path', d=f'M {float(self.x1):.6} {float(self.y1):.6} L {float(self.x2):.6} {float(self.y2):.6}',
                fill='none', stroke=color, stroke_width=str(width))

@dataclass(frozen=True)
class Arc(GraphicPrimitive):
    """ Circular arc with line width ``width`` going from ``(x1, y1)`` to ``(x2, y2)`` around center at ``(cx, cy)``. """
    #: Start X coodinate
    x1 : float
    #: Start Y coodinate
    y1 : float
    #: End X coodinate
    x2 : float
    #: End Y coodinate
    y2 : float
    #: Center X coordinate relative to ``x1``
    cx : float
    #: Center Y coordinate relative to ``y1``
    cy : float
    #: ``True`` if this arc is clockwise from start to end. Selects between the large arc and the small arc given this
    #: start, end and center
    clockwise : bool
    #: Line width of this arc.
    width : float

    @property
    def is_circle(self):
        return math.isclose(self.x1, self.x2) and math.isclose(self.y1, self.y2)

    def flip(self):
        return replace(self, x1=self.x2, y1=self.y2, x2=self.x1, y2=self.y1,
            cx=(self.x1 + self.cx) - self.x2, cy=(self.y1 + self.cy) - self.y2, clockwise=not self.clockwise)

    def bounding_box(self):
        r = self.width/2
        (min_x, min_y), (max_x, max_y) = arc_bounds(self.x1, self.y1, self.x2, self.y2, self.cx, self.cy, self.clockwise)
        return (min_x-r, min_y-r), (max_x+r, max_y+r)

    def to_svg(self, fg='black', bg='white', tag=Tag):
        color = fg if self.polarity_dark else bg
        arc = svg_arc((self.x1, self.y1), (self.x2, self.y2), (self.cx, self.cy), self.clockwise)
        width = f'{self.width:.6}' if not math.isclose(self.width, 0) else '0.01mm'
        return tag('path', d=f'M {float(self.x1):.6} {float(self.y1):.6} {arc}',
                fill='none', stroke=color, stroke_width=width)

@dataclass(frozen=True)
class Rectangle(GraphicPrimitive):
    #: **Center** X coordinate
    x : float
    #: **Center** Y coordinate
    y : float
    #: width
    w : float
    #: height
    h : float
    #: rotation around center in radians
    rotation : float

    def bounding_box(self):
        return self.to_arc_poly().bounding_box()

    def to_arc_poly(self):
        sin, cos = math.sin(self.rotation), math.cos(self.rotation)
        sw, cw = sin*self.w/2, cos*self.w/2
        sh, ch = sin*self.h/2, cos*self.h/2
        x, y = self.x, self.y
        return ArcPoly([
            (x - (cw+sh), y - (ch+sw)),
            (x - (cw+sh), y + (ch+sw)),
            (x + (cw+sh), y + (ch+sw)),
            (x + (cw+sh), y - (ch+sw)),
            ])

    def to_svg(self, fg='black', bg='white', tag=Tag):
        color = fg if self.polarity_dark else bg
        x, y = self.x - self.w/2, self.y - self.h/2
        return tag('rect', x=prec(x), y=prec(y), width=prec(self.w), height=prec(self.h),
                **svg_rotation(self.rotation, self.x, self.y), fill=color)