//! Parcel data model and subdivision/edit entry points (spec §3, §4).
//!
//! A [`Parcel`] is a simple polygon with exactly one frontage edge
//! coincident with a road segment (invariant **I2**). Parcels are
//! grouped by their containing block (face), and the whole crate's
//! public output is a [`ParcelSet`].
//!
//! Subdivision and edit operations are exposed via the free
//! functions [`subdivide_all`] and [`apply_road_edit`].

pub mod classify;
pub mod deform;
pub mod regularize;
pub mod subdivide;

use std::collections::HashMap;
use std::fmt;

use glam::DVec2;
use slotmap::{new_key_type, SlotMap};

use crate::geometry::{Polygon, EPS_GEOM};
use crate::network::graph::FaceId;
use crate::network::RoadId;

pub use deform::{apply_road_edit, EditOutcome, RoadEdit};
pub use subdivide::{subdivide_all, subdivide_all_with_stats, SubdivisionStats};

new_key_type! {
    /// Stable identifier for a [`Parcel`].
    pub struct ParcelId;
    /// Stable identifier for a vertex in the shared-vertex registry.
    /// Vertices that coincide within `EPS_GEOM` resolve to the same
    /// `VertexId`, so adjacent parcels' boundaries can never drift
    /// apart — moving a vertex via the registry writes through to
    /// every parcel that references it. (See journal D17.)
    pub struct VertexId;
}

#[derive(Debug, Clone)]
pub(crate) struct VertexRecord {
    pub pos: DVec2,
    /// Every parcel that references this vertex, with the index into
    /// that parcel's polygon ring.
    pub refs: Vec<(ParcelId, usize)>,
}

/// Kind of a parcel boundary edge (spec §3.2).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum EdgeKind {
    /// Edge lies coincident with a road segment.
    Frontage,
    /// Edge is adjacent to the frontage edge in the parcel ring.
    Side,
    /// All other edges.
    Back,
}

/// Trait that downstream code implements for buildings to participate
/// in deform-time fit checks (spec §4.6).
pub trait BuildingFitCheck: 'static {
    /// True iff the building still fits inside `parcel`.
    fn fits_in(&self, parcel: &Parcel) -> bool;
}

/// Owning wrapper around a `BuildingFitCheck`. Stored on parcels via
/// `Option<BuildingHandle>`.
///
/// Note: the spec describes `BuildingHandle` as "opaque". This crate
/// implements it as a heap-allocated trait object so that the
/// deformation pipeline can call `fits_in` without taking a callback
/// from the user. Recorded as revision §11 (R1).
pub struct BuildingHandle {
    inner: Box<dyn BuildingFitCheck>,
}

impl BuildingHandle {
    /// Wrap a building implementation.
    #[must_use]
    pub fn new<B: BuildingFitCheck>(b: B) -> Self {
        Self { inner: Box::new(b) }
    }

    /// Forward to the inner [`BuildingFitCheck::fits_in`].
    #[must_use]
    pub fn fits_in(&self, parcel: &Parcel) -> bool {
        self.inner.fits_in(parcel)
    }
}

impl fmt::Debug for BuildingHandle {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("BuildingHandle").finish_non_exhaustive()
    }
}

/// A single parcel.
#[derive(Debug)]
pub struct Parcel {
    pub(crate) polygon: Polygon,
    /// Per-vertex shared-registry ids, parallel to `polygon.vertices()`.
    /// Two parcels that share a boundary vertex hold the same
    /// `VertexId` for that position; mutations via
    /// `ParcelSet::move_vertex` propagate to every referrer.
    pub(crate) vertex_ids: Vec<VertexId>,
    pub(crate) edge_kinds: Vec<EdgeKind>,
    pub(crate) frontage_road: RoadId,
    pub(crate) frontage_edge_index: usize,
    pub(crate) block: FaceId,
    pub(crate) building: Option<BuildingHandle>,
}

impl Parcel {
    /// Vertex ring of the parcel polygon (CCW, not closed).
    #[must_use]
    pub fn vertices(&self) -> &[DVec2] {
        self.polygon.vertices()
    }

