"""
Polygon-Polygon Intersection Analysis

This module provides functionality for computing geometric intersections between two
polygon datasets. It identifies all areas where polygons from two different files
overlap and creates a new dataset containing the intersection geometries.

Main Functions
--------------
intersect_polygon_with_polygon_file : Calculate intersection between two polygon datasets

Key Features
------------
- Spatial indexing for efficient intersection queries (rtree)
- Multi-format support (Shapefile, GeoJSON, GeoPackage, etc.)
- Multi-part geometry handling (MULTIPOLYGON, GEOMETRYCOLLECTION)
- Configurable spatial reference system via EPSG codes
- Geometry validation and automatic repair
- Comprehensive error handling and logging
- Progress tracking for large datasets

Use Cases
---------
1. **Overlay Analysis**: Find areas where two geographic regions overlap
2. **Land Use Planning**: Identify parcels affected by zoning changes
3. **Environmental Analysis**: Calculate overlap between protected areas and development zones
4. **Spatial Join**: Create attribute combinations from intersecting features
5. **Coverage Analysis**: Determine shared coverage between datasets
6. **Administrative Boundaries**: Find overlaps between jurisdictional regions

Technical Details
-----------------
The module uses spatial indexing to optimize intersection calculations. For N features
in the base file and M features in the new file, a naive approach requires N*M
intersection tests. Spatial indexing reduces this to approximately N*log(M) by using
bounding box tests to filter candidate pairs before performing exact geometric tests.

Spatial Index Selection:
Uses rtree (libspatialindex backend) for spatial indexing.

The intersection process:
1. Build spatial index of base polygon bounding boxes
2. For each polygon in new file, query index for candidates
3. Perform exact geometric intersection on candidates only
4. Handle multi-part results (MULTIPOLYGON, GEOMETRYCOLLECTION)
5. Create output features with tracking attributes

Performance Characteristics
---------------------------
- Time Complexity: O(N*log(M)) with spatial indexing vs O(N*M) without
- Space Complexity: O(N) for spatial index
- Typical speedup: 10-100x for datasets with >1000 features

Dependencies
------------
- GDAL/OGR: Vector I/O and geometric operations
- rtree: Spatial indexing (automatic fallback)
- numpy: Numerical operations (optional)

See Also
--------
- calculate_polygon_difference: Find areas in base but not in new dataset
- intersect_polyline_with_polygon_files: Calculate polyline-polygon intersections
- filter_vector_by_polygon: Extract features within a bounding polygon
"""

import os
import logging
from typing import Optional
from datetime import datetime
from osgeo import ogr, osr, gdal

# Import spatial indexing utility
from pyearth.toolbox.spatialindex.setup_spatial_index import setup_spatial_index

# Import vector format support utilities
from pyearth.gis.gdal.gdal_vector_format_support import (
    get_vector_driver_from_extension,
    get_vector_format_from_extension
)

# Configure logging
logger = logging.getLogger(__name__)


