/**
 * PolygonDivideInteraction
 *
 * A three-phase OpenLayers interaction for dividing a polygon into N
 * equal-area pieces:
 *   Phase 1 – SELECT: hover to highlight, click to select a polygon
 *   Phase 2 – EDGE:   hover to highlight edges, click to pick the divide
 *                      direction (cuts will be perpendicular to this edge)
 *   Phase 3 – FORM:   wait for the popup form to call performDivide(n)
 *
 * After a successful divide the original feature is removed and N new
 * coloured features are added.  The interaction fires `beforedivide` and
 * `afterdivide` events compatible with ol-ext's UndoRedo.
 */

import ol_interaction_Interaction from 'ol/interaction/Interaction';
import VectorSource from 'ol/source/Vector';
import VectorLayer from 'ol/layer/Vector';
import Feature from 'ol/Feature';
import { Style, Stroke, Fill } from 'ol/style';
import { LineString, Polygon as PolygonGeom } from 'ol/geom';
import { dividePolygon } from '../geom/polygonDivide.js';
import { showToast } from '../toast.js';

// Highlight style for the selected polygon (phase 1)
const HIGHLIGHT_STYLE = new Style({
  stroke: new Stroke({ color: '#0ea5e9', width: 3 }),
  fill: new Fill({ color: 'rgba(14,165,233,0.15)' }),
});

// Style for the hovered edge (phase 2)
const EDGE_STYLE = new Style({
  stroke: new Stroke({ color: '#8b5cf6', width: 4, lineDash: [10, 6] }),
});

/**
 * Generate N visually distinct colours using evenly-spaced HSL hues.
 */
function pieceColors(n) {
  const colors = [];
  for (let i = 0; i < n; i++) {
    const hue = Math.round((i * 360) / n);
    colors.push({
      stroke: `hsl(${hue}, 70%, 45%)`,
      fill: `hsla(${hue}, 70%, 55%, 0.25)`,
    });
  }
  return colors;
}

export class PolygonDivideInteraction extends ol_interaction_Interaction {
  /**
   * @param {Object} options
   * @param {VectorSource|VectorSource[]} [options.sources]  Specific sources
   *   to search.  If omitted the interaction searches all visible vector layers.
   * @param {number} [options.snapDistance=25]  Pixel distance for hover.
   */
  constructor(options = {}) {
    super({
      handleEvent: (e) => this._handleEvent(e),
    });

    this.snapDistance_ = options.snapDistance || 25;
    this._sources = options.sources
      ? (Array.isArray(options.sources) ? options.sources : [options.sources])
      : null;

    // Phase: 'select' | 'edge' | 'form' | 'pick'
    this._phase = 'select';
    this._selectedFeature = null;
    this._selectedSource = null;
    this._selectedEdge = null; // [p0, p1] — the edge the user clicked
    this._dividedFeatures = null; // features created after divide (for pick phase)

    // Overlay layer for polygon highlight
    this._overlaySource = new VectorSource({ useSpatialIndex: false });
    this._overlayLayer = new VectorLayer({
      source: this._overlaySource,
      displayInLayerSwitcher: false,
      style: HIGHLIGHT_STYLE,
    });

    // Overlay layer for edge highlight
    this._edgeSource = new VectorSource({ useSpatialIndex: false });
    this._edgeLayer = new VectorLayer({
      source: this._edgeSource,
      displayInLayerSwitcher: false,
      style: EDGE_STYLE,
    });
  }

  /* ------------------------------------------------------------------ */
  /*  Map lifecycle                                                      */
  /* ------------------------------------------------------------------ */

  setMap(map) {
    if (this.getMap()) {
      this.getMap().removeLayer(this._overlayLayer);
      this.getMap().removeLayer(this._edgeLayer);
    }
    super.setMap(map);
    if (map) {
      this._overlayLayer.setMap(map);
      this._edgeLayer.setMap(map);
    }
  }

  setActive(active) {
    super.setActive(active);
    if (!active) {
      this._reset();
    }
  }

  /* ------------------------------------------------------------------ */
  /*  Source helpers                                                      */
  /* ------------------------------------------------------------------ */

  _getSources() {
    if (this._sources) return this._sources;
    if (!this.getMap()) return [];
    const sources = [];
    const collect = (layers) => {
      layers.forEach((layer) => {
        if (layer.getVisible()) {
          if (layer.getSource && layer.getSource() instanceof VectorSource) {
            sources.push(layer.getSource());
          } else if (layer.getLayers) {
            collect(layer.getLayers());
          }
        }
      });
    };
    collect(this.getMap().getLayers());
    return sources;
  }

  /* ------------------------------------------------------------------ */
  /*  Event router                                                       */
  /* ------------------------------------------------------------------ */

