
import sys
fo = sys.stdout

from typing import Sequence

from geogst.core.profiles.geoprofiles import *
from geogst.core.profiles.profilers import *
from geogst.core.geometries.projections.project import *


@singledispatch
def project_points(
    obj,
    *args,
    **kwargs,
):

    return 1


@project_points.register(GeoProfiles)
def _(
    geoprofiles: GeoProfiles,
    data: Sequence,
    data_wkt_crs: str,
    max_profile_distance: numbers.Real,
    projection_method: ProjectionMethod = ProjectionMethod.NEAREST,
    input_type: PointsInput = PointsInput.POINTS,
    **kargs
) -> Union[Error, 'projections']:

    """
    Proiect points onto the geoprofiles.

    :param data: a list of point dataset.
    :param max_profile_distance: the maximum point distance from the profile.
    :param projection_method: the projection method to use for the points.
    :param input_type: the type of the input dataset. Default is points.
    :param cat_key: the category name of the point dataset.
    :return: the error status.
    """

    try:

        if input_type == PointsInput.POINTS:

            data_projected_onto_profiles = project_points(
                geoprofiles.profilers,
                data,
                data_wkt_crs,
                max_profile_distance,
                projection_method,
                input_type,
                **kargs
            )

        elif input_type == PointsInput.ATTITUDES:

            data_projected_onto_profiles = project_attitudes(
                geoprofiles.profilers,
                data,
                data_wkt_crs,
                max_profile_distance,
                projection_method,
                **kargs
            )

        elif input_type == PointsInput.FAULTS:

            data_projected_onto_profiles = Error(
                True,
                caller_name(),
                Exception(f"Faults not implemented"),
                traceback.format_exc())

        elif input_type == PointsInput.FOCAL_MECHANISMS:

            data_projected_onto_profiles = Error(
                True,
                caller_name(),
                Exception(f"Focal mechanisms not implemented"),
                traceback.format_exc())

        else:

            data_projected_onto_profiles = Error(
                True,
                caller_name(),
                Exception(f"Got not implemented point input type: {input_type}"),
                traceback.format_exc())

        return data_projected_onto_profiles

    except Exception as e:

        return Error(
            True,
            caller_name(),
            e,
            traceback.format_exc())


@project_points.register(Profilers)
def _(
    profilers: Profilers,
    data: List[Tuple[Category, Point]],
    data_wkt_crs: str,
    max_profile_distance: numbers.Real,
    projection_method: ProjectionMethod = ProjectionMethod.NEAREST,
    input_type: PointsInput = PointsInput.POINTS,
    **kwargs
) -> Union[Error, List[Dict[Category, List[PointTrace]]]]:
    """
    Projects a set of points onto the section profile.

    :param data: the set of points to be plotted onto the section.
    :param max_profile_distance: the maximum allowed projection distance between the individual point and the profile
    :param projection_method: the method to project the points to the section.
    :param kwargs: the keyword arguments.
    :return: the parsed point traces and an error status.
    """

    try:

        point_traces = []

        for ndx, line_profiler in enumerate(profilers, start=1):

            profile_points = project_points(
                line_profiler,
                data=data,
                data_wkt_crs=data_wkt_crs,
                max_profile_distance=max_profile_distance,
                projection_method=projection_method,
                **kwargs
            )
            if isinstance(profile_points, Error):
                raise Exception(f"{profile_points!r}")

            point_traces.append(profile_points)

        return point_traces

    except Exception as e:

        return Error(
            True,
            caller_name(),
            e,
            traceback.format_exc())


