#!/usr/bin/env python

# /***************************************************************************
# utils.py
# ----------
# Date                 : September 2019
# copyright            : (C) 2019 by Nyall Dawson
# email                : nyall.dawson@gmail.com
#
#  ***************************************************************************/
#
# /***************************************************************************
#  *                                                                         *
#  *   This program is free software; you can redistribute it and/or modify  *
#  *   it under the terms of the GNU General Public License as published by  *
#  *   the Free Software Foundation; either version 2 of the License, or     *
#  *   (at your option) any later version.                                   *
#  *                                                                         *
#  ***************************************************************************/

"""
Conversion utilities
"""

import math
import os
import re
import unicodedata
import xml.etree.ElementTree as ET
from pathlib import Path
from typing import Optional, List

from qgis.PyQt.QtCore import Qt, QPointF, QVariant
from qgis.PyQt.QtGui import QFontDatabase
from qgis.core import (
    QgsField,
    QgsFields,
    QgsMemoryProviderUtils,
    QgsFeature,
    QgsVectorLayer,
    QgsFeatureRequest,
)

from ..bintools.extractor import Extractor


class ConversionUtils:
    """
    Conversion utilities
    """

    @staticmethod
    def convert_angle(angle: float) -> float:
        """
        Converts an ESRI angle (counter-clockwise) to a QGIS angle (clockwise)
        """
        a = 360 - angle
        if a > 180:
            # instead of "359", use "-1"
            a -= 360
        return a

    @staticmethod
    def adjust_offset_for_rotation(offset, rotation):
        """
        Adjusts marker offset to account for rotation
        """
        angle = -math.radians(rotation)
        return QPointF(
            offset.x() * math.cos(angle) - offset.y() * math.sin(angle),
            offset.x() * math.sin(angle) + offset.y() * math.cos(angle),
        )

    @staticmethod
    def symbol_pen_to_qpenstyle(style):
        """
        Converts a symbol pen style to a QPenStyle
        """
        types = {
            "solid": Qt.PenStyle.SolidLine,
            "dashed": Qt.PenStyle.DashLine,
            "dotted": Qt.PenStyle.DotLine,
            "dash dot": Qt.PenStyle.DashDotLine,
            "dash dot dot": Qt.PenStyle.DashDotDotLine,
            "null": Qt.PenStyle.NoPen,
        }
        return types[style]

    @staticmethod
    def symbol_pen_to_qpencapstyle(style):
        """
        Converts a symbol pen cap to a QPenCapStyle
        """
        types = {
            "butt": Qt.PenCapStyle.FlatCap,
            "round": Qt.PenCapStyle.RoundCap,
            "square": Qt.PenCapStyle.SquareCap,
        }
        return types[style]

    @staticmethod
    def symbol_pen_to_qpenjoinstyle(style):
        """
        Converts a symbol pen join to a QPenJoinStyle
        """
        types = {
            "miter": Qt.PenJoinStyle.MiterJoin,
            "round": Qt.PenJoinStyle.RoundJoin,
            "bevel": Qt.PenJoinStyle.BevelJoin,
        }
        return types[style]

    @staticmethod
    def convert_mdb_table_to_memory_layer(file_path: str, table_name: str):
        """
        Extracts the contents of a non-spatial table from a MDB to a memory layer
        :param file_path: path to .mdb file
        :param table_name: table name to extract
        """
        rows = Extractor.extract_non_spatial_table_from_mdb(file_path, table_name)

        header = rows[0]
        rows = rows[1:]

        fields = QgsFields()
        for idx, h in enumerate(header):
            # sniff first row
            if isinstance(rows[0][idx], float):
                field_type = QVariant.Double
            elif isinstance(rows[0][idx], int):
                field_type = QVariant.Int
            else:
                field_type = QVariant.String

            fields.append(QgsField(h, field_type))

        layer = QgsMemoryProviderUtils.createMemoryLayer(table_name, fields)

        for row in rows:
            f = QgsFeature()
            f.initAttributes(len(row))
            f.setAttributes(row)
            layer.dataProvider().addFeature(f)

        return layer

    @staticmethod
    def path_insensitive(path):
        """
        Recursive part of path_insensitive to do the work.
        """
        try:
            return ConversionUtils._path_insensitive(path) or path
        except PermissionError:
            return path

    @staticmethod
    def _path_insensitive(path) -> Optional[str]:  # pylint: disable=too-many-return-statements
        """
        Recursive part of path_insensitive to do the work.
        """

        if path == "" or os.path.exists(path):
            return Path(path).absolute().as_posix() if path else path

        path = Path(path).absolute().as_posix()

        base = os.path.basename(path)  # may be a directory or a file
        dirname = os.path.dirname(path)

        suffix = ""
        if not base:  # dir ends with a slash?
            if len(dirname) < len(path):
                suffix = path[: len(path) - len(dirname)]

            base = os.path.basename(dirname)
            dirname = os.path.dirname(dirname)

        if not base:
            return None

        if not os.path.exists(dirname):
            dirname = ConversionUtils._path_insensitive(dirname)
            if not dirname:
                return None

        # at this point, the directory exists but not the file

        try:  # we are expecting dirname to be a directory, but it could be a file
            files = os.listdir(dirname)
        except OSError:
            return None

        base_low = base.lower()
        try:
            base_final = next(fl for fl in files if fl.lower() == base_low)
        except StopIteration:
            return None

        if base_final:
            return Path(os.path.join(dirname, base_final) + suffix).as_posix()
        else:
            return None

    @staticmethod
    def is_absolute_path(path: str) -> bool:
        """
        Returns True if a path is an absolute path
        """
        if path.startswith("."):
            return False

        if path.startswith(r"//"):
            return True

        return bool(re.match(r"^\w:", path))

    @staticmethod
    def get_absolute_path(path: str, base: str) -> str:
        """
        Converts a path to an absolute path, in a case insensitive way
        """
        base_folder = base
        if Path(base_folder).is_file():
            base_folder = Path(base_folder).parent.as_posix()

        path = path.replace("\\", "/")

        if ConversionUtils.is_absolute_path(path):
            return ConversionUtils.path_insensitive(path)

        res = ConversionUtils.path_insensitive("{}/{}".format(base_folder, path))
        res = res.replace("/./", "/")
        return res

    @staticmethod
    def format_xml(input_xml: ET.Element) -> str:
        """
        Pretty formats an XML string
        """
        xml = ET.tostring(input_xml, encoding="unicode")

        if os.name == "nt":
            # seen segfaults on lxml.fromstring on Windows?
            return xml

        try:
            from lxml import etree as LET  # pylint: disable=import-outside-toplevel

            root = LET.fromstring(xml)
            tree = LET.ElementTree(root)
            LET.indent(tree, "   ")
            return LET.tostring(tree, encoding="utf-8").decode("utf-8")
        except ImportError:
            return xml

    @staticmethod
    def is_gdal_version_available(major: int, minor: int, rev: int) -> bool:
        """
        Returns True if GDAL is greater than or equal to the specified version
        """
        from osgeo import gdal  # pylint: disable=import-outside-toplevel

        required_version_int = major * 1000000 + minor * 10000 + rev * 100

        return int(gdal.VersionInfo("VERSION_NUM")) >= required_version_int

    @staticmethod
    def open_gdb_items_layer(gdb_path: str) -> Optional[QgsVectorLayer]:
        """
        Opens a GDB items layer
        """

        catalog_path = Path(gdb_path) / "a00000001.gdbtable"
        catalog_layer = QgsVectorLayer(catalog_path.as_posix(), "catalog")
        assert catalog_layer.isValid()

        request = QgsFeatureRequest().setFilterExpression("lower(\"Name\")='gdb_items'")
        try:
            items_feature = next(catalog_layer.getFeatures(request))
        except StopIteration:
            return None

        id_field_idx = catalog_layer.fields().lookupField("ID")
        items_id = items_feature[id_field_idx]

        items_path = Path(gdb_path) / "a{:0>8x}.gdbtable".format(items_id)
        items_layer = QgsVectorLayer(items_path.as_posix(), "items")
        return items_layer

    @staticmethod
    def safe_filename(text: str) -> str:
        """
        Converts text to a string safe for a filename
        """
        normalized = unicodedata.normalize("NFD", text)

        # Filter out non-Latin characters
        return re.sub(r"[^a-zA-Z0-9_\-]", "_", normalized)

    @staticmethod
    def available_font_families() -> List[str]:
        """
        Returns the installed font families
        """
        try:
            return QFontDatabase().families()
        except TypeError:
            return QFontDatabase.families()

    @staticmethod
    def available_font_styles(family: str) -> List[str]:
        """
        Returns the available font styles for a family
        """
        try:
            return QFontDatabase().styles(family)
        except TypeError:
            return QFontDatabase.styles(family)

    @staticmethod
    def font_style_string(font) -> str:
        """
        Returns the style string for a font
        """
        try:
            return QFontDatabase().styleString(font)
        except TypeError:
            return QFontDatabase.styleString(font)
