"""
***************************************************************************
    MinimumBoundingGeometry.py
    --------------------------
    Date                 : September 2017
    Copyright            : (C) 2017 by Nyall Dawson
    Email                : nyall dot dawson at gmail dot 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.                                   *
*                                                                         *
***************************************************************************
"""

__author__ = "Nyall Dawson"
__date__ = "September 2017"
__copyright__ = "(C) 2017, Nyall Dawson"

import os
import math

from qgis.PyQt.QtGui import QIcon
from qgis.PyQt.QtCore import QMetaType

from qgis.core import (
    QgsApplication,
    QgsField,
    QgsFeatureSink,
    QgsGeometry,
    QgsWkbTypes,
    QgsFeatureRequest,
    QgsFields,
    QgsRectangle,
    QgsProcessingException,
    QgsProcessingParameterFeatureSource,
    QgsProcessingParameterField,
    QgsProcessingParameterEnum,
    QgsProcessingParameterFeatureSink,
    QgsProcessing,
    QgsFeature,
    QgsVertexId,
    QgsMultiPoint,
)

from processing.algs.qgis.QgisAlgorithm import QgisAlgorithm

pluginPath = os.path.split(os.path.split(os.path.dirname(__file__))[0])[0]


class MinimumBoundingGeometry(QgisAlgorithm):
    INPUT = "INPUT"
    OUTPUT = "OUTPUT"
    TYPE = "TYPE"
    FIELD = "FIELD"

    def icon(self):
        return QgsApplication.getThemeIcon("/algorithms/mAlgorithmConvexHull.svg")

    def svgIconPath(self):
        return QgsApplication.iconPath("/algorithms/mAlgorithmConvexHull.svg")

    def group(self):
        return self.tr("Vector geometry")

    def groupId(self):
        return "vectorgeometry"

    def __init__(self):
        super().__init__()
        self.type_names = [
            self.tr("Envelope (Bounding Box)"),
            self.tr("Minimum Oriented Rectangle"),
            self.tr("Minimum Enclosing Circle"),
            self.tr("Convex Hull"),
        ]

    def initAlgorithm(self, config=None):
        self.addParameter(
            QgsProcessingParameterFeatureSource(self.INPUT, self.tr("Input layer"))
        )
        self.addParameter(
            QgsProcessingParameterField(
                self.FIELD,
                self.tr("Field (optional, set if features should be grouped by class)"),
                parentLayerParameterName=self.INPUT,
                optional=True,
            )
        )
        self.addParameter(
            QgsProcessingParameterEnum(
                self.TYPE, self.tr("Geometry type"), options=self.type_names
            )
        )
        self.addParameter(
            QgsProcessingParameterFeatureSink(
                self.OUTPUT,
                self.tr("Bounding geometry"),
                QgsProcessing.SourceType.TypeVectorPolygon,
            )
        )

    def name(self):
        return "minimumboundinggeometry"

    def displayName(self):
        return self.tr("Minimum bounding geometry")

    def tags(self):
        return self.tr(
            "bounding,box,bounds,envelope,minimum,oriented,rectangle,enclosing,circle,convex,hull,generalization"
        ).split(",")

    def processAlgorithm(self, parameters, context, feedback):
        source = self.parameterAsSource(parameters, self.INPUT, context)
        if source is None:
            raise QgsProcessingException(
                self.invalidSourceError(parameters, self.INPUT)
            )

        field_name = self.parameterAsString(parameters, self.FIELD, context)
        type = self.parameterAsEnum(parameters, self.TYPE, context)
        use_field = bool(field_name)

        field_index = -1

        fields = QgsFields()
        fields.append(QgsField("id", QMetaType.Type.Int, "", 20))

        if use_field:
            # keep original field type, name and parameters
            field_index = source.fields().lookupField(field_name)
            if field_index >= 0:
                fields.append(source.fields()[field_index])
        if type == 0:
            # envelope
            fields.append(QgsField("width", QMetaType.Type.Double, "", 20, 6))
            fields.append(QgsField("height", QMetaType.Type.Double, "", 20, 6))
            fields.append(QgsField("area", QMetaType.Type.Double, "", 20, 6))
            fields.append(QgsField("perimeter", QMetaType.Type.Double, "", 20, 6))
        elif type == 1:
            # oriented rect
            fields.append(QgsField("width", QMetaType.Type.Double, "", 20, 6))
            fields.append(QgsField("height", QMetaType.Type.Double, "", 20, 6))
            fields.append(QgsField("angle", QMetaType.Type.Double, "", 20, 6))
            fields.append(QgsField("area", QMetaType.Type.Double, "", 20, 6))
            fields.append(QgsField("perimeter", QMetaType.Type.Double, "", 20, 6))
        elif type == 2:
            # circle
            fields.append(QgsField("radius", QMetaType.Type.Double, "", 20, 6))
            fields.append(QgsField("area", QMetaType.Type.Double, "", 20, 6))
        elif type == 3:
            # convex hull
            fields.append(QgsField("area", QMetaType.Type.Double, "", 20, 6))
            fields.append(QgsField("perimeter", QMetaType.Type.Double, "", 20, 6))

        (sink, dest_id) = self.parameterAsSink(
            parameters,
            self.OUTPUT,
            context,
            fields,
            QgsWkbTypes.Type.Polygon,
            source.sourceCrs(),
        )
        if sink is None:
            raise QgsProcessingException(self.invalidSinkError(parameters, self.OUTPUT))

        if field_index >= 0:
            geometry_dict = {}
            bounds_dict = {}
            total = 50.0 / source.featureCount() if source.featureCount() else 1
            features = source.getFeatures(
                QgsFeatureRequest().setSubsetOfAttributes([field_index])
            )
            for current, f in enumerate(features):
                if feedback.isCanceled():
                    break

                if not f.hasGeometry():
                    continue

                if type == 0:
                    # bounding boxes - calculate on the fly for efficiency
                    if f[field_index] not in bounds_dict:
                        bounds_dict[f[field_index]] = f.geometry().boundingBox()
                    else:
                        bounds_dict[f[field_index]].combineExtentWith(
                            f.geometry().boundingBox()
                        )
                else:
                    if f[field_index] not in geometry_dict:
                        geometry_dict[f[field_index]] = [f.geometry()]
                    else:
                        geometry_dict[f[field_index]].append(f.geometry())

                feedback.setProgress(int(current * total))

            # bounding boxes
            current = 0
            if type == 0:
                total = 50.0 / len(bounds_dict) if bounds_dict else 1
                for group, rect in bounds_dict.items():
                    if feedback.isCanceled():
                        break

                    # envelope
                    feature = QgsFeature()
                    feature.setGeometry(QgsGeometry.fromRect(rect))
                    feature.setAttributes(
                        [
                            current,
                            group,
                            rect.width(),
                            rect.height(),
                            rect.area(),
                            rect.perimeter(),
                        ]
                    )
                    sink.addFeature(feature, QgsFeatureSink.Flag.FastInsert)
                    geometry_dict[group] = None

                    feedback.setProgress(50 + int(current * total))
                    current += 1
            else:
                total = 50.0 / len(geometry_dict) if geometry_dict else 1

                for group, geometries in geometry_dict.items():
                    if feedback.isCanceled():
                        break

                    feature = self.createFeature(
                        feedback, current, type, geometries, group
                    )
                    sink.addFeature(feature, QgsFeatureSink.Flag.FastInsert)
                    geometry_dict[group] = None

                    feedback.setProgress(50 + int(current * total))
                    current += 1
        else:
            total = 80.0 / source.featureCount() if source.featureCount() else 1
            features = source.getFeatures(QgsFeatureRequest().setSubsetOfAttributes([]))
            geometry_queue = []
            bounds = QgsRectangle()
            for current, f in enumerate(features):
                if feedback.isCanceled():
                    break

                if not f.hasGeometry():
                    continue

                if type == 0:
                    # bounding boxes, calculate on the fly for efficiency
                    bounds.combineExtentWith(f.geometry().boundingBox())
                else:
                    geometry_queue.append(f.geometry())
                feedback.setProgress(int(current * total))

            if not feedback.isCanceled():
                if type == 0:
                    feature = QgsFeature()
                    feature.setGeometry(QgsGeometry.fromRect(bounds))
                    feature.setAttributes(
                        [
                            0,
                            bounds.width(),
                            bounds.height(),
                            bounds.area(),
                            bounds.perimeter(),
                        ]
                    )
                else:
                    feature = self.createFeature(feedback, 0, type, geometry_queue)
                sink.addFeature(feature, QgsFeatureSink.Flag.FastInsert)

        return {self.OUTPUT: dest_id}

    def createFeature(self, feedback, feature_id, type, geometries, class_field=None):
        attrs = [feature_id]
        if class_field is not None:
            attrs.append(class_field)

        multi_point = QgsMultiPoint()

        for g in geometries:
            if feedback.isCanceled():
                break

            vid = QgsVertexId()
            while True:
                if feedback.isCanceled():
                    break
                found, point = g.constGet().nextVertex(vid)
                if found:
                    multi_point.addGeometry(point)
                else:
                    break

        geometry = QgsGeometry(multi_point)
        output_geometry = None
        if type == 0:
            # envelope
            rect = geometry.boundingBox()
            output_geometry = QgsGeometry.fromRect(rect)
            attrs.append(rect.width())
            attrs.append(rect.height())
            attrs.append(rect.area())
            attrs.append(rect.perimeter())
        elif type == 1:
            # oriented rect
            output_geometry, area, angle, width, height = (
                geometry.orientedMinimumBoundingBox()
            )
            attrs.append(width)
            attrs.append(height)
            attrs.append(angle)
            attrs.append(area)
            attrs.append(2 * width + 2 * height)
        elif type == 2:
            # circle
            output_geometry, center, radius = geometry.minimalEnclosingCircle(
                segments=72
            )
            attrs.append(radius)
            attrs.append(math.pi * radius * radius)
        elif type == 3:
            # convex hull
            output_geometry = geometry.convexHull()
            attrs.append(output_geometry.constGet().area())
            attrs.append(output_geometry.constGet().perimeter())
        f = QgsFeature()
        f.setAttributes(attrs)
        f.setGeometry(output_geometry)
        return f