@project_points.register(LineProfiler)
def _(
    line_profiler: LineProfiler,
    data: List[Tuple[Category, Point, Fault]],
    data_wkt_crs: str,
    max_profile_distance: numbers.Real,
    projection_method: ProjectionMethod = ProjectionMethod.NEAREST,
    input_type: PointsInput = PointsInput.POINTS,
    **kwargs
) -> Union[Error, defaultdict[PointTrace]]:
    """
    Projects a set of 3D points onto the section profile.

    :param data: the set of 3D points to project onto the section.
    :param max_profile_distance: the maximum allowed projection distance between the points and the profile.
    :param projection_method: the method to use for projecting the points onto the section.
    :param kwargs: the keyword arguments.
    :return: dictionary storing projected points and error status.
    """

    try:

        if projection_method == ProjectionMethod.NEAREST:
            map_axes = None
        elif projection_method == ProjectionMethod.COMMON_AXIS:
            map_axes = Axis(kwargs['trend'], kwargs['plunge'])
        elif projection_method == ProjectionMethod.INDIVIDUAL_AXES:
            map_axes = [Axis(trend, plunge) for trend, plunge in kwargs['individual_axes_values']]
        else:
            raise Exception(f"Mapping method is {projection_method}")

        projected_points = defaultdict(list)

        for ndx_super, (category, position) in enumerate(data, start=1):

            previous_segments_offset = 0

            src_point_in_profiler_crs = project(
                position,
                data_wkt_crs,
                line_profiler.wkt_crs,
            )

            for ndx, segment_profiler in enumerate(line_profiler, start=1):

                intersection_point_in_profiler_crs, err = segment_profiler.project_point(
                    point=src_point_in_profiler_crs,
                    projection_method=projection_method,
                    min_profile_distance=MIN_DISTANCE_TOLERANCE,
                    map_axis=map_axes if not isinstance(map_axes, list) else map_axes[ndx],
                    axis_angular_tolerance=MIN_DISORIENTATION_TOLERANCE
                )

                if err:
                    raise Exception(f"{err!r}")

                if intersection_point_in_profiler_crs is None:
                    continue

                point_trace, err = segment_profiler.parse_single_point_projection_result(
                    source_pt=src_point_in_profiler_crs,
                    intersection_point=intersection_point_in_profiler_crs,
                    max_profile_distance=max_profile_distance,
                    offset=previous_segments_offset
                )

                if err:
                    raise Exception(f"{err!r}")

                if point_trace is not None:
                    projected_points[category].append(point_trace)

                previous_segments_offset += segment_profiler.length()

        best_points_projections = defaultdict(PointTrace)

        for category, solutions in projected_points.items():
            if len(solutions) == 1:
                best_points_projections[category] = solutions[0]
            else:
                min_distance = min([solution.dist for solution in solutions])
                for solution in solutions:
                    if solution.dist <= min_distance:
                        best_points_projections[category] = solution
                        break

        return best_points_projections

    except Exception as e:

        return Error(
            True,
            caller_name(),
            e,
            traceback.format_exc())


@singledispatch
def project_attitudes(
    obj,
    data: List[Tuple[Category, Point, Plane]],
    data_wkt_crs: str,
    max_profile_distance: numbers.Real,
    projection_method: ProjectionMethod = ProjectionMethod.NEAREST,
    **kwargs
) -> Tuple[Union[type(None), List[Dict[Category, PlaneTrace]]], Error]:
    """
    Projects a set of georeferenced space3d attitudes onto the section profile.

    :param data: the set of georeferenced space3d attitudes to plot on the section.
    :param projection_method: the method to map the attitudes to the section.
    :param max_profile_distance: the maximum projection distance between the plane_attitude and the profile
    :return: an attitudes set and an error status.
    """

    return Error()


@project_attitudes.register(Profilers)
def _(
    profilers: Profilers,
    data: List[Tuple[Category, Point, Plane]],
    data_wkt_crs: str,
    max_profile_distance: numbers.Real,
    projection_method: ProjectionMethod = ProjectionMethod.NEAREST,
    **kwargs
) -> Union[None, Error, List[Dict[Category, PlaneTrace]]]:
    """
    Projects a set of georeferenced space3d attitudes onto the section profile.

    :param data: the set of georeferenced space3d attitudes to plot on the section.
    :param projection_method: the method to map the attitudes to the section.
    :param max_profile_distance: the maximum projection distance between the plane_attitude and the profile
    :return: an attitudes set and an error status.
    """

    try:

        plane_traces = []

        for ndx, line_profiler in enumerate(profilers, start=1):

            profile_attitudes = project_attitudes(
                line_profiler,
                data,
                data_wkt_crs,
                max_profile_distance,
                projection_method,
                **kwargs
            )
            if isinstance(profile_attitudes, Error):
                return profile_attitudes

            plane_traces.append(profile_attitudes)

        return plane_traces

    except Exception as e:

        return Error(
            True,
            caller_name(),
            e,
            traceback.format_exc())


