import functools
import math
from pathlib import Path
from typing import Iterable, Optional, Generator, Any, List, Callable

import axipy


class Notify:

    @staticmethod
    def try_except_notify(func: Callable) -> Callable:
        @functools.wraps(func)
        def inner(*args, **kwargs) -> Any:
            try:
                result = func(*args, **kwargs)
            except Exception as e:
                Notify.notify_on_failure(e)
                # pass further
                raise e
            else:
                return result

        return inner

    @staticmethod
    def notify_on_failure(e: Exception) -> None:
        axipy.run_in_gui(lambda: axipy.Notifications.push(
            "Экспорт таблиц",
            f"Не удалось рассчитать охват. ({str(e)})",
            axipy.Notifications.Critical
        ))


class _TabFilesResultBoundsCalculator:
    """
    _TabFilesResultBoundsCalculator class
    """

    class BoundsListEmpty(Exception):
        def __init__(self) -> None:
            super().__init__("Список охватов входных файлов пустой.")

    def __init__(
            self,
            tab_files: Iterable[Path],
            inp_cs: Optional[axipy.CoordSystem] = None,
            out_cs: Optional[axipy.CoordSystem] = None,
    ) -> None:
        self.__tab_files: Iterable[Path] = tab_files
        self.__inp_cs: Optional[axipy.CoordSystem] = inp_cs
        self.__out_cs: Optional[axipy.CoordSystem] = out_cs

    @Notify.try_except_notify
    def calculate(self) -> axipy.Rect:
        r: axipy.Rect = self.__calculate_result_bounds()
        x_min, y_min, x_max, y_max = r.xmin, r.ymin, r.xmax, r.ymax
        width, height = r.width, r.height
        # Start
        max_length = max(width, height)
        if max_length == 0:
            max_length = max(abs(x_max), abs(y_max))
            if max_length == 0:
                max_length = 1e8

        p = math.ceil(math.log10(abs(max_length)))
        if abs(p) >= 20:
            raise Exception("Слишком большое значение охвата")

        if self.__in_extent(r, -10 ** p, 10 ** p):
            x_min, y_min, x_max, y_max = (-10 ** p, -10 ** p, 10 ** p, 10 ** p)
        else:
            if self.__in_extent(r, 0, 2 * 10 ** p):
                x_min, y_min, x_max, y_max = (0, 0, 2 * 10 ** p, 2 * 10 ** p)
            else:
                if self.__in_extent(r, -2 * 10 ** p, 0):
                    x_min, y_min, x_max, y_max = (-2 * 10 ** p, -2 * 10 ** p, 0, 0)
                else:
                    c_x = self.__circle_center(x_min, x_max, p)
                    c_y = self.__circle_center(y_min, y_max, p)

                    x_min, y_min, x_max, y_max = (c_x - 10 ** p, c_y - 10 ** p, c_x + 10 ** p, c_y + 10 ** p)

        return axipy.Rect(x_min, y_min, x_max, y_max)

    def __generate_bounds_from_table(self, table: axipy.Table) -> Generator[axipy.Rect, Any, None]:
        items = table.items()

        for item in items:
            g = item.geometry
            if g is None:
                continue

            if self.__inp_cs is not None:
                g.coordsystem = self.__inp_cs

            if self.__out_cs is not None:
                g = g.reproject(self.__out_cs)

            bounds = g.bounds
            yield bounds

    def __generate_bounds_from_tab_files(self) -> Generator[axipy.Rect, Any, None]:
        for tab_file in self.__tab_files:
            bounds = self.__get_bounds_from_tab_file(tab_file)
            if bounds is not None:
                yield bounds

    def __merge_result_rect(self, bounds_list: List[axipy.Rect]) -> axipy.Rect:
        if len(bounds_list) < 1:
            raise self.BoundsListEmpty

        result_rect = bounds_list[0]
        for bounds in bounds_list[1:]:
            result_rect = result_rect.merge(bounds)

        return result_rect

    def __calculate_result_bounds(self) -> axipy.Rect:
        bounds_list = list(self.__generate_bounds_from_tab_files())
        if len(bounds_list) < 1:
            Notify.notify_on_failure(self.BoundsListEmpty())
        return self.__merge_result_rect(bounds_list)

    def __get_bounds_from_table(self, table: axipy.Table) -> axipy.Rect:
        bounds_list = list(self.__generate_bounds_from_table(table))
        return self.__merge_result_rect(bounds_list)

    def __get_bounds_from_tab_file(self, tab_file: Path) -> Optional[axipy.Rect]:
        source = axipy.provider_manager.tab.get_source(str(tab_file))
        table, close_needed = axipy.provider_manager._open_hidden_check_if_close_needed(source)

        try:
            bounds = self.__get_bounds_from_table(table)
        except self.BoundsListEmpty:
            return None
        finally:
            if close_needed:
                table.close()

        return bounds

    def __in_extent(self, r: axipy.Rect, extent1: int, extent2: int) -> bool:
        """
        Функция проверяет нахождение точек во входном промежутке, по x и по y.
        """
        return (
                extent1 < r.xmin and r.xmax < extent2 and
                extent1 < r.ymin and r.ymax < extent2
        )

    def __circle_center(self, arg_1: float, arg_2: float, p: int) -> int:
        """
        Функция поиска круглой середины.
        """
        return round((arg_1 + arg_2) / 2 / 10 ** p) * 10 ** p
