from pathlib import Path
from typing import Any, List, Union, Optional

import numpy as np

from qgis.PyQt.QtCore import QMetaType
from qgis.core import (QgsCoordinateReferenceSystem, QgsCoordinateTransformContext,
                       QgsExpressionContext,
                       QgsExpressionContextScope, QgsFeature, QgsMapLayer,
                       QgsField, QgsFields, QgsProcessingFeedback, QgsProject, QgsProperty,
                       QgsRemappingProxyFeatureSink, QgsRemappingSinkDefinition,
                       QgsVectorFileWriter, QgsVectorLayer)
from qgis.core import QgsFeatureIterator
from ..core import is_profile_field
from ..core.spectralprofile import decodeProfileValueDict, encodeProfileValueDict, SpectralProfileFileReader, \
    SpectralProfileFileWriter
from ...fieldvalueconverter import GenericFieldValueConverter


class GeoJsonFieldValueConverter(QgsVectorFileWriter.FieldValueConverter):

    def __init__(self, fields: QgsFields):
        super(GeoJsonFieldValueConverter, self).__init__()
        self.mFields: QgsFields = QgsFields(fields)

        # define converter functions
        self.mFieldDefinitions = dict()
        self.mFieldConverters = dict()

        for field in self.mFields:
            name = field.name()
            idx = self.mFields.lookupField(name)
            if field.type() != QMetaType.QString and is_profile_field(field):
                converted_field = QgsField(name=name, type=QMetaType.QString, typeName='string', len=-1)
                self.mFieldDefinitions[name] = converted_field
                self.mFieldConverters[idx] = lambda v, f=converted_field: self.convertProfileField(v, f)

            else:
                self.mFieldDefinitions[name] = QgsField(super().fieldDefinition(field))
                self.mFieldConverters[idx] = lambda v: v

    def convertProfileField(self, value, field: QgsField) -> str:
        d = decodeProfileValueDict(value, numpy_arrays=True)
        d['y'] = d['y'].astype(np.float32)
        text = encodeProfileValueDict(d, field)
        return text

    def clone(self) -> QgsVectorFileWriter.FieldValueConverter:
        return GeoJsonFieldValueConverter(self.mFields)

    def convert(self, fieldIdxInLayer: int, value: Any) -> Any:
        return self.mFieldConverters[fieldIdxInLayer](value)

    def fieldDefinition(self, field: QgsField) -> QgsField:
        return QgsField(self.mFieldDefinitions[field.name()])

    def convertedFields(self) -> QgsFields:
        fields = QgsFields()
        for f in self.mFields:
            fields.append(self.fieldDefinition(f))
        return fields


class GeoJSONSpectralLibraryWriter(SpectralProfileFileWriter):

    def __init__(self, *args,
                 crs: QgsCoordinateReferenceSystem = None,
                 rfc7946: bool = True, **kwds):
        super().__init__(*args, **kwds)

        if crs is None:
            crs = QgsCoordinateReferenceSystem('EPSG:4326')
        self.mCrs = crs
        self.mRFC7946 = rfc7946

    @classmethod
    def id(cls) -> str:
        return 'GeoJSON'

    @classmethod
    def filterString(cls) -> bool:
        return "GeoJSON (*.geojson)"

    def writeFeatures(self,
                      path: Union[str, Path],
                      features: List[QgsFeature],
                      feedback: Optional[QgsProcessingFeedback] = None) -> List[Path]:

        path = Path(path)

        if feedback is None:
            feedback = QgsProcessingFeedback()

        if isinstance(features, QgsFeatureIterator):
            features = list(features)

        if len(features) == 0:
            feedback.pushInfo('No features to write')
            return []

        if isinstance(features, QgsFeatureIterator):
            features = list(features)

        f0 = features[0]

        srcFields = f0.fields()
        wkbType = f0.geometry().wkbType()

        layerName = Path(path).stem

        transformContext = QgsProject.instance().transformContext()
        crsJson = QgsCoordinateReferenceSystem('EPSG:4326')

        path: Path = Path(path)

        datasourceOptions = []

        layerOptions = [f'DESCRIPTION={layerName}']
        layerOptions.insert(0, f'RFC7946={"YES" if self.mRFC7946 else "NO"}')

        options = QgsVectorFileWriter.SaveVectorOptions()
        options.actionOnExistingFile = QgsVectorFileWriter.ActionOnExistingFile.CreateOrOverwriteFile
        options.feedback = feedback
        options.datasourceOptions = datasourceOptions
        options.layerOptions = layerOptions
        options.fileEncoding = 'UTF-8'
        options.skipAttributeCreation = False
        options.driverName = 'GeoJSON'

        dstFields = GenericFieldValueConverter.compatibleTargetFields(srcFields, 'GeoJSON')
        converter = GenericFieldValueConverter(srcFields, dstFields)
        # converter = GeoJsonFieldValueConverter(srcFields)
        options.fieldValueConverter = converter
        # dstFields = converter.convertedFields()

        writer_crs: QgsCoordinateReferenceSystem = crsJson if self.mRFC7946 else self.mCrs
        writer: QgsVectorFileWriter = QgsVectorFileWriter.create(path.as_posix(),
                                                                 dstFields,
                                                                 wkbType,
                                                                 writer_crs,
                                                                 transformContext,
                                                                 options)
        # we might need to transform the coordinates to JSON EPSG:4326
        mappingDefinition = QgsRemappingSinkDefinition()
        mappingDefinition.setSourceCrs(self.mCrs)
        mappingDefinition.setDestinationCrs(writer_crs)
        mappingDefinition.setDestinationFields(dstFields)
        mappingDefinition.setDestinationWkbType(wkbType)

        for field in srcFields:
            field: QgsField
            mappingDefinition.addMappedField(field.name(), QgsProperty.fromField(field.name()))

        expressionContext = QgsExpressionContext()
        expressionContext.setFields(srcFields)
        expressionContext.setFeedback(feedback)

        scope = QgsExpressionContextScope()
        scope.setFields(srcFields)
        expressionContext.appendScope(scope)
        transformationContext = QgsCoordinateTransformContext()

        featureSink = QgsRemappingProxyFeatureSink(mappingDefinition, writer)
        featureSink.setExpressionContext(expressionContext)
        featureSink.setTransformContext(transformationContext)

        if writer.hasError() != QgsVectorFileWriter.NoError:
            feedback.reportError(f'Error when creating {path}: {writer.errorMessage()}')
            return []

        featureSink.flushBuffer()

        if not featureSink.addFeatures(features):
            feedback.reportError(f'Error when creating feature: {writer.errorMessage()}')
            return []

        del writer

        lyr = QgsVectorLayer(path.as_posix())
        if lyr.isValid():
            for dstField in dstFields:
                i = lyr.fields().lookupField(dstField.name())
                if i >= 0:
                    lyr.setEditorWidgetSetup(i, dstField.editorWidgetSetup())
            msg, success = lyr.saveDefaultStyle(QgsMapLayer.StyleCategory.AllStyleCategories)
            if not success:
                feedback.reportError(msg)

        return [path]


class GeoJSONSpectralLibraryReader(SpectralProfileFileReader):

    def __init__(self, *args, **kwds):
        super().__init__(*args, **kwds)

    @classmethod
    def id(cls) -> str:
        return 'GeoJSON'

    @classmethod
    def canReadFile(cls, path: Union[str, Path]) -> bool:
        return Path(path).suffix == '.geojson'

    def asFeatures(self) -> List[QgsFeature]:
        lyr = QgsVectorLayer(self.path().as_posix())
        lyr.loadDefaultStyle()
        return list(lyr.getFeatures())
