import enum
import sys
import logging
from typing import Generator, Union, List, Iterator, Optional

from axipy import (Version, CoordFormatter, CoordSystem, Pnt, Point, Polygon, Line, LineString, GeometryCollection,
                   FloatCoord, Geometry, AngleCoord, DialogTask, Ellipse, Rectangle, RoundRectangle, Arc, Feature, 
                   ListPoint)


class FormatCoordinateData(int, enum.Enum):
    simple = 0
    degree = 1
    dash = 2


class TypeCoordinatesData(int, enum.Enum):
    value = 0  # Координаты представлены в виде списка точек
    azimuth = 1  # Первая координата рассматривается как точка, остальные длина + направление (азимут) в градусах
    rumb = 2  # Тоже, что и azimuth, но в румбах


class CoordinateProcess:
    FormatCoordinates: FormatCoordinateData = FormatCoordinateData.simple
    TypeCoordinates: TypeCoordinatesData = TypeCoordinatesData.value
    IdFeatureField: str = 'id_feature'
    IdGeometryField: str = 'id_geometry'
    IdPointField: str = 'id_point'
    ValueFirstField: str = 'v1'
    ValueSecondField: str = 'v2'
    Precision: int = None

    ValueFirstFieldList: List[str] = ['v1', 'lon', 'longitude', 'x', 'долгота']
    ValueSecondFieldList: List[str] = ['v2', 'lat', 'latitude', 'y', 'широта']

    def __init__(self) -> None:
        if not Version.compare(5) <= 0:
            self._formatter = CoordFormatter()


class PointsParams:
    def __init__(self) -> None:
        self.clear()

    def clear(self) -> None:
        self.points = []
        self.holes = []


