import itertools
import time
import traceback
from functools import wraps
from typing import Any, Callable, Iterator, List, Optional, Union

from axipy import (
    AxipyProgressHandler,
    CoordSystem,
    Feature,
    Geometry,
    Layer,
    MapView,
    Notifications,
    Rect,
    Table,
    Text,
    VectorLayer,
    data_manager,
    get_database,
    selection_manager,
    task_manager,
    tr,
    view_manager,
)
from PySide2.QtCore import QLocale
from PySide2.QtSql import QSqlQuery
from PySide2.QtWidgets import QHBoxLayout

debug = False  # TO: False


def print_(*args) -> None:
    if debug:
        print(*args)


def print_exc_() -> None:
    if debug:
        traceback.print_exc()
        print()


def time_f(func: Callable) -> Any:
    @wraps(func)
    def wrapper(*args, **kwargs) -> Any:
        t1 = time.time()
        result = func(*args, **kwargs)
        t2 = time.time()
        t = t2 - t1
        if t > 0.1:
            print_(t)
            print_(func.__name__, ": ", time.strftime("%M:%S", time.localtime(t)), "\n")
        return result

    return wrapper


# @time_f
def quick_envelope(
    handler: AxipyProgressHandler = None, geometries: Iterator[Geometry] = None, size=None
) -> Optional[Rect]:
    """
    Функция возвращает нормализованный ограничивающий прямоугольник.
    """
    if handler:
        handler.set_max_progress(size)

    g = next(geometries, None)
    if g is None:
        return None
    r = g.bounds
    r.normalize()
    xmin, ymin, xmax, ymax = r.xmin, r.ymin, r.xmax, r.ymax

    if handler:
        handler.add_progress(1)

    for g in geometries:

        # TODO w and w/out handler
        if handler:
            handler.raise_if_canceled()

        r = g.bounds
        r.normalize()
        xmin, ymin, xmax, ymax = min(xmin, r.xmin), min(ymin, r.ymin), max(xmax, r.xmax), max(ymax, r.ymax)

        if handler:
            handler.add_progress(1)

    return Rect(xmin, ymin, xmax, ymax)


class DataManagerGuard:
    """
    В конструкторе добавляем в DataManager, а в деструкторе удаляем.
    TODO: Правильный путь - это дать возможность добавлять таблицы, которые
        не отображаются в открытых данных. Очень удобно для временных таблиц
    """

    def __init__(self, table) -> None:
        from secrets import token_hex

        self._table = table
        self._table.name = "selection_" + token_hex(nbytes=16)
        data_manager.add(self._table)

    def __del__(self) -> None:
        data_manager.remove(self._table)

    @property
    def data(self) -> Table:
        return self._table

    @property
    def name(self) -> str:
        return self._table.name


def wrap_tmp_table(table: Table):
    return DataManagerGuard(table)


def selection_as_tmp_table() -> DataManagerGuard:
    """
    Создаём временную таблицу на основе выборки, которую регистрируем в DataManager,
    чтобы её можно было использовать в SQL запросах.
    """
    table = selection_manager.get_as_table()
    return wrap_tmp_table(table)


def editable_table_from_view(view: MapView) -> Optional[Table]:
    if not isinstance(view, MapView):
        return None
    editable_layer = view.editable_layer
    if editable_layer is None:
        return None
    editable_table = editable_layer.data_object
    if not isinstance(editable_table, Table):
        print(f"Типом источника данных должен быть axipy.da.Table вместо {type(editable_table)}")
        return None
    return editable_table


def current_editable_table() -> Table:
    """
    Функция возвращает таблицу для текущего редактируемого слоя активной карты.
    """
    map_view = view_manager.active  # type: MapView
    return editable_table_from_view(map_view)


def ensure_editable_table() -> Table:
    table = current_editable_table()
    assert table is not None
    return table


def normalize_if_not_valid(geometries: Union[List[Geometry], Geometry]) -> Union[List[Geometry], Geometry]:
    def normalize(geometries_arg: List[Geometry]):
        res = []
        for g in geometries_arg:
            if not (g.is_valid or isinstance(g, Text)):
                g.normalize()
            res.append(g)
        return res

    if isinstance(geometries, list):
        return normalize(geometries)
    else:
        return normalize([geometries])[0]


def text_to_float(text: str, locale: QLocale) -> float:
    """
    Преобразовывает текстовою строку в float с использованием локали.

    RuntimeError при ошибках преобразования
    """
    value, ok = locale.toFloat(text)
    if not ok:
        raise RuntimeError("Не удалось преобразовать значение " "поля ввода в число.")
    return value


