#!/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  # nosec B405
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")
        if not catalog_layer.isValid():
            raise AssertionError("Could not read catalog layer")

        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 correct_font_capitalisation(family: str) -> str:
        """
        Corrects the font family capitalisation to match the fonts installed
        on the system.

        Returns the name unchanged if not matched
        """
        for candidate in ConversionUtils.available_font_families():
            if candidate.lower().strip() == family.lower().strip():
                return candidate

        return family

    @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)
