from typing import Union, List
import pathlib
import math

import numpy as np

import ezdxf
import ezdxf.entities

from fibomat.shapes import Shape, Polyline, Polygon, Line, Spot, Circle, Ellipse, Rect
# from fibomat.utils.math import norm


def _is_rect(vertices):
    if len(vertices) == 4:
        # check if the diagonals are equal
        if math.isclose(norm(vertices[0]-vertices[2]),
                        norm(vertices[1]-vertices[3])):
            return True
    return False


def _rect_from_vertices(vertices):

    # find the vertex with the minimum y value to get a base site of the rect.
    i_min = np.where(vertices[:, 1] == vertices[:, 1].min())[0]

    if len(i_min) == 1:
        # we found one, now check, if the left or right vertex will result in a
        # smaller rotation of the base site compared to x axis.
        base_1 = vertices[(i_min[0]+1) % 4] - vertices[i_min[0]]
        base_2 = vertices[i_min[0]] - vertices[(i_min[0]-1) % 4]

        if np.abs(np.arctan2(base_1[1], base_1[0])) < np.abs(np.arctan2(base_2[1], base_2[0])):
            base_start_vertex_i = i_min[0]
            base_end_vertex_i = (i_min[0]+1) % 4

            theta = np.arctan2(base_1[1], base_1[0])
        else:
            base_start_vertex_i = (i_min[0]-1) % 4
            base_end_vertex_i = i_min[0]

            theta = np.arctan2(base_2[1], base_2[0])

    elif len(i_min) == 2:
        # we got two, the base site is parallel to x axis
        base_start_vertex_i = i_min[0]
        base_end_vertex_i = i_min[1]

        if vertices[base_start_vertex_i, 0] > vertices[base_end_vertex_i, 0]:
            base_start_vertex_i, base_end_vertex_i = base_end_vertex_i, base_start_vertex_i

        base = vertices[base_end_vertex_i] - vertices[base_start_vertex_i]
        theta = np.arctan2(base[1], base[0])
    else:
        raise RuntimeError('Something went horribly wrong.')

    width = norm(vertices[base_start_vertex_i]-vertices[base_end_vertex_i])
    height = norm(vertices[base_end_vertex_i]-vertices[(base_end_vertex_i+1) % 4])

    center = np.mean(vertices, axis=0)

    return Rect(width=width, height=height, theta=theta, center=center)


# TODO: let vertices_raw be a generator object
def _polyline(ocs, vertices_raw, closed):
    vertices = np.empty(shape=(len(vertices_raw), 2))
    for i, vertex in enumerate(vertices_raw):
        vertices[i] = ocs.to_wcs(vertex)[:2]

    if closed:
        if _is_rect(vertices):
            return _rect_from_vertices(vertices)
        return Polygon(vertices)

    # it could be possible that start and end are equal but it is not marked as
    # closed.
    if np.allclose(vertices[0], vertices[-1]):
        cropped_vertices = vertices[:-1]
        if _is_rect(cropped_vertices):
            return _rect_from_vertices(cropped_vertices)
        return Polygon(cropped_vertices)

    return Polyline(vertices)


def _ellipse(major, minor, center):
    major = np.array(major)
    minor = np.array(minor)

    angle_major = np.abs(np.arctan2(major[1], major[0]))

    if angle_major > np.pi/2:
        major *= -1.
        angle_major -= np.pi

    angle_minor = np.abs(np.arctan2(minor[1], minor[0]))
    if angle_minor > np.pi / 2:
        angle_minor *= -1.
        angle_major -= np.pi

    # if angle_major > np.pi/2:
    #     angle_major -= np.pi / 2
    #
    # if angle_minor > np.pi / 2:
    #     angle_minor -= np.pi / 2

    print(angle_major, angle_minor)

    if angle_major <= angle_minor:
        a = norm(major)
        b = norm(minor)

        theta = np.arctan2(major[1], major[0])

    else:
        a = norm(minor)
        b = norm(major)

        theta = np.arctan2(minor[1], minor[0])

    if theta > np.pi / 2:
        theta -= np.pi
    elif theta < -np.pi / 2:
        theta += np.pi

    return Ellipse(a, b, theta, center)


def shapes_from_dxf(file_path: pathlib.Path) -> List[Shape]:
    """
    Reads a dxf file an convert the contained entities (shapes) to fibomat shapes.

    .. note:: z components of all entities are ignored.

    Args:
        file_path (pathlib.Path): path to file

    Returns:
        List[Shape]
    """
    doc = ezdxf.readfile(str(file_path))

    shapes = []

    # we try to get all objects from modelspace
    for obj in doc.modelspace():
        ocs = obj.ocs()
        dxftype = obj.dxftype()

        if dxftype == 'POINT':
            spot: ezdxf.entities.Point = obj
            shapes.append(Spot(spot.dxf.location[:2]))
        elif dxftype == 'LINE':
            line: ezdxf.entities.Line = obj
            shapes.append(Line(line.dxf.start[:2], line.dxf.end[:2]))
        elif dxftype == 'POLYLINE':
            poly: ezdxf.entities.Polyline = obj

            # maybe these to clauses are equivalent
            if not poly.is_2d_polyline or not poly.get_mode() == 'AcDb2dPolyline':
                raise RuntimeError('Polyline is not 2d.')

            if poly.has_arc:
                raise RuntimeError('Arcs are not supported in polyline.')

            vertices = list(poly.points())
            shapes.append(_polyline(ocs, vertices, poly.is_closed))
        elif dxftype == 'LWPOLYLINE':
            poly: ezdxf.entities.LWPolyline = obj

            if poly.has_arc:
                raise RuntimeError('Arcs are not supported in polyline.')

            vertices = list(poly.vertices())
            shapes.append(_polyline(ocs, vertices, poly.closed))
        elif dxftype == 'CIRCLE':
            circle: ezdxf.entities.Circle = obj
            shapes.append(
                Circle(
                    r=circle.dxf.radius,
                    center=ocs.to_wcs(circle.dxf.center)[:2])
            )
        elif dxftype == 'ELLIPSE':
            ellipse: ezdxf.entities.Ellipse = obj
            if not math.isclose(ellipse.dxf.start_param, 0.) or not math.isclose(ellipse.dxf.end_param, 2.* np.pi):
                raise RuntimeError("Elliptical arcs are not supported.")

            shapes.append(_ellipse(ellipse.dxf.major_axis[:2], ellipse.minor_axis[:2], ellipse.dxf.center[:2]))
        else:
            raise RuntimeError(
                f'Encountered (currently) unsupported shape {obj.dxftype()}'
            )

    return shapes
