
from typing import Union, Tuple, Sequence, List
from functools import singledispatch
import numbers

import sys
fo = sys.stdout

import numpy as np

from pyproj import Transformer, CRS

from pyproj.transformer import TransformerGroup, AreaOfInterest

from ..shape import *
from ..points import *
from ..lines import *
from ..polygons import *


def project_xy(
    x,
    y,
    src_crs: Union[numbers.Integral, CRS],
    dest_crs: Union[numbers.Integral, CRS],
) -> Union[None, Tuple[numbers.Real, numbers.Real]]:

    try:

        transformer = Transformer.from_crs(
            src_crs,
            dest_crs,
            always_xy=True,
        )
        return transformer.transform(x, y)

    except Exception as e:

        fo.write(f"\n{e!r}")
        return None


def project_xy_to_geographic(
    x,
    y,
    src_crs: Union[numbers.Integral, CRS],
) -> Union[None, Tuple[numbers.Real, numbers.Real]]:

    try:

        transformer = Transformer.from_crs(src_crs, "EPSG:4326", always_xy=True)
        return transformer.transform(x, y)

    except Exception as e:

        fo.write(f"\n{e!r}")
        return None


def get_transformers(
    source_crs: Union[numbers.Integral, CRS],
    dest_crs: Union[numbers.Integral, CRS],
    area_of_interest: Tuple[ numbers.Real, numbers.Real, numbers.Real, numbers.Real,]
) -> Union[None, List[Transformer]]:


    # Crea un TransformerGroup con area_of_interest

    tg = TransformerGroup(
        source_crs,
        dest_crs,
        area_of_interest=AreaOfInterest(*area_of_interest)
    )

    return tg.transformers


@singledispatch
def project(
    shape: Union[Shape, Sequence[Shape]],
    source_crs: Union[numbers.Integral, CRS],
    dest_crs: Union[numbers.Integral, CRS],
) -> Union[None, Shape]:
    """

    :param obj:
    :param source_crs:
    :param dest_crs:
    :return:
    """

    return NotImplemented


@project.register(Point)
def _(
    point: Point,
    source_crs: Union[numbers.Integral, CRS],
    dest_crs: Union[numbers.Integral, CRS],
) -> Union[None, Point]:

    try:

        xy = project_xy(
            point.x,
            point.y,
            src_crs=source_crs,
            dest_crs=dest_crs,)

        if xy is None:
            return None

        x, y = xy
        proj_coords = np.copy(point.coords)
        proj_coords[:2] = x, y
        return Point(proj_coords)

    except Exception as e:

        fo.write(f"\n{e!r}")
        return None


@project.register(Segment)
def _(
    segment: Segment,
    source_crs: Union[numbers.Integral, CRS],
    dest_crs: Union[numbers.Integral, CRS],
) -> Union[None, Segment]:

    try:

        start_point = project(
            segment.start_pt,
            source_crs,
            dest_crs,
        )

        end_point = project(
            segment.end_pt,
            source_crs,
            dest_crs,
        )

        if start_point is None or end_point is None:
            return None

        return Segment(start_point, end_point)

    except Exception as e:

        fo.write(f"\n{e!r}")

        return None


@project.register(Ln)
def _(
    line: Ln,
    source_crs: Union[numbers.Integral, CRS],
    dest_crs: Union[numbers.Integral, CRS],
) -> Union[None, Ln]:

    try:

        projected_points = []
        for point in line.pts():
            proj_point = project(
                point,
                source_crs,
                dest_crs
            )
            if proj_point is None:
                return None
            projected_points.append(proj_point)

        return Ln.from_points(*projected_points)

    except Exception as e:

        fo.write(f"\n{e!r}")
        return None


@project.register(MultiLine)
def _(
    multiline: MultiLine,
    source_crs: Union[numbers.Integral, CRS],
    dest_crs: Union[numbers.Integral, CRS],
) -> Union[None, MultiLine]:

    try:

        projected_lines = []
        for line in multiline:
            projected_line = project(
                line,
                source_crs,
                dest_crs,
            )
            if projected_line is None:
                continue
            projected_lines.append(projected_line)

        return MultiLine(projected_lines)

    except Exception as e:

        fo.write(f"\n{e!r}")
        return None


@project.register(Polygon)
def _(
    polygon: Polygon,
    source_crs: Union[numbers.Integral, CRS],
    dest_crs: Union[numbers.Integral, CRS],
) -> Union[None, Polygon]:

    try:

        outer_line = polygon.outer_line()
        if outer_line is None:
            fo.write(f"\nPolygon outer line is None")

        projected_outer_line = project(
            outer_line,
            source_crs,
            dest_crs,
        )

        projected_inner_lines = []
        inner_lines = polygon.inner_lines()
        if inner_lines:
            for inner_line in inner_lines:
                projected_inner_line = project(
                    inner_line,
                    source_crs,
                    dest_crs,
                )
                if projected_inner_line is not None:
                    projected_inner_lines.append(projected_inner_line)

        return Polygon(projected_outer_line, projected_inner_lines)

    except Exception as e:

        fo.write(f"\n{e!r}")
        return None


@project.register(list)
def _(
    features: list,
    source_crs: Union[numbers.Integral, CRS],
    dest_crs: Union[numbers.Integral, CRS],
) -> Union[None, list]:

    try:

        projected_features = []
        for feature in features:
            projected_feature = project(
                feature,
                source_crs,
                dest_crs,
            )
            projected_features.append(projected_feature)

        return projected_features

    except Exception as e:

        fo.write(f"\n{e!r}")
        return None