  _handleEvent(e) {
    if (!this.getActive()) return true;

    // Escape cancels at any phase
    if (e.type === 'keydown' && e.originalEvent?.key === 'Escape') {
      if (this._phase === 'form') {
        this.cancelDivide();
      } else {
        this._reset();
      }
      return false;
    }

    if (this._phase === 'select') {
      if (e.type === 'pointermove') return this._onSelectMove(e);
      if (e.type === 'singleclick') return this._onSelectClick(e);
    }

    if (this._phase === 'edge') {
      if (e.type === 'pointermove') return this._onEdgeMove(e);
      if (e.type === 'singleclick') return this._onEdgeClick(e);
    }

    if (this._phase === 'pick') {
      if (e.type === 'pointermove') return this._onPickMove(e);
      if (e.type === 'singleclick') return this._onPickClick(e);
    }

    return true;
  }

  /* ------------------------------------------------------------------ */
  /*  Phase 1: SELECT polygon                                            */
  /* ------------------------------------------------------------------ */

  _onSelectMove(e) {
    const map = this.getMap();
    if (!map) return true;

    this._overlaySource.clear();

    const hit = this._closestPolygon(e);
    if (hit) {
      const clone = hit.feature.clone();
      this._overlaySource.addFeature(clone);
      map.getTargetElement().style.cursor = 'pointer';
    } else {
      map.getTargetElement().style.cursor = '';
    }
    return true;
  }

  _onSelectClick(e) {
    const hit = this._closestPolygon(e);
    if (!hit) return true;

    this._selectedFeature = hit.feature;
    this._selectedSource = hit.source;

    // Keep polygon highlight visible during edge phase
    this._overlaySource.clear();
    const clone = hit.feature.clone();
    clone.set('_permanent', true);
    this._overlaySource.addFeature(clone);

    this._phase = 'edge';
    showToast('Click the edge to divide along.', 'info', 3000);
    return false;
  }

  _closestPolygon(e) {
    let best = null;
    let bestDist = this.snapDistance_ + 1;

    for (const source of this._getSources()) {
      const feat = source.getClosestFeatureToCoordinate(e.coordinate);
      if (!feat) continue;
      const geom = feat.getGeometry();
      if (!geom) continue;
      const type = geom.getType();
      if (type !== 'Polygon' && type !== 'MultiPolygon') continue;

      const closest = geom.getClosestPoint(e.coordinate);
      const line = new LineString([e.coordinate, closest]);
      const distPx = line.getLength() / e.frameState.viewState.resolution;

      if (distPx < bestDist) {
        bestDist = distPx;
        best = { feature: feat, source };
      }
    }
    return best;
  }

  /* ------------------------------------------------------------------ */
  /*  Phase 2: EDGE selection                                            */
  /* ------------------------------------------------------------------ */

  _onEdgeMove(e) {
    const map = this.getMap();
    if (!map) return true;

    this._edgeSource.clear();

    const edge = this._closestEdgeSegment(this._selectedFeature, e);
    if (edge) {
      const edgeFeat = new Feature(new LineString([edge.segStart, edge.segEnd]));
      this._edgeSource.addFeature(edgeFeat);
      map.getTargetElement().style.cursor = 'crosshair';
    } else {
      map.getTargetElement().style.cursor = '';
    }
    return true;
  }

  _onEdgeClick(e) {
    const edge = this._closestEdgeSegment(this._selectedFeature, e);
    if (!edge) return true;

    this._selectedEdge = [edge.segStart, edge.segEnd];
    this._edgeSource.clear();

    this._phase = 'form';

    // Dispatch divideform so MapView can show the popup
    const geom = this._selectedFeature.getGeometry();
    const ext = geom.getExtent();
    const center = [(ext[0] + ext[2]) / 2, (ext[1] + ext[3]) / 2];

    this.dispatchEvent({
      type: 'divideform',
      feature: this._selectedFeature,
      source: this._selectedSource,
      coordinate: center,
    });

    return false;
  }

  /**
   * Find the closest edge segment of a polygon feature to the cursor.
   */
  _closestEdgeSegment(feature, e) {
    const geom = feature.getGeometry();
    let ring;
    if (geom.getType() === 'Polygon') {
      ring = geom.getCoordinates()[0];
    } else if (geom.getType() === 'MultiPolygon') {
      ring = geom.getCoordinates()[0][0];
    } else {
      return null;
    }

    const resolution = e.frameState.viewState.resolution;
    let bestDist = Infinity;
    let bestSeg = null;
    const n = ring.length - 1;

    for (let i = 0; i < n; i++) {
      const a = ring[i];
      const b = ring[i + 1];
      const dx = b[0] - a[0], dy = b[1] - a[1];
      const lenSq = dx * dx + dy * dy;
      if (lenSq < 1e-20) continue;

      let t = ((e.coordinate[0] - a[0]) * dx + (e.coordinate[1] - a[1]) * dy) / lenSq;
      t = Math.max(0, Math.min(1, t));
      const projX = a[0] + t * dx, projY = a[1] + t * dy;
      const distPx = Math.sqrt((e.coordinate[0] - projX) ** 2 + (e.coordinate[1] - projY) ** 2) / resolution;

      if (distPx < bestDist) {
        bestDist = distPx;
        bestSeg = { segStart: a, segEnd: b };
      }
    }
    return bestDist <= this.snapDistance_ ? bestSeg : null;
  }