class CoordinateReader(CoordinateProcess):
    # Чтение координат из таблицы

    IsClosedPolygon: bool = True
    IsClosedLinear: bool = False
    HasNumber: bool = True
    HasPartNumber: bool = True
    HasPointNumber: bool = True

    def __init__(self) -> None:
        super().__init__()

    def _get_typed_geometry(self, param, coordsystem: CoordSystem = None) -> Geometry:
        if len(param.points) == 1 and isinstance(param.points[0], Pnt):
            return Point(param.points[0].x, param.points[0].y, cs=coordsystem)
        elif param.points[0] == param.points[len(param.points) - 1] and self.IsClosedPolygon:
            param.points[0] = param.points[len(param.points) - 1]
            p = Polygon(param.points, cs=coordsystem)
            for h in param.holes:
                if h[0] == h[len(h) - 1]:
                    h[0] = h[len(h) - 1]
                else:
                    h.append(h[0])
                p.holes.append(h)
            return p
        elif len(param.points) == 2:
            return Line(param.points[0], param.points[1], cs=coordsystem)
        elif self.IsClosedLinear and len(param.points) > 2 and param.points[0] == param.points[1]:
            param.points.insert(len(param.points)-1, param.points.pop(1))
        return LineString(param.points, cs=coordsystem)

    @staticmethod
    def _normalized_geometry(g: Geometry) -> Geometry:
        if g and not g.is_valid:
            return g.normalize()
        return g

    def _get_geometry(self, params: list, coordsystem: CoordSystem, id_: int) -> Generator:
        geometry = None
        try:
            if len(params) == 1 and len(params[0].points):
                geometry = self._normalized_geometry(self._get_typed_geometry(params[0], coordsystem))
            elif len(params) > 1:
                coll = GeometryCollection(cs=coordsystem)
                for a in params:
                    coll.append(self._get_typed_geometry(a))
                geometry = self._normalized_geometry(coll.try_to_simplified())
        except Exception as e:
            logging.error(f'Error to get geometry (id={id_}): {e}')
        yield id_, geometry

    def __is_empty(self, a: Optional[str]) -> bool:
        if isinstance(a, str): 
            if a is None or len(a) == 0:
                return True
        return False

    def read(self, features, task: DialogTask, coordsystem: CoordSystem = None) -> Generator:
        # Предполагается, что записи предварительно отсортированы запросом.
        # Возращает генератор с зачитанными геометриями
        UNSET_VALUE = -sys.maxsize
        id_ = UNSET_VALUE # id
        id_g = UNSET_VALUE # geometry number
        id_p = UNSET_VALUE # point number
        id_curr = -1 # current id
        id_p_prev = UNSET_VALUE # previous id

        pars = []
        # индексы значений
        idx_v1 = 3 - int(not self.HasNumber) - int(not self.HasPartNumber) - int(not self.HasPointNumber)
        idx_v2 = idx_v1 + 1

        if not self.HasNumber:
            logging.debug('Id is not present')

        def to_int(v):
            if isinstance(v, (float, str)) and len(v):
                return int(v)
            if type(v) != int:
                logging.error(f'Bad value type {v}')
            return v

        for f in features:
            if task is not None:
                if task.is_canceled:
                    break
                rem = int(f.id) % 1000
                if rem == 0:
                    task.value = f.id

            logging.debug(f"input->{';'.join(str(f[i]) for i in range(0, idx_v2+1))}")
            if self.HasNumber and self.__is_empty(f[0]):
                logging.error(f'Пустой идентификатор. Запись пропущена.')
                continue

            if self.HasPointNumber:
                id_p_prev = id_p
                id_p = to_int(f[idx_v1-1])

            if self.HasNumber:
                id_curr = f[0]
            elif self.HasPointNumber:
                if id_p_prev > id_p:
                    id_curr = id_curr + 1
            else:
                id_curr = id_curr + 1
            if id_ != id_curr:
                if len(pars):
                    id_p_prev = UNSET_VALUE
                    yield from self._get_geometry(pars, coordsystem, id_)
                    pars.clear()
                id_ = id_curr
                logging.debug(f'New id={id_}')
                id_g = UNSET_VALUE
            if self.HasPartNumber and id_g != to_int(f[1]):
                id_g = to_int(f[1])
                if id_g >= 0:
                    pars.append(PointsParams())
                else:  # holes
                    pars[-1].holes.append([])
            elif not self.HasPartNumber and id_g == UNSET_VALUE:
                pars.append(PointsParams())
                id_g = 0
            v1 = 0
            v2 = 0
            if Version.compare(5) <= 0:
                try:
                    if self.__is_empty(f[idx_v1]) or self.__is_empty(f[idx_v2]):
                        logging.error(f'Ошибочные данные (id={id_})')
                        continue
                    v1 = FloatCoord(f[idx_v1]).value
                    v2 = FloatCoord(f[idx_v2]).value
                except Exception as e:
                    logging.error(f'Ошибочные данные ({f[idx_v1]} {f[idx_v2]}. {e.with_traceback})')
            else:
                v1, valid_v1 = self._formatter.as_float(f[idx_v1])
                v2, valid_v2 = self._formatter.as_float(f[idx_v2])
                if not valid_v1 or not valid_v2:
                    logging.error(f'Ошибочные данные ({f[idx_v1]} {f[idx_v2]})')
            p = None
            logging.debug(f'id={id_} id_g={id_g} coord=({v1} {v2})')
            is_direction = self.TypeCoordinates in [TypeCoordinatesData.azimuth, TypeCoordinatesData.rumb]
            is_values = self.TypeCoordinates == TypeCoordinatesData.value
            if id_g >= 0:  # it is't hole
                if is_values or len(pars[-1].points) == 0:
                    p = Pnt(v1, v2)
                elif is_direction:
                    p = Geometry.point_by_azimuth(pars[-1].points[-1], v2, v1, coordsystem)
            else:
                if is_values or len(pars[-1].holes[-1]) == 0:
                    p = Pnt(v1, v2)
                elif is_direction:
                    p = Geometry.point_by_azimuth(pars[-1].holes[-1][-1], v2, v1, coordsystem)
            if p is not None:
                if id_g >= 0:
                    pars[-1].points.append(p)
                else:
                    pars[-1].holes[-1].append(p)
        if len(pars):
            yield from self._get_geometry(pars, coordsystem, id_)


