# -*- coding: utf-8 -*-
"""
Advanced Map Downloader - QGIS Plugin

A QGIS plugin for downloading georeferenced maps with multiple coordinate input types.
Part of MAS Raster Processing tools.

Author: Mirjan Ali Sha
Email: mastools.help@gmail.com
"""

import os
import math
from qgis.PyQt.QtCore import QSettings, QTranslator, QCoreApplication, QCoreApplication as QApp
from qgis.PyQt.QtGui import QIcon
from qgis.PyQt.QtWidgets import QAction, QToolBar, QMessageBox
from qgis.core import QgsProject, QgsRasterLayer
from qgis.gui import QgsMapToolPan

from .advanced_map_downloader_dialog import AdvancedMapDownloaderDialog
from .coordinate_handlers import (
    parse_xyz_tile, parse_square_grid, parse_latlon_pixels,
    parse_wkt, parse_wkb, parse_geojson
)
from .canvas_tools import (
    RectangleCaptureTool, ExtentRubberBand, get_canvas_extent
)
from .export_handlers import render_map, export_map


class AdvancedMapDownloader:
    """QGIS Plugin Implementation for Advanced Map Downloader."""

    def __init__(self, iface):
        """Constructor.

        :param iface: An interface instance that will be passed to this class
            which provides the hook by which you can manipulate the QGIS
            application at run time.
        :type iface: QgsInterface
        """
        self.iface = iface
        self.plugin_dir = os.path.dirname(__file__)

        # Initialize locale
        locale = QSettings().value('locale/userLocale')[0:2]
        locale_path = os.path.join(
            self.plugin_dir,
            'i18n',
            'AdvancedMapDownloader_{}.qm'.format(locale))

        if os.path.exists(locale_path):
            self.translator = QTranslator()
            self.translator.load(locale_path)
            QCoreApplication.installTranslator(self.translator)

        # Declare instance attributes
        self.actions = []
        self.menu = self.tr(u'&MAS Raster Processing')  # Common menu name
        self.toolbar = None

        # Dialog and tools
        self.dlg = None
        self.point_tool = None
        self.rectangle_tool = None
        self.rubber_band = None
        self.previous_map_tool = None

        # Plugin state
        self.first_start = True

    def tr(self, message):
        """Get the translation for a string using Qt translation API."""
        return QCoreApplication.translate('AdvancedMapDownloader', message)

    def initGui(self):
        """Create the menu entries and toolbar icons inside the QGIS GUI."""

        icon_path = os.path.join(self.plugin_dir, 'icon.png')

        # Check if the MAS Raster Processing toolbar already exists, if not
        # create it
        self.toolbar = self.iface.mainWindow().findChild(
            QToolBar, 'MASRasterProcessingToolbar')
        if self.toolbar is None:
            self.toolbar = self.iface.addToolBar(u'MAS Raster Processing')
            self.toolbar.setObjectName('MASRasterProcessingToolbar')

        # Create the action
        action = QAction(
            QIcon(icon_path),
            u"Advanced Map Downloader",
            self.iface.mainWindow()
        )

        action.setToolTip(
            "Advanced Map Downloader - Download georeferenced maps with multiple coordinate input options"
        )
        action.setStatusTip(
            "Download maps from any layer with XYZ tiles, grid, lat-lon, WKT, WKB, or GeoJSON coordinates"
        )

        action.triggered.connect(self.run)

        # Add to raster menu under MAS Raster Processing
        self.iface.addPluginToRasterMenu(self.menu, action)

        # Add to toolbar
        self.toolbar.addAction(action)

        # Add to actions list for cleanup
        self.actions.append(action)

    def unload(self):
        """Removes the plugin menu item and icon from QGIS GUI."""
        # Clean up rubber band
        self._cleanup_rubber_band()

        # Close dialog if open
        if self.dlg:
            self.dlg.close()

        # Remove actions
        for action in self.actions:
            self.iface.removePluginRasterMenu(self.menu, action)
            self.toolbar.removeAction(action)

    def _cleanup_rubber_band(self):
        """Clean up rubber band visualization."""
        if self.rubber_band:
            self.rubber_band.remove()
            self.rubber_band = None

    def _on_dialog_closed(self):
        """Handle dialog close event to clean up rubber band."""
        self._cleanup_rubber_band()
        self._restore_map_tool()

    def _restore_map_tool(self):
        """Restore the previous map tool."""
        if self.previous_map_tool:
            self.iface.mapCanvas().setMapTool(self.previous_map_tool)
        else:
            pan_tool = QgsMapToolPan(self.iface.mapCanvas())
            self.iface.mapCanvas().setMapTool(pan_tool)

    def _on_draw_canvas(self, coord_type):
        """Handle Draw from Canvas button click - always draws rectangle."""
        # Store current map tool
        self.previous_map_tool = self.iface.mapCanvas().mapTool()

        # Hide dialog temporarily
        self.dlg.hide()

        # Always use rectangle capture tool for all input types
        self.rectangle_tool = RectangleCaptureTool(self.iface.mapCanvas())
        self.rectangle_tool.rectangle_captured.connect(
            lambda minLat, minLon, maxLat, maxLon: self._on_rectangle_captured(
                minLat, minLon, maxLat, maxLon, coord_type
            )
        )
        self.iface.mapCanvas().setMapTool(self.rectangle_tool)

        self.iface.mainWindow().statusBar().showMessage(
            "Draw rectangle on map to define download area", 5000
        )

    def _on_rectangle_captured(
            self, min_lat, min_lon, max_lat, max_lon, coord_type):
        """Handle rectangle capture from canvas - auto-adjusts all settings."""
        self._restore_map_tool()

        # Calculate center, width, and height
        center_lat = (min_lat + max_lat) / 2
        center_lon = (min_lon + max_lon) / 2

        # Transform extent to EPSG:3857 to calculate meters
        from qgis.core import QgsCoordinateReferenceSystem, QgsCoordinateTransform, QgsPointXY
        crs_src = QgsCoordinateReferenceSystem("EPSG:4326")
        crs_dest = QgsCoordinateReferenceSystem("EPSG:3857")
        transformer = QgsCoordinateTransform(
            crs_src, crs_dest, QgsProject.instance())

        pt_min = transformer.transform(QgsPointXY(min_lon, min_lat))
        pt_max = transformer.transform(QgsPointXY(max_lon, max_lat))

        width_m = abs(pt_max.x() - pt_min.x())
        height_m = abs(pt_max.y() - pt_min.y())

        # Get current GSD
        gsd = self.dlg.spinBox_gsd.value()

        # Calculate pixel dimensions based on GSD
        width_px = max(1, int(width_m / gsd))
        height_px = max(1, int(height_m / gsd))

        if coord_type == 'Lat-Long with Pixels':
            # Set center lat/lon and auto-calculate pixel dimensions
            self.dlg.set_latlon(round(center_lat, 6), round(center_lon, 6))
            self.dlg.spinBox_widthPx.setValue(width_px)
            self.dlg.spinBox_heightPx.setValue(height_px)

        elif coord_type == 'XYZ Tile':
            # Convert to XYZ tile at current zoom level
            z = self.dlg.spinBox_z.value()
            x, y = self._latlon_to_tile(center_lat, center_lon, z)
            self.dlg.set_xyz(z, x, y)

        elif coord_type == 'Square Grid':
            # Set center and calculate grid size (use average of width and
            # height)
            self.dlg.set_grid_latlon(
                round(
                    center_lat, 6), round(
                    center_lon, 6))
            # Set grid size to the average dimension in meters
            avg_size_m = (width_m + height_m) / 2
            current_unit = self.dlg.comboBox_gridUnit.currentText()
            # Convert to current unit
            unit_conversions = {
                'meters': 1.0,
                'kilometers': 1000.0,
                'miles': 1609.344,
                'nautical_miles': 1852.0
            }
            grid_size = avg_size_m / unit_conversions.get(current_unit, 1.0)
            self.dlg.spinBox_gridSize.setValue(grid_size)

        elif coord_type == 'WKT':
            # Create WKT polygon
            wkt = f"POLYGON (({min_lon} {min_lat}, {max_lon} {min_lat}, {max_lon} {max_lat}, {min_lon} {max_lat}, {min_lon} {min_lat}))"
            self.dlg.set_wkt(wkt)

        elif coord_type == 'WKB':
            # Create WKB from WKT
            from qgis.core import QgsGeometry
            wkt = f"POLYGON (({min_lon} {min_lat}, {max_lon} {min_lat}, {max_lon} {max_lat}, {min_lon} {max_lat}, {min_lon} {min_lat}))"
            geom = QgsGeometry.fromWkt(wkt)
            wkb_hex = geom.asWkb().hex()
            self.dlg.set_wkb(wkb_hex)

        elif coord_type == 'GeoJSON':
            # Create GeoJSON polygon
            import json
            geojson = {
                "type": "Polygon",
                "coordinates": [[[min_lon, min_lat], [max_lon, min_lat],
                                [max_lon, max_lat], [min_lon, max_lat],
                                [min_lon, min_lat]]]
            }
            self.dlg.set_geojson(json.dumps(geojson, indent=2))

        # Show dialog again
        self.dlg.show()

        # Update rubber band preview
        self._update_rubber_band_preview()

        self.iface.mainWindow().statusBar().showMessage(
            f"Captured area: {width_m:.0f}m x {height_m:.0f}m ({width_px}x{height_px} px)", 3000
        )

    def _latlon_to_tile(self, lat, lon, z):
        """Convert lat/lon to XYZ tile coordinates."""
        n = 2.0 ** z
        x = int((lon + 180.0) / 360.0 * n)
        lat_rad = math.radians(lat)
        y = int(
            (1.0 -
             math.log(
                 math.tan(lat_rad) +
                 1.0 /
                 math.cos(lat_rad)) /
                math.pi) /
            2.0 *
            n)
        return x, y

    def _on_use_canvas(self):
        """Handle Use Canvas Extent button click."""
        min_lat, min_lon, max_lat, max_lon = get_canvas_extent(self.iface)
        coord_type = self.dlg.get_current_coord_type()

        center_lat = (min_lat + max_lat) / 2
        center_lon = (min_lon + max_lon) / 2

        if coord_type == 'Lat-Long with Pixels':
            self.dlg.set_latlon(round(center_lat, 6), round(center_lon, 6))

        elif coord_type == 'XYZ Tile':
            z = self.dlg.spinBox_z.value()
            x, y = self._latlon_to_tile(center_lat, center_lon, z)
            self.dlg.set_xyz(z, x, y)

        elif coord_type == 'Square Grid':
            self.dlg.set_grid_latlon(
                round(
                    center_lat, 6), round(
                    center_lon, 6))

        elif coord_type == 'WKT':
            wkt = f"POLYGON (({min_lon} {min_lat}, {max_lon} {min_lat}, {max_lon} {max_lat}, {min_lon} {max_lat}, {min_lon} {min_lat}))"
            self.dlg.set_wkt(wkt)

        elif coord_type == 'WKB':
            from qgis.core import QgsGeometry
            wkt = f"POLYGON (({min_lon} {min_lat}, {max_lon} {min_lat}, {max_lon} {max_lat}, {min_lon} {max_lat}, {min_lon} {min_lat}))"
            geom = QgsGeometry.fromWkt(wkt)
            wkb_hex = geom.asWkb().hex()
            self.dlg.set_wkb(wkb_hex)

        elif coord_type == 'GeoJSON':
            import json
            geojson = {
                "type": "Polygon",
                "coordinates": [[[min_lon, min_lat], [max_lon, min_lat],
                                [max_lon, max_lat], [min_lon, max_lat],
                                [min_lon, min_lat]]]
            }
            self.dlg.set_geojson(json.dumps(geojson, indent=2))

        self._update_rubber_band_preview()

        self.iface.mainWindow().statusBar().showMessage(
            "Canvas extent captured", 3000
        )

    def _update_rubber_band_preview(self):
        """Update the rubber band to show current extent."""
        try:
            params = self.dlg.get_parameters()
            if not params:
                return

            # Parse coordinates and get extent
            extent = self._get_extent_from_params(params)
            if extent:
                if not self.rubber_band:
                    self.rubber_band = ExtentRubberBand(self.iface.mapCanvas())

                self.rubber_band.set_extent(extent['extent'], 'EPSG:3857')
        except Exception: # nosec B110
            pass

    def _get_extent_from_params(self, params):
        """Parse parameters and return extent dict."""
        coord_type = params['coord_type']
        gsd = params['gsd']

        if coord_type == 'Lat-Long with Pixels':
            return parse_latlon_pixels(
                params['lat'], params['lon'],
                params['width_px'], params['height_px'], gsd
            )

        elif coord_type == 'XYZ Tile':
            return parse_xyz_tile(params['z'], params['x'], params['y'], gsd)

        elif coord_type == 'Square Grid':
            result = parse_square_grid(
                params['lat'], params['lon'],
                params['grid_size'], params['grid_unit']
            )
            # Calculate pixel dimensions from extent and GSD
            result['gsd'] = gsd
            result['width_px'] = int(result['extent'].width() / gsd)
            result['height_px'] = int(result['extent'].height() / gsd)
            return result

        elif coord_type == 'WKT':
            return parse_wkt(params['wkt'], gsd)

        elif coord_type == 'WKB':
            return parse_wkb(params['wkb'], gsd)

        elif coord_type == 'GeoJSON':
            return parse_geojson(params['geojson'], gsd)

        return None

    def _on_download(self):
        """Handle Download button click."""
        # Get parameters
        params = self.dlg.get_parameters()

        if params is None:
            QMessageBox.critical(
                self.dlg,
                "Error",
                "Invalid input parameters. Please check your coordinate values."
            )
            return

        # Validate parameters
        if not params['layer']:
            QMessageBox.critical(
                self.dlg,
                "Error",
                "Please select a layer."
            )
            return

        if not params['output_path']:
            QMessageBox.critical(
                self.dlg,
                "Error",
                "Please specify an output file path."
            )
            return

        # Set downloading state
        self.dlg.set_downloading(True)
        self.dlg.set_status("Preparing download...")
        self.dlg.set_progress(10)
        QApp.processEvents()

        try:
            # Update status
            self.dlg.set_status("Parsing coordinates...")
            self.dlg.set_progress(20)
            QApp.processEvents()

            # Get extent from parameters
            extent_data = self._get_extent_from_params(params)

            if not extent_data:
                raise Exception("Could not parse coordinate input")

            extent = extent_data['extent']
            gsd = extent_data.get('gsd', params['gsd'])
            width_px = extent_data.get('width_px', int(extent.width() / gsd))
            height_px = extent_data.get(
                'height_px', int(extent.height() / gsd))

            # Update status
            self.dlg.set_status(
                f"Rendering map ({width_px}x{height_px} px)...")
            self.dlg.set_progress(40)
            QApp.processEvents()

            # Render the map
            image = render_map(
                params['layer'], extent, width_px, height_px, 'EPSG:3857'
            )

            # Update status
            self.dlg.set_status("Exporting file...")
            self.dlg.set_progress(70)
            QApp.processEvents()

            # Export to file
            output_path = export_map(
                image,
                extent,
                gsd,
                params['output_path'],
                params['export_format'])

            # Update the output path (in case extension was added)
            params['output_path'] = output_path

            # Update status
            self.dlg.set_progress(90)
            QApp.processEvents()

            # Load as layer if checkbox is selected
            if params['load_as_layer']:
                self.dlg.set_status("Loading layer...")
                QApp.processEvents()

                layer_name = os.path.basename(params['output_path'])
                raster_layer = QgsRasterLayer(
                    params['output_path'], layer_name)
                if raster_layer.isValid():
                    QgsProject.instance().addMapLayer(raster_layer)

            # Complete!
            self.dlg.set_progress(100)
            self.dlg.set_status(f"✓ Downloaded: {os.path.basename(output_path)}")

            # Clear rubber band after successful download
            self._cleanup_rubber_band()

            self.iface.mainWindow().statusBar().showMessage(
                f"Map downloaded to: {output_path}", 5000
            )

        except Exception as e:
            self.dlg.set_progress(0)
            self.dlg.set_status(f"✗ Error: {str(e)}")
            QMessageBox.critical(
                self.dlg,
                "Error",
                f"Failed to download map:\n{str(e)}"
            )

        finally:
            # Re-enable controls
            self.dlg.set_downloading(False)

    def run(self):
        """Run method that performs all the real work."""

        # Create dialog if first time
        if self.first_start:
            self.first_start = False
            self.dlg = AdvancedMapDownloaderDialog(self.iface.mainWindow())

            # Connect dialog signals
            self.dlg.draw_from_canvas_clicked.connect(self._on_draw_canvas)
            self.dlg.use_canvas_clicked.connect(self._on_use_canvas)
            self.dlg.download_clicked.connect(self._on_download)
            self.dlg.dialog_closed.connect(self._on_dialog_closed)
            self.dlg.input_changed.connect(self._update_rubber_band_preview)

        # Refresh layers every time dialog is opened
        self.dlg.populate_layers()

        # Reset progress
        self.dlg.reset_progress()

        # Create rubber band for extent preview
        if not self.rubber_band:
            self.rubber_band = ExtentRubberBand(self.iface.mapCanvas())

        # Show the dialog (non-modal - stays on top)
        self.dlg.show()
        self.dlg.raise_()
        self.dlg.activateWindow()
