#! python3  # noqa: E265

"""
    Model converter from lookup to Isogeo model.
"""

# #############################################################################
# ########## Libraries #############
# ##################################

# Standard library
import json
import logging
import re
from time import gmtime, strftime
from typing import List

# 3rd party
import geojson
from isogeo_pysdk import CoordinateSystem, Event, FeatureAttribute, Metadata, Workgroup

# submodules
from scan_metadata_processor import _
from scan_metadata_processor.__about__ import __title__, __version__
from scan_metadata_processor.converter import (
    MATCHER_FORMAT,
    MATCHER_GEOMETRY,
    match_coordinate_system,
)
from scan_metadata_processor.parser.models import Lookup

# #############################################################################
# ########## Globals ###############
# ##################################

# log
logger = logging.getLogger(__name__)

# regex
reg_digits_parentesis = re.compile(r"^.*?\([^\d]*(\d+)[^\d]*\).*$")

# ##############################################################################
# ########## Classes ###############
# ##################################


class LookupToIsogeo:
    """Model converter from lookup to Isogeo model.

    :param Lookup in_lookup: source lookup model
    :param list isogeo_formats: Isogeo formats registry
    :param Workgroup isogeo_group: Isogeo group
    :param list group_coordsys: list of group selected coordinate-systems. Optional. Defaults to [].
    """

    COORDINATES_ROUND_LEVEL = 3

    def __init__(
        self,
        in_lookup: Lookup,
        isogeo_formats: list,
        isogeo_group: Workgroup,
        group_coordsys: List = [],
    ):
        """Instanciation method."""
        self.in_lookup = in_lookup
        self.formats = isogeo_formats
        self.group = isogeo_group
        self.group_srs = group_coordsys

    def prepare_event(
        self, evt_type: str = "creation", evt_description: str = None
    ) -> Event:
        """Prepare event text and date.

        :param str evt_type: type of event to create. Defaults to: "creation" - optional
        :param str evt_description: event text. Defaults to: None - optional

        :return: event object ready to be created
        :rtype: Event
        """
        # prepare text
        if evt_type == "creation":
            evt_kind = "update"
            # default text
            default_txt = _(
                "Metadata generated by the Scan, then created by {} v{}."
            ).format(__title__, __version__,)

            # custom or default text
            if evt_description:
                evt_txt = evt_description
            else:
                evt_txt = default_txt

            if self.in_lookup.fmeEnv:
                evt_txt += "\nUsed ETL: {} ({}.fmw)".format(
                    self.in_lookup.fmeEnv.get("version"),
                    self.in_lookup.fmeEnv.get("fmw"),
                )
        elif evt_type == "recreation":
            evt_kind = "update"
            # default text
            default_txt = _(
                "The metadata has been automatically recreated after it was "
                "manually deleted because the source dataset was scanned again."
            )
            # custom or default text
            if evt_description:
                evt_txt = evt_description
            else:
                evt_txt = default_txt
        elif evt_type == "update":
            evt_kind = "update"
            # default text
            default_txt = _(
                "Updated because source dataset has been detected as modified."
            )
            # custom or default text
            if evt_description:
                evt_txt = evt_description
            else:
                evt_txt = default_txt
        else:
            pass

        # creation event
        evt_date = strftime("%Y-%m-%d", gmtime())

        return Event(date=evt_date, description=evt_txt, kind=evt_kind)

    def prepare_feature_attributes(self) -> List[FeatureAttribute]:
        """Prepare feature attributes to be added to the metadata (only for vectors).

        :return: list of feature attributes ready to be added
        :rtype: List[FeatureAttribute]
        """
        out_feature_attributes = []

        # if no vector, no feature attributes
        if self.match_type != "vectorDataset":
            return out_feature_attributes

        # check if attributes were detected by the scan
        if not isinstance(self.in_lookup.attributes, list) or not len(
            self.in_lookup.attributes
        ):
            return out_feature_attributes

        for featattr in self.in_lookup.attributes:
            # extract length from feature-attribute type
            length_filter = reg_digits_parentesis.match(featattr.get("type"))
            if length_filter:
                attr_length = length_filter.group(1)
                attr_type = featattr.get("type").replace("({})".format(attr_length), "")
            else:
                attr_length = None
                attr_type = featattr.get("type")

            # load in object and to the output list
            out_feature_attributes.append(
                FeatureAttribute(
                    name=featattr.get("name"), dataType=attr_type, length=attr_length
                )
            )

        return out_feature_attributes

    def extract_envelope(self) -> dict:
        """Load envelope as GeoJSON, validate it and reduce precision to be more light.

        :return: geographic envelope as dict
        :rtype: dict
        """
        out_envelope = None

        if self.in_lookup.envelope:
            try:
                in_envelope = geojson.loads(self.in_lookup.envelope)
                if not in_envelope.is_valid:
                    logger.warning(
                        "Envelope GeoJSON of '{}' is not valid: {}".format(
                            self.in_lookup.name, in_envelope.errors()
                        )
                    )
                # round coordinates
                out_envelope = geojson.GeoJSON.to_instance(
                    geojson.utils.map_coords(
                        lambda x: round(x, self.COORDINATES_ROUND_LEVEL), in_envelope
                    )
                )
            except Exception as err:
                logger.error(
                    "Failed loading the envelope from {}. Using the raw string."
                    "Trace: {}".format(self.in_lookup.name, err)
                )
                out_envelope = json.loads(self.in_lookup.envelope)

        return out_envelope

    def extract_name(self) -> str:
        """Extract dataset name from lookup. \
        Handle specific cases like FileGDB Open which adds an ugly slash.

        :rtype: str
        """
        out_name = self.in_lookup.name

        # using open api driver add a forbidden char: '/'. Let's remove it.
        if (
            self.match_format_code == "filegdb"
            and self.in_lookup.formatShort == "FILEGDB"
        ):
            out_name = out_name.replace(r"/", ".")

        return out_name

    def extract_path(self) -> str:
        """Extract path.

        :return: dataset path
        :rtype: str
        """
        out_path = None
        if self.in_lookup.path:
            out_path = self.in_lookup.path

        return out_path

    @property
    def match_srs(self) -> CoordinateSystem:
        """If group coordinate-system has been passed to the class, use the matcher. \
        Otherwise, just return a CoordinateSystem with the lookup EPSG code \
        or None missing.

        :return: Isogeo coordinate system
        :rtype: CoordinateSystem
        """
        return match_coordinate_system(self.group_srs, self.in_lookup)[1]

    @property
    def match_format_code(self) -> str:
        """Try to match lookup format (FME short and long names) with:
          1. custom matching table included in submodule
          2. Isogeo formats registry codes.

        :return: Isogeo format code
        :rtype: str
        """
        isogeo_formats_codes = [i.get("code") for i in self.formats]
        # matching format
        md_format = MATCHER_FORMAT.get(self.in_lookup.formatShort)
        # first try to refer to Isogeo formats matrix
        if md_format in isogeo_formats_codes:
            logger.debug(
                "Format '{}' matched custom formats conversion table: {}".format(
                    self.in_lookup.formatShort, md_format
                )
            )
            out_format_code = md_format
        elif (
            isinstance(self.in_lookup.formatShort, str)
            and self.in_lookup.formatShort.lower() in isogeo_formats_codes
        ):
            logger.debug(
                "Format '{}' found in Isogeo formats registry (codes): {}".format(
                    self.in_lookup.formatShort.lower(), md_format
                )
            )
            out_format_code = self.in_lookup.formatShort.lower()
        else:
            out_format_code = None
            logging.warning(
                "Format '{}' not recognized for file: {}".format(
                    self.in_lookup.formatShort, self.in_lookup.name
                )
            )

        return out_format_code

    @property
    def match_format_version(self) -> str:
        """Try to determine the format version when there is only one.

        :return: format version
        :rtype: str
        """
        out_format_version = None

        # without format code, no format version
        if not self.match_format_code:
            return out_format_version

        # get the Isogeo format matching format code
        matched_fmt = [
            fmt for fmt in self.formats if fmt.get("code") == self.match_format_code
        ][0]

        # remove null versions
        matched_versions = [
            version for version in matched_fmt.get("versions") if version is not None
        ]

        # if only one version,so let's use it!
        if len(matched_versions) == 1:
            out_format_version = matched_versions[0]

        return out_format_version

    @property
    def match_geometry_type(self) -> str:
        """Match geometry type from lookup (FME) with Isogeo.

        :return: dataset geometry type
        :rtype: str
        """
        # matching format
        geometry_type = MATCHER_GEOMETRY.get(self.in_lookup.geometryType)

        return geometry_type

    @property
    def match_type(self) -> str:
        """Match dataset type, mainly using the lookup model included helpers.

        :return: metadata type
        :rtype: str
        """
        # default type
        out_md_type = "resource"

        # match between lookup status and type
        if self.in_lookup.isVector:
            out_md_type = "vectorDataset"
        elif self.in_lookup.isRaster:
            out_md_type = "rasterDataset"

        return out_md_type

    def as_metadata(self) -> Metadata:
        """Use submethods to return the lookup as an Isogeo metadata model \
        ready to be created.

        :return: Isogeo metadata converted from source lookup
        :rtype: Metadata
        """
        consolidated_md = Metadata(
            _creator=self.group,
            coordinateSystem=self.match_srs,
            editionProfile="manual",
            envelope=self.extract_envelope(),
            features=self.in_lookup.numberOfFeatures or None,
            featureAttributes=self.prepare_feature_attributes(),
            format=self.match_format_code,
            formatVersion=self.match_format_version,
            geometry=self.match_geometry_type,
            language=self.group.metadataLanguage or "fr",
            name=self.extract_name(),
            path=self.extract_path(),
            title=self.extract_name(),
            series=False,
            type=self.match_type,
        )

        return consolidated_md


# #############################################################################
# ##### Main #######################
# ##################################
if __name__ == "__main__":
    pass