    /// Polygon view.
    #[must_use]
    pub fn polygon(&self) -> &Polygon {
        &self.polygon
    }

    /// Edge classification, parallel to `vertices()`.
    #[must_use]
    pub fn edge_kinds(&self) -> &[EdgeKind] {
        &self.edge_kinds
    }

    /// The single road this parcel faces (invariant **I2**).
    #[must_use]
    pub fn frontage_road(&self) -> RoadId {
        self.frontage_road
    }

    /// Endpoints of the frontage edge.
    #[must_use]
    pub fn frontage_edge(&self) -> (DVec2, DVec2) {
        let v = self.polygon.vertices();
        let i = self.frontage_edge_index;
        (v[i], v[(i + 1) % v.len()])
    }

    /// Frontage length, meters.
    #[must_use]
    pub fn frontage_length(&self) -> f64 {
        let (a, b) = self.frontage_edge();
        (b - a).length()
    }

    /// Parcel area, m².
    #[must_use]
    pub fn area(&self) -> f64 {
        self.polygon.area()
    }

    /// Attach a building. Returns the previously-attached building, if
    /// any.
    pub fn attach_building(&mut self, b: BuildingHandle) -> Option<BuildingHandle> {
        self.building.replace(b)
    }

    /// Detach (and return) the currently attached building, if any.
    pub fn detach_building(&mut self) -> Option<BuildingHandle> {
        self.building.take()
    }

    /// Whether a building is currently attached.
    #[must_use]
    pub fn has_building(&self) -> bool {
        self.building.is_some()
    }
}

/// The output of `subdivide_all`: a stable-ID indexed collection of
/// parcels, organized by their containing block.
#[derive(Debug, Default)]
pub struct ParcelSet {
    pub(crate) parcels: SlotMap<ParcelId, Parcel>,
    pub(crate) by_block: HashMap<FaceId, Vec<ParcelId>>,
    pub(crate) by_road: HashMap<RoadId, Vec<ParcelId>>,
    /// Shared-vertex registry. Two coincident polygon vertices (within
    /// `EPS_GEOM`) resolve to the same `VertexId` and become one
    /// physical vertex shared by every parcel that touches it.
    pub(crate) vertices: SlotMap<VertexId, VertexRecord>,
    /// Spatial index on `vertices` keyed by an `EPS_GEOM`-rounded
    /// integer grid; used to find an existing matching vertex on
    /// insertion in O(1) average.
    pub(crate) vertex_grid: HashMap<(i64, i64), Vec<VertexId>>,
}

impl ParcelSet {
    /// Number of parcels.
    #[must_use]
    pub fn len(&self) -> usize {
        self.parcels.len()
    }

    /// True if there are no parcels.
    #[must_use]
    pub fn is_empty(&self) -> bool {
        self.parcels.is_empty()
    }

    /// Iterate over `(ParcelId, &Parcel)` pairs.
    pub fn iter(&self) -> impl Iterator<Item = (ParcelId, &Parcel)> {
        self.parcels.iter()
    }

    /// Look up a parcel by id.
    #[must_use]
    pub fn get(&self, id: ParcelId) -> Option<&Parcel> {
        self.parcels.get(id)
    }

    /// Look up a parcel mutably by id.
    pub fn get_mut(&mut self, id: ParcelId) -> Option<&mut Parcel> {
        self.parcels.get_mut(id)
    }