def intersect_polygon_with_polygon_file(
    sFilename_base: str,
    sFilename_new: str,
    sFilename_difference_out: str,
    spatial_ref_epsg: int = 4326
) -> None:
    """
    Calculate geometric intersection between two polygon datasets.

    This function identifies all areas where polygons from two input files overlap,
    creating a new dataset containing the intersection geometries. It uses spatial
    indexing to efficiently process large datasets by filtering candidates with
    bounding box tests before performing exact geometric intersections.

    The function automatically handles multi-part geometries (MULTIPOLYGON,
    GEOMETRYCOLLECTION) by decomposing them into individual polygon components.
    Each output feature includes tracking attributes linking it to the source
    polygons from both input files.

    Parameters
    ----------
    sFilename_base : str
        Absolute path to the base polygon file. Supports multiple formats including
        Shapefile (.shp), GeoJSON (.geojson, .json), GeoPackage (.gpkg), GML (.gml),
        and KML (.kml). This file is indexed for efficient spatial queries.
    sFilename_new : str
        Absolute path to the polygon file to intersect with base. Must use same
        format conventions as base file. Each polygon in this file is tested against
        candidate polygons from the base file.
    sFilename_difference_out : str
        Absolute path for the output file containing intersection polygons. Format
        determined by file extension. Parent directory must exist or will be created.
        Existing files are overwritten.
    spatial_ref_epsg : int, optional
        EPSG code for output spatial reference system. Default is 4326 (WGS84).
        Common values: 4326 (WGS84 lat/lon), 3857 (Web Mercator), 32633 (UTM 33N).
        Both input files should use compatible coordinate systems.

    Returns
    -------
    None
        Results are written to `sFilename_difference_out`. Function returns None
        upon successful completion.

    Raises
    ------
    RuntimeError
        If input files cannot be opened, spatial index creation fails, output
        file cannot be created, or geometric operations fail.
    ValueError
        If file format is not supported or file extension is invalid.
    OSError
        If file paths are invalid or directory creation fails.

    Notes
    -----
    1. **Spatial Indexing**: The function uses rtree (libspatialindex backend) for spatial indexing. This dramatically improves performance by using bounding box tests to identify candidate pairs before performing expensive exact geometric intersection tests.

    2. **Multi-Format Support**: Both input and output files can use any format
       supported by GDAL/OGR. Format is detected from file extension using
       `get_vector_driver_from_extension()`. Common formats:
       - Shapefile: .shp (requires .shx, .dbf, .prj)
       - GeoJSON: .geojson, .json
       - GeoPackage: .gpkg
       - GML: .gml
       - KML: .kml

    3. **Geometry Validation**: Invalid geometries are automatically repaired using
       the Buffer(0) technique. Geometries that cannot be repaired are skipped with
       warnings logged.

    4. **Multi-Part Handling**: Intersection results may produce MULTIPOLYGON or
       GEOMETRYCOLLECTION geometries when input polygons partially overlap. These
       are automatically decomposed into individual POLYGON features in the output.

    5. **Output Attributes**: Each output feature includes three attributes:
       - id: Sequential identifier for the intersection polygon
       - base_fid: Feature ID from base file
       - new_fid: Feature ID from new file

    6. **Coordinate Systems**: The function does not perform coordinate transformations.
       Input files should use compatible coordinate systems. The spatial_ref_epsg
       parameter only sets the output file's spatial reference metadata.

    7. **Performance**: For N base features and M new features, performance is:
       - Without spatial index: O(N*M) - tests all pairs
       - With spatial index: O(N*log(M)) - tests only candidates
       - Typical speedup: 10-100x for datasets with >1000 features

    8. **Empty Results**: If no intersections are found, an output file with empty
       layer is created. Check feature count to determine if intersections exist.

    9. **Resource Management**: All dataset handles are properly closed in a finally
       block to prevent file locks and memory leaks, even if errors occur.

    10. **Progress Tracking**: Processing progress is logged every 100 features,
        useful for monitoring long-running operations on large datasets.

    Examples
    --------
    Basic usage with GeoJSON files:

    >>> intersect_polygon_with_polygon_file(
    ...     sFilename_base='/data/parcels.geojson',
    ...     sFilename_new='/data/zoning.geojson',
    ...     sFilename_difference_out='/output/parcel_zoning_intersect.geojson'
    ... )
    # Output: parcel_zoning_intersect.geojson with polygons showing where parcels
    # overlap with zoning districts

    Using Shapefiles with custom spatial reference:

    >>> intersect_polygon_with_polygon_file(
    ...     sFilename_base='/data/protected_areas.shp',
    ...     sFilename_new='/data/development_zones.shp',
    ...     sFilename_difference_out='/output/conflicts.shp',
    ...     spatial_ref_epsg=3857  # Web Mercator
    ... )
    # Output: conflicts.shp showing areas where protected areas overlap with
    # development zones, using Web Mercator projection

    Finding administrative boundary overlaps:

    >>> intersect_polygon_with_polygon_file(
    ...     sFilename_base='/data/counties.gpkg',
    ...     sFilename_new='/data/watersheds.gpkg',
    ...     sFilename_difference_out='/output/county_watershed_overlap.gpkg'
    ... )
    # Output: county_watershed_overlap.gpkg with intersection polygons
    # Attributes include base_fid (county ID) and new_fid (watershed ID)

    Expected log output for successful processing:
    ```
    INFO: GDAL version: 3.4.1
    INFO: Using rtree for spatial indexing
    INFO: Input formats - Base: GeoJSON, New: GeoJSON, Output: GeoJSON
    INFO: Removed existing output file: /output/intersect.geojson
    INFO: Using spatial reference EPSG:4326
    INFO: Base file contains 523 polygon features
    INFO: Polygon file contains 89 polygon features
    INFO: Building spatial index for base features...
    INFO: Indexed 523 valid polygon features
    INFO: Processing intersections...
    INFO: Processed 100/89 polygon features
    INFO: Generated 1847 intersection polygons
    INFO: Processing completed in 12.45 seconds (0.21 minutes)
    ```

    See Also
    --------
    calculate_polygon_difference : Find areas in base not overlapping with new dataset
    intersect_polyline_with_polygon_files : Calculate polyline-polygon intersections
    filter_vector_by_polygon : Extract features within a bounding polygon
    setup_spatial_index : Automatic spatial indexing library selection
    """
    # Record start time for performance tracking
    start_time = datetime.now()

    # Set up spatial indexing library (rtree with automatic fallback)
    logger.info(f"GDAL version: {gdal.__version__}")
    RTreeClass = setup_spatial_index()
    logger.info(f"Using rtree for spatial indexing")

    # Validate input files exist
    if not os.path.exists(sFilename_base):
        raise FileNotFoundError(f"Base file not found: {sFilename_base}")
    if not os.path.exists(sFilename_new):
        raise FileNotFoundError(f"New file not found: {sFilename_new}")

    # Create output directory if needed
    output_dir = os.path.dirname(sFilename_difference_out)
    if output_dir and not os.path.exists(output_dir):
        os.makedirs(output_dir)
        logger.info(f"Created output directory: {output_dir}")

    try:
        # Get drivers based on file extensions
        try:
            driver_base = get_vector_driver_from_extension(sFilename_base)
            driver_new = get_vector_driver_from_extension(sFilename_new)
            driver_out = get_vector_driver_from_extension(sFilename_difference_out)

            format_base = get_vector_format_from_extension(sFilename_base)
            format_new = get_vector_format_from_extension(sFilename_new)
            format_out = get_vector_format_from_extension(sFilename_difference_out)

            logger.info(f"Input formats - Base: {format_base}, New: {format_new}, Output: {format_out}")
        except ValueError as e:
            raise RuntimeError(f"Unsupported file format: {e}")

        # Remove existing output file to ensure clean write
        if os.path.exists(sFilename_difference_out):
            if format_out == 'ESRI Shapefile':
                # Remove all shapefile component files
                base_name = os.path.splitext(sFilename_difference_out)[0]
                for ext in ['.shp', '.shx', '.dbf', '.prj', '.cpg']:
                    if os.path.exists(base_name + ext):
                        os.remove(base_name + ext)
            else:
                os.remove(sFilename_difference_out)
            logger.info(f"Removed existing output file: {sFilename_difference_out}")

        # Create output dataset
        pDataset_out = driver_out.CreateDataSource(sFilename_difference_out)
        if pDataset_out is None:
            raise RuntimeError(f"Could not create output file: {sFilename_difference_out}")

        # Configure spatial reference system for output
        pSpatial_reference_gcs = osr.SpatialReference()
        pSpatial_reference_gcs.ImportFromEPSG(spatial_ref_epsg)
        logger.info(f"Using spatial reference EPSG:{spatial_ref_epsg}")

        # Create output layer with polygon geometry type
        pLayerOut = pDataset_out.CreateLayer('intersection', pSpatial_reference_gcs, ogr.wkbPolygon)
        if pLayerOut is None:
            raise RuntimeError("Could not create output layer")

        # Define output attribute schema
        pLayerOut.CreateField(ogr.FieldDefn('id', ogr.OFTInteger64))
        pLayerOut.CreateField(ogr.FieldDefn('base_fid', ogr.OFTInteger64))
        pLayerOut.CreateField(ogr.FieldDefn('new_fid', ogr.OFTInteger64))

        pLayerDefn = pLayerOut.GetLayerDefn()

        # Open base polygon dataset
        pDataset_base = driver_base.Open(sFilename_base, 0)
        if pDataset_base is None:
            raise RuntimeError(f"Could not open base file: {sFilename_base}")

        pLayer_base = pDataset_base.GetLayer()
        if pLayer_base is None:
            raise RuntimeError("Could not access layer in base file")

        nFeature_base = pLayer_base.GetFeatureCount()
        if nFeature_base == 0:
            logger.warning("Base file contains no features")
            return

        logger.info(f"Base file contains {nFeature_base} polygon features")

        # Open new polygon dataset
        pDataset_new = driver_new.Open(sFilename_new, 0)
        if pDataset_new is None:
            raise RuntimeError(f"Could not open new file: {sFilename_new}")

        pLayer_new = pDataset_new.GetLayer()
        if pLayer_new is None:
            raise RuntimeError("Could not access layer in new file")

        nFeature_new = pLayer_new.GetFeatureCount()
        if nFeature_new == 0:
            logger.warning("New file contains no features")
            return

        logger.info(f"Polygon file contains {nFeature_new} polygon features")

        # Build spatial index for base polygon features
        logger.info("Building spatial index for base features...")

        index_base = RTreeClass()

        base_features = {}  # Cache base features
        indexed_count = 0

        for idx, pFeature_base in enumerate(pLayer_base):
            fid = pFeature_base.GetFID()
            pGeometry_base = pFeature_base.GetGeometryRef()

            if pGeometry_base is None:
                logger.warning(f"Base feature {fid} has no geometry, skipping")
                continue

            # Validate geometry
            if not pGeometry_base.IsValid():
                logger.warning(f"Base feature {fid} has invalid geometry, attempting to fix")
                pGeometry_base = pGeometry_base.Buffer(0)

            if pGeometry_base is None or pGeometry_base.IsEmpty():
                logger.warning(f"Base feature {fid} has empty geometry after validation, skipping")
                continue

            try:
                # Get bounding box
                envelope = pGeometry_base.GetEnvelope()
                left, right, bottom, top = envelope

                # Insert into spatial index
                pBound = (left, bottom, right, top)
                index_base.insert(fid, pBound)


                # Cache feature
                base_features[fid] = pFeature_base.Clone()
                indexed_count += 1
            except Exception as e:
                logger.error(f"Error indexing base feature {fid}: {e}")
                continue

        logger.info(f"Indexed {indexed_count} valid polygon features")

        if not base_features:
            logger.warning("No valid base features found for indexing")
            return

        # Process new polygon features and calculate intersections
        logger.info("Processing intersections...")
        lID_polygon = 1
        processed_features = 0

        for idx, pFeature_new in enumerate(pLayer_new):
            processed_features += 1
            if processed_features % 100 == 0:
                logger.info(f"Processed {processed_features}/{nFeature_new} polygon features")

            new_fid = pFeature_new.GetFID()
            pGeometry_new = pFeature_new.GetGeometryRef()

            if pGeometry_new is None:
                logger.warning(f"Polygon feature {new_fid} has no geometry, skipping")
                continue

            # Validate geometry
            if not pGeometry_new.IsValid():
                logger.warning(f"Polygon feature {new_fid} has invalid geometry, attempting to fix")
                pGeometry_new = pGeometry_new.Buffer(0)

            if pGeometry_new is None or pGeometry_new.IsEmpty():
                logger.warning(f"Polygon feature {new_fid} has empty geometry after validation, skipping")
                continue

            try:
                # Get bounding box for spatial query
                envelope = pGeometry_new.GetEnvelope()
                left, right, bottom, top = envelope

                # Query spatial index to find candidate polygons

                pBound = (left, bottom, right, top)
                aIntersect = list(index_base.intersection(pBound))

                # Process each candidate base polygon feature
                for base_fid in aIntersect:
                    if base_fid not in base_features:
                        continue

                    pFeature_base = base_features[base_fid]
                    pGeometry_base = pFeature_base.GetGeometryRef()

                    try:
                        # Perform exact geometric intersection test
                        if not pGeometry_new.Intersects(pGeometry_base):
                            continue

                        # Calculate geometric intersection
                        pGeometry_intersect = pGeometry_new.Intersection(pGeometry_base)

                        # Skip if intersection is empty
                        if pGeometry_intersect is None or pGeometry_intersect.IsEmpty():
                            continue

                        # Handle output based on geometry type
                        pGeometrytype_intersect = pGeometry_intersect.GetGeometryName()

                        if pGeometrytype_intersect == 'POLYGON':
                            # Simple polygon result
                            pFeatureOut = ogr.Feature(pLayerDefn)
                            pFeatureOut.SetGeometry(pGeometry_intersect)
                            pFeatureOut.SetField("id", lID_polygon)
                            pFeatureOut.SetField("base_fid", base_fid)
                            pFeatureOut.SetField("new_fid", new_fid)
                            pLayerOut.CreateFeature(pFeatureOut)
                            lID_polygon += 1

                        elif pGeometrytype_intersect in ['MULTIPOLYGON', 'GEOMETRYCOLLECTION']:
                            # Multi-part geometry - decompose into individual polygons
                            for i in range(pGeometry_intersect.GetGeometryCount()):
                                pGeometry_part = pGeometry_intersect.GetGeometryRef(i)
                                if pGeometry_part is not None and not pGeometry_part.IsEmpty():
                                    part_type = pGeometry_part.GetGeometryName()
                                    if part_type == 'POLYGON':
                                        pFeatureOut = ogr.Feature(pLayerDefn)
                                        pFeatureOut.SetGeometry(pGeometry_part)
                                        pFeatureOut.SetField("id", lID_polygon)
                                        pFeatureOut.SetField("base_fid", base_fid)
                                        pFeatureOut.SetField("new_fid", new_fid)
                                        pLayerOut.CreateFeature(pFeatureOut)
                                        lID_polygon += 1

                    except Exception as e:
                        logger.error(f"Error processing intersection between base {base_fid} and polygon {new_fid}: {e}")
                        continue

            except Exception as e:
                logger.error(f"Error processing polygon feature {new_fid}: {e}")
                continue

        logger.info(f"Generated {lID_polygon - 1} intersection polygons")

    except Exception as e:
        logger.error(f"Error during processing: {e}")
        raise RuntimeError(f"Failed to calculate polygon-polygon intersection: {e}")

    finally:
        # Clean up resources
        if 'pDataset_base' in locals():
            pDataset_base = None
        if 'pDataset_new' in locals():
            pDataset_new = None
        if 'pDataset_out' in locals():
            pDataset_out = None

    # Report timing
    end_time = datetime.now()
    delta = end_time - start_time
    seconds = delta.total_seconds()
    minutes = seconds / 60

    logger.info(f"Processing completed in {seconds:.2f} seconds ({minutes:.2f} minutes)")

    return None