@project_attitudes.register(LineProfiler)
def _(
    line_profiler: LineProfiler,
    attitudes: List[Tuple[Category, Point, Plane]],
    data_wkt_crs: str,
    max_profile_distance: numbers.Real,
    projection_method: ProjectionMethod,
    **kwargs
) -> Union[None, Dict[Category, PlaneTrace]]:
    """
    Projects a set of space3d attitudes onto the section profile.

    :param attitudes: the set of georeferenced space3d attitudes to plot on the section.
    :param max_profile_distance: the maximum projection distance between the plane_attitude and the profile
    :param projection_method: the method to map the attitudes to the section.
    :return: list of PlaneTrace values.
    """

    try:

        if projection_method == ProjectionMethod.NEAREST:
            map_axes = None
        elif projection_method == ProjectionMethod.COMMON_AXIS:
            map_axes = Axis(kwargs['trend'], kwargs['plunge'])
        elif projection_method == ProjectionMethod.INDIVIDUAL_AXES:
            map_axes = [Axis(trend, plunge) for trend, plunge in kwargs['individual_axes_values']]
        else:
            raise Exception(f"Mapping method is {projection_method}")

        """
        if mapping_method['method'] not in ('nearest', 'common axis', 'individual axes', 'attitude'):
            return None, Error(True, caller_name(), Exception(
                f"Mapping method is '{mapping_method['method']}'. One of 'nearest', 'common axis', 'individual axes', 'attitude' expected"))

        if mapping_method['method'] == 'individual axes' and len(mapping_method['individual_axes_values']) != len(attitudes):
            return None, Error(True, caller_name(), Exception(
                f"Individual axes values are {len(mapping_method['individual_axes_values'])} but attitudes are {len(attitudes)}"))

        if mapping_method['method'] == 'nearest':
            map_axes = None
        elif mapping_method['method'] == 'common axis':
            map_axes = Axis(mapping_method['trend'], mapping_method['plunge'])
        else:
            map_axes = [Axis(trend, plunge) for trend, plunge in mapping_method['individual_axes_values']]
        """

        projected_attitudes = defaultdict(list)

        for ndx_super, (attitude_id, position, attitude) in enumerate(attitudes, start=1):

            previous_segments_offset = 0

            src_point_in_profiler_crs = project(
                position,
                data_wkt_crs,
                line_profiler.wkt_crs,
            )

            for ndx, segment_profiler in enumerate(line_profiler, start=1):

                result, err = segment_profiler.project_attitude(
                    attitude=attitude,
                    point=src_point_in_profiler_crs,
                    projection_method=projection_method,
                    min_profile_distance=MIN_DISTANCE_TOLERANCE,
                    map_axis=map_axes if not isinstance(map_axes, list) else map_axes[ndx],
                    axis_angular_tolerance=MIN_DISORIENTATION_TOLERANCE
                )

                if err:
                    raise Exception(f"{err!r}")

                if result is None:
                    continue

                intersection_vector, intersection_point_in_profiler_crs = result

                profile_attitude = segment_profiler.parse_single_attitude_projection_result(
                    attitude=attitude,
                    attitude_intersection=intersection_vector,
                    source_pt=src_point_in_profiler_crs,
                    intersection_point=intersection_point_in_profiler_crs,
                    max_profile_distance=max_profile_distance,
                    offset=previous_segments_offset
                )

                if isinstance(profile_attitude, Error):
                    return profile_attitude

                if profile_attitude is not None:
                    projected_attitudes[attitude_id].append(profile_attitude)

                previous_segments_offset += segment_profiler.length()

        best_attitudes_projections = defaultdict(PlaneTrace)

        for attitude_id, solutions in projected_attitudes.items():

            if len(solutions) == 1:
                best_attitudes_projections[attitude_id] = solutions[0]
            else:
                min_distance = min([solution.dist for solution in solutions])
                for solution in solutions:
                    if solution.dist <= min_distance:
                        best_attitudes_projections[attitude_id] = solution
                        break

        return best_attitudes_projections

    except Exception as e:

        return Error(
            True,
            caller_name(),
            e,
            traceback.format_exc())