class CoordinateWriter(CoordinateProcess):
    # Запись геометрий в таблицу

    LENGTH_STR_FIELD = 32

    def __init__(self) -> None:
        super().__init__()

    def _process_geometry(self, g: Geometry, idx_obj=None) -> Generator:
        if idx_obj is None:
            idx_obj = [0]
        if isinstance(g, LineString):
            yield from self._process_points(g.points, idx_obj, g.coordsystem)
        elif isinstance(g, Polygon):
            yield from self._process_points(g.points, idx_obj, g.coordsystem)
            for h in g.holes:
                idx_obj[0] += 1
                yield from self._process_points(h, -idx_obj[0], g.coordsystem)
        elif isinstance(g, Line):
            yield from self._process_points([g.begin, g.end], idx_obj, g.coordsystem)
        elif isinstance(g, Point):
            yield from self._process_points([Pnt(g.x, g.y)], idx_obj, g.coordsystem)
        elif isinstance(g, (Rectangle, RoundRectangle, Ellipse)):
            polygon = g.to_polygon()
            yield from self._process_geometry(polygon, idx_obj)
        elif isinstance(g, Arc):
            polyline = g.to_linestring()
            yield from self._process_geometry(polyline, idx_obj)
        elif isinstance(g, GeometryCollection):
            for gg in g:
                yield from self._process_geometry(gg, idx_obj)
                idx_obj[0] += 1
        else:
            logging.error(f'Тип {g} не поддерживается.')

    def _process_value(self, v: float) -> Union[str, float]:
        if self.FormatCoordinates == FormatCoordinateData.degree:
            if Version.compare(5) <= 0:
                return AngleCoord(v).as_string(precision=self.Precision)
            else:
                return self._formatter.as_string(v)
        elif self.FormatCoordinates == FormatCoordinateData.dash:
            if Version.compare(5) <= 0:
                return AngleCoord(v).as_string('-', precision=self.Precision)
            else:
                return self._formatter.as_string_with_delimeter(v)
        if Version.compare(5) <= 0 and self.Precision is not None:
            return FloatCoord(v).as_float_round(self.Precision)
        return v

    def _process_simple_points(self, points: List[float], idx_geom: int) -> Generator:
        for idx, pnt in enumerate(points):
            yield idx_geom, idx, self._process_value(pnt.x), self._process_value(pnt.y)

    def _process_distance_and_azimuth(self, points: ListPoint, idx_geom: int, coordsystem) -> Generator:
        length = len(points)
        for idx in range(0, length - 1):
            if idx == 0:
                yield idx_geom, idx, self._process_value(points[idx].x), self._process_value(points[idx].y)
            dist, angle_ = Geometry.distance_by_points(points[idx], points[idx + 1], coordsystem)
            if Version.compare(5) <= 0 and self.Precision is not None:
                dist = FloatCoord(dist).as_float_round(self.Precision)
            if self.TypeCoordinates == TypeCoordinatesData.rumb:
                if Version.compare(5) <= 0:
                    angle = AngleCoord(angle_).as_rumb(self.Precision)
                else:
                    angle = self._formatter.double_to_rumb(angle_)
            else:
                if Version.compare(5) <= 0 and self.Precision is not None:
                    angle = FloatCoord(angle_).as_float_round(self.Precision)
                else:
                    angle = angle_
            yield idx_geom, idx + 1, dist, angle

    def _process_points(self, points: ListPoint, idx_geom: int, coordsystem: CoordSystem) -> Generator:
        idx = idx_geom[0] if type(idx_geom) == list else idx_geom
        if self.TypeCoordinates == TypeCoordinatesData.value or len(points) == 1:
            yield from self._process_simple_points(points, idx)
        else:
            yield from self._process_distance_and_azimuth(points, idx, coordsystem)

    def write(self, features_iterator: Iterator[Feature], task: DialogTask) -> Generator:
        # Возвращает итератор двух видов в зависимости от установленного TypeCoordinates:
        # (Номер записи, Номер геометрии, номер точки, координата X, координата Y)
        # (Номер записи, Номер геометрии, номер точки, дистанция, азимут)
        for id_, feature in enumerate((f for f in features_iterator if f.geometry is not None), start=1):
            if task.is_canceled:
                break
            task.value += 1
            for v in self._process_geometry(feature.geometry, [0]):
                yield id_, v[0], v[1], v[2], v[3]