  /* ------------------------------------------------------------------ */
  /*  Phase 3: FORM — called externally by the popup                     */
  /* ------------------------------------------------------------------ */

  /**
   * Divide the selected polygon into `n` equal-area pieces.
   * Called by the MapView popup's Confirm handler.
   *
   * @param {number} n  Number of pieces (>= 2)
   */
  performDivide(n) {
    if (this._phase !== 'form' || !this._selectedFeature) return;

    const feature = this._selectedFeature;
    const source = this._selectedSource;
    const geom = feature.getGeometry();

    let polygonCoords;
    if (geom.getType() === 'Polygon') {
      polygonCoords = geom.getCoordinates();
    } else if (geom.getType() === 'MultiPolygon') {
      polygonCoords = geom.getCoordinates()[0];
    }

    const result = dividePolygon(polygonCoords, n, this._selectedEdge);

    if (!result.pieces) {
      showToast(result.error || 'Division failed.', 'error', 5000);
      this._reset();
      return;
    }

    // Create N new coloured features
    const colors = pieceColors(n);
    const newFeatures = result.pieces.map((coords, i) => {
      const f = feature.clone();
      f.setGeometry(new PolygonGeom(coords));
      f.setStyle(new Style({
        stroke: new Stroke({ color: colors[i].stroke, width: 2.5 }),
        fill: new Fill({ color: colors[i].fill }),
      }));
      return f;
    });

    // Dispatch beforedivide (UndoRedo compatible)
    const evtData = {
      type: 'beforedivide',
      original: feature,
      features: newFeatures,
    };
    this.dispatchEvent(evtData);
    source.dispatchEvent({ ...evtData });

    // Replace original with pieces
    source.removeFeature(feature);
    for (const f of newFeatures) {
      source.addFeature(f);
    }

    // Dispatch afterdivide
    const afterEvt = {
      type: 'afterdivide',
      original: feature,
      features: newFeatures,
    };
    this.dispatchEvent(afterEvt);
    source.dispatchEvent({ ...afterEvt });

    // If original was a parcel, enter pick phase for UPN assignment
    const isParcel = feature.get('_layerType') === 'parcel';
    if (isParcel) {
      this._dividedFeatures = newFeatures;
      this._phase = 'pick';
      showToast('Click the polygon that should keep the original identifier.', 'info', 5000);

      this.dispatchEvent({
        type: 'dividedparcel',
        features: newFeatures,
        originalProps: feature.getProperties(),
        source,
      });
    } else {
      showToast(`Polygon divided into ${n} equal pieces.`, 'success');
      this._reset();
    }
  }

  /* ------------------------------------------------------------------ */
  /*  Phase 4: PICK — select which piece keeps the UPN                   */
  /* ------------------------------------------------------------------ */

  _onPickMove(e) {
    const map = this.getMap();
    if (!map) return true;

    this._overlaySource.clear();

    // Highlight whichever divided piece is under the cursor
    const hit = this._closestDividedPiece(e);
    if (hit) {
      const clone = hit.clone();
      this._overlaySource.addFeature(clone);
      map.getTargetElement().style.cursor = 'pointer';
    } else {
      map.getTargetElement().style.cursor = '';
    }
    return true;
  }

  _onPickClick(e) {
    const hit = this._closestDividedPiece(e);
    if (!hit) return true;

    this.dispatchEvent({
      type: 'dividepick',
      picked: hit,
      features: this._dividedFeatures,
    });

    this._reset();
    return false;
  }

  /**
   * Find the closest divided piece to the cursor.
   */
  _closestDividedPiece(e) {
    if (!this._dividedFeatures) return null;
    let best = null;
    let bestDist = this.snapDistance_ + 1;

    for (const feat of this._dividedFeatures) {
      const geom = feat.getGeometry();
      if (!geom) continue;
      const closest = geom.getClosestPoint(e.coordinate);
      const line = new LineString([e.coordinate, closest]);
      const distPx = line.getLength() / e.frameState.viewState.resolution;
      if (distPx < bestDist) {
        bestDist = distPx;
        best = feat;
      }
    }
    return best;
  }

  /**
   * Cancel the divide operation and return to select phase.
   * Called by the MapView popup's Cancel handler.
   */
  cancelDivide() {
    this.dispatchEvent({ type: 'dividecancel' });
    this._reset();
  }

  /* ------------------------------------------------------------------ */
  /*  Reset                                                              */
  /* ------------------------------------------------------------------ */

  _reset() {
    this._phase = 'select';
    this._selectedFeature = null;
    this._selectedSource = null;
    this._selectedEdge = null;
    this._dividedFeatures = null;
    this._overlaySource.clear();
    this._edgeSource.clear();

    const map = this.getMap();
    if (map) {
      map.getTargetElement().style.cursor = '';
    }
  }
}