def ensure_sql_init_for_bg_thread() -> None:
    query = QSqlQuery(get_database())
    task_manager.run_in_gui(lambda: query.exec_("Select 'Init SQL Engine'"))


def clone_cs(cs: CoordSystem) -> Optional[CoordSystem]:
    if cs is None:
        return None

    # TODO: implement clone() for CoordSystem.
    return CoordSystem._wrap(getattr(cs, "_shadow"))


class Connection:
    """
    Вспомогательный класс который хранит соединение между произвольным сигналом
    и любой функцией переданной в виде слота
    """

    def __init__(self, signal, slot, auto_connect=True) -> None:
        self._is_connected = False
        self._signal = signal
        self._slot = slot
        if auto_connect:
            self.connect()

    def __del__(self) -> None:
        try:
            self.disconnect()
        except (Exception,):
            print_exc_()

    @property
    def signal(self):
        return self._signal

    @property
    def slot(self):
        return self._slot

    def connect(self) -> None:
        if self._is_connected:
            return None
        self._signal.connect(self._slot)
        self._is_connected = True

    def disconnect(self) -> None:
        if not self._is_connected:
            return None
        self._signal.disconnect(self._slot)
        self._is_connected = False


def has_table(mv: MapView, table: Table) -> bool:
    """
    Проверяет есть ли таблица среди слоёв карты.
    """
    return find_layer(mv, table) is not None


def find_layer(mv: MapView, table: Table) -> Optional[Layer]:
    """
    Ищет слой в карте содежащий переданную таблицу.
    """
    if table is None:
        return None

    for layer in filter(lambda l: isinstance(l.data_object, Table), mv.map.layers):
        if layer.data_object.name == table.name:
            return layer
    if mv.map.cosmetic.data_object.name == table.name:
        return mv.map.cosmetic
    return None


def create_hbox(*widgets) -> QHBoxLayout:
    """
    Создает горизонтальную разметку с входными виджетами.

    :param widgets: Список виджетов.
    :return: Горизонтальная разметка (QHBoxLayout).
    """
    hbox = QHBoxLayout()
    for widget in widgets:
        hbox.addWidget(widget)
    return hbox


def active_map_vector_layers(cosmetic=True) -> Optional[Iterator[VectorLayer]]:
    active_view = view_manager.active
    if not isinstance(active_view, MapView):
        return None

    layers = active_view.map.layers
    vector_layers = filter(lambda x: isinstance(x, VectorLayer), layers)

    if cosmetic:
        vector_layers = itertools.chain((active_view.map.cosmetic,), vector_layers)

    return vector_layers


counter = 0


def insert_to_table(
    handler: AxipyProgressHandler,
    items: Iterator,
    out_table: Table,
    title,
    progress_wrap=False,
    size=None,
    default_notify=True,
) -> int:
    """
    Вставка в таблицу из итератора, с поддержкой прогресса и проверкой на выход геометрии за пределы СК.

    :param handler:
    :param items:
    :param out_table:
    :param title: Название инструмента для уведомлений
    :param progress_wrap: Необходимость обработки прогресса
    :param size: Размер итератора
    :param default_notify: Уведомление о созданных записях
    :return: Количество вставленных записей
    """
    global counter
    counter = 0

    if progress_wrap:
        handler.set_max_progress(size)
        handler.set_progress(-1)

        def prepare_features(f: Feature):
            handler.add_progress(1)
            handler.raise_if_canceled()
            return f

        items = map(prepare_features, items)

    out_table_cs = out_table.coordsystem

    def ensure_reproj(f: Feature):

        g = f.geometry
        if g.coordsystem != out_table_cs:
            try:
                g = f.geometry.reproject(out_table_cs)
            except (Exception,):
                global counter
                counter += 1
                return None
            else:
                f.geometry = g
        return f

    items = map(ensure_reproj, items)
    items = filter(None, items)

    try:
        out_table.insert(items)
    except (Exception,):
        print_exc_()
        n = handler.progress()
    else:
        handler.set_progress(handler.progress() + 1)
        n = handler.progress()
    if counter > 0:
        Notifications.push(title, tr(f"Не удалось преобразовать геометрии: {counter}"), Notifications.Warning)
        n = n - counter

    n = int(n)

    if default_notify:
        if n > 0:
            n_type = Notifications.Success
        else:
            n_type = Notifications.Critical
        Notifications.push(title, tr(f"Было создано записей: {n}."), n_type)

    return n