    /// Iterate over the parcel ids within a single road's frontage.
    pub fn parcels_on_road(&self, road: RoadId) -> impl Iterator<Item = ParcelId> + '_ {
        self.by_road
            .get(&road)
            .into_iter()
            .flat_map(|v| v.iter().copied())
    }

    /// Position of a registered vertex.
    #[must_use]
    pub fn vertex_position(&self, vid: VertexId) -> Option<DVec2> {
        self.vertices.get(vid).map(|r| r.pos)
    }

    /// All `(parcel_id, vertex_index)` pairs that reference this
    /// vertex. Useful for "what touches this corner?" queries and
    /// for the deform pipeline.
    #[must_use]
    pub fn vertex_refs(&self, vid: VertexId) -> &[(ParcelId, usize)] {
        self.vertices.get(vid).map_or(&[][..], |r| &r.refs)
    }

    /// Move a registered vertex to a new position. The change is
    /// written through to every parcel referencing this vertex,
    /// so adjacent parcels' shared boundaries stay in lockstep.
    ///
    /// **Caution:** this does not validate that the resulting parcel
    /// polygons remain simple (I1) — that's the caller's
    /// responsibility, since validity depends on which combinations
    /// of vertices move together. The deform pipeline checks each
    /// affected parcel after committing a batch of moves.
    pub fn move_vertex(&mut self, vid: VertexId, new_pos: DVec2) {
        let refs = match self.vertices.get_mut(vid) {
            Some(r) => {
                r.pos = new_pos;
                r.refs.clone()
            }
            None => return,
        };
        for (pid, idx) in refs {
            if let Some(p) = self.parcels.get_mut(pid) {
                p.polygon.set_vertex_unchecked(idx, new_pos);
            }
        }
    }

    /// Find an existing registered vertex within `EPS_GEOM` of
    /// `pos`, or create a new one. Lookup uses an `EPS_GEOM`-rounded
    /// 3×3 spatial-hash neighborhood.
    fn find_or_create_vertex(&mut self, pos: DVec2) -> VertexId {
        let key = vertex_key(pos);
        for dx in -1..=1 {
            for dy in -1..=1 {
                let bucket = (key.0 + dx, key.1 + dy);
                if let Some(ids) = self.vertex_grid.get(&bucket) {
                    for &vid in ids {
                        if let Some(rec) = self.vertices.get(vid) {
                            if (rec.pos - pos).length_squared() < EPS_GEOM * EPS_GEOM {
                                return vid;
                            }
                        }
                    }
                }
            }
        }
        let id = self.vertices.insert(VertexRecord {
            pos,
            refs: Vec::new(),
        });
        self.vertex_grid.entry(key).or_default().push(id);
        id
    }

    pub(crate) fn insert(&mut self, mut parcel: Parcel) -> ParcelId {
        let block = parcel.block;
        let road = parcel.frontage_road;
        // Build vertex_ids by snapping each polygon vertex to the
        // shared registry.
        let n = parcel.polygon.len();
        let mut vids = Vec::with_capacity(n);
        for v in parcel.polygon.vertices() {
            vids.push(self.find_or_create_vertex(*v));
        }
        parcel.vertex_ids = vids.clone();
        let id = self.parcels.insert(parcel);
        for (idx, &vid) in vids.iter().enumerate() {
            if let Some(rec) = self.vertices.get_mut(vid) {
                rec.refs.push((id, idx));
            }
        }
        self.by_block.entry(block).or_default().push(id);
        self.by_road.entry(road).or_default().push(id);
        id
    }

    /// Remove a parcel and unregister its vertices. Empty
    /// `VertexRecord`s are left in place for now (they get garbage
    /// collected lazily; cheap to leave around).
    pub(crate) fn remove(&mut self, pid: ParcelId) -> Option<Parcel> {
        let parcel = self.parcels.remove(pid)?;
        if let Some(v) = self.by_block.get_mut(&parcel.block) {
            v.retain(|&x| x != pid);
        }
        if let Some(v) = self.by_road.get_mut(&parcel.frontage_road) {
            v.retain(|&x| x != pid);
        }
        for &vid in &parcel.vertex_ids {
            if let Some(rec) = self.vertices.get_mut(vid) {
                rec.refs.retain(|&(p, _)| p != pid);
            }
        }
        Some(parcel)
    }
}

#[allow(clippy::cast_possible_truncation)]
fn vertex_key(pos: DVec2) -> (i64, i64) {
    let scale = 1.0 / EPS_GEOM;
    (
        (pos.x * scale).round() as i64,
        (pos.y * scale).round() as i64,
    )
}
