use serde::{Deserialize, Serialize};

use crate::model::Material;
use crate::scope::{Quat, Vec3};

/// Optional snap-binding attached to a `Split` op.
///
/// When set, after the slot sizes are resolved the interior boundaries are
/// snapped to the nearest registered snap-plane along the split axis carrying
/// the matching `label`, provided the snap-plane lies within `tolerance`
/// world-space units of the resolved boundary. Slots on either side of a
/// snapped boundary stretch / shrink to absorb the offset; total scope
/// length is preserved.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SnapBinding {
    /// Group label of snap-planes to align to (matches `RegSnap("label")`).
    pub label: String,
    /// Maximum world-space distance between a slot boundary and a snap-plane
    /// for the snap to apply. When `None`, defaults to `5%` of the split-axis
    /// scope length at interpret time.
    pub tolerance: Option<f64>,
}

/// The axis along which a `Split` or `Repeat` operation acts.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum Axis {
    X,
    Y,
    Z,
}

/// Sizing mode for a single slot within a `Split` operation.
///
/// Mirrors CityEngine CGA syntax:
/// - `Absolute(n)`: a fixed world-unit size.
/// - `Relative(t)`: a fraction `t` of the scope's total dimension (prefix `'` in CGA text).
/// - `Floating(n)`: a weight that shares the remaining space after absolutes are consumed
///   (prefix `~` in CGA text). Multiple floating slots divide the remainder proportionally.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum SplitSize {
    Absolute(f64),
    Relative(f64),
    Floating(f64),
}

impl SplitSize {
    pub fn is_valid(&self) -> bool {
        match self {
            SplitSize::Absolute(v) | SplitSize::Relative(v) | SplitSize::Floating(v) => {
                v.is_finite() && *v > 0.0
            }
        }
    }
}

/// A single slot in a `Split` operation: a size mode paired with a successor rule name.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SplitSlot {
    pub size: SplitSize,
    /// The name of the shape rule to invoke on the resulting child scope.
    pub rule: String,
}

/// Face selectors for the `Comp(Faces)` decomposition.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum FaceSelector {
    Top,
    Bottom,
    Front,
    Back,
    Left,
    Right,
    /// Matches all non-top, non-bottom faces (shorthand for the four sides).
    Side,
    /// Matches all faces not otherwise mapped.
    All,
}

impl FaceSelector {
    pub fn parse(s: &str) -> Option<Self> {
        match s {
            "top" | "Top" => Some(Self::Top),
            "bottom" | "Bottom" => Some(Self::Bottom),
            "front" | "Front" => Some(Self::Front),
            "back" | "Back" => Some(Self::Back),
            "left" | "Left" => Some(Self::Left),
            "right" | "Right" => Some(Self::Right),
            "side" | "Side" => Some(Self::Side),
            "all" | "All" | "_" => Some(Self::All),
            _ => None,
        }
    }
}

/// A single mapping in a `Comp(Faces)` block: a face selector → rule name.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CompFaceCase {
    pub selector: FaceSelector,
    pub rule: String,
}

/// The decomposition target for a `Comp` operation.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum CompTarget {
    /// Decomposes the volume into its six axis-aligned face scopes.
    Faces(Vec<CompFaceCase>),
}

/// Face selectors for the `Offset` operation.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum OffsetSelector {
    /// The inset/outset region (the area inside the border).
    Inside,
    /// The surrounding border strips.
    Border,
    /// Matches any selector not otherwise mapped.
    All,
}

impl OffsetSelector {
    pub fn parse(s: &str) -> Option<Self> {
        match s {
            "inside" | "Inside" => Some(Self::Inside),
            "border" | "Border" => Some(Self::Border),
            "all" | "All" | "_" => Some(Self::All),
            _ => None,
        }
    }
}

/// A single mapping in an `Offset` block: a selector → rule name.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct OffsetCase {
    pub selector: OffsetSelector,
    pub rule: String,
}

/// Roof shape types for the `Roof` operation.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum RoofType {
    // ── Original types ────────────────────────────────────────────────────────
    /// Four triangular slope panels meeting at a single apex.
    Pyramid,
    /// Single slope panel from front eave to back eave.
    Shed,
    /// Two slope panels meeting at a horizontal ridge; two triangular gable ends.
    Gable,
    /// Four trapezoidal slope panels meeting at a horizontal ridge.
    Hip,

    // ── New types ─────────────────────────────────────────────────────────────
    /// Flat horizontal roof — a single horizontal panel covering the scope top.
    Flat,
    /// Two rectangular slope panels only (Gable without the triangular end panels).
    OpenGable,
    /// Two slope panels + two rectangular (non-tapered) gable-end wall panels.
    BoxGable,
    /// Four panels from a rectangular base meeting at a single apex point (no ridge).
    /// Equivalent to `Pyramid` for square footprints; left/right end panels are triangular.
    PyramidHip,
    /// Two inward-tilting slopes forming a central valley (inverted Gable).
    Butterfly,
    /// Four panels forming two parallel ridges with a central valley between them (M profile).
    MShaped,
    /// Two pitches per slope: steeper lower zone + shallower upper zone (barn roof).
    /// Requires `secondary_pitch` in `RoofConfig`.
    Gambrel,
    /// Gambrel applied to all four sides: 4 steep lower panels + 4 shallow upper panels.
    /// Requires `secondary_pitch` in `RoofConfig`.
    Mansard,
    /// Asymmetric Gable: the ridge is offset toward one end (`ridge_offset` in `RoofConfig`).
    /// Front slope is steeper; back slope is shallower. Gable ends are asymmetric triangles.
    Saltbox,
    /// Gable with clipped hip ends: the upper corners of each gable end are replaced by
    /// small triangular hip panels. Controlled by `tier_height` in `RoofConfig`.
    Jerkinhead,
    /// Hip roof with a small gable rising from the ridge centre.
    /// Controlled by `tier_height` (fraction of slope from base where the gable starts).
    DutchGable,
}

impl RoofType {
    pub fn parse(s: &str) -> Option<Self> {
        match s {
            "pyramid" | "Pyramid" => Some(Self::Pyramid),
            "shed" | "Shed" => Some(Self::Shed),
            "gable" | "Gable" => Some(Self::Gable),
            "hip" | "Hip" => Some(Self::Hip),
            "flat" | "Flat" => Some(Self::Flat),
            "openGable" | "OpenGable" => Some(Self::OpenGable),
            "boxGable" | "BoxGable" => Some(Self::BoxGable),
            "pyramidHip" | "PyramidHip" => Some(Self::PyramidHip),
            "butterfly" | "Butterfly" => Some(Self::Butterfly),
            "mShaped" | "MShaped" => Some(Self::MShaped),
            "gambrel" | "Gambrel" => Some(Self::Gambrel),
            "mansard" | "Mansard" => Some(Self::Mansard),
            "saltbox" | "Saltbox" => Some(Self::Saltbox),
            "jerkinhead" | "Jerkinhead" => Some(Self::Jerkinhead),
            "dutchGable" | "DutchGable" => Some(Self::DutchGable),
            _ => None,
        }
    }
}

/// Face selectors for the `Roof` operation.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum RoofFaceSelector {
    /// The main sloped panel(s) — front/back in most roof types.
    Slope,
    /// The triangular vertical end panels of a Gable or Saltbox roof.
    GableEnd,
    /// The steeper, lower zone of a Gambrel or Mansard roof.
    LowerSlope,
    /// The shallower, upper zone of a Gambrel or Mansard roof.
    UpperSlope,
    /// The small triangular hip panels at the clipped ends of a Jerkinhead roof.
    HipEnd,
    /// The inward-facing slopes of a Butterfly or MShaped valley.
    ValleySlope,
    /// The outer slopes of an MShaped roof (facing away from the valley).
    OuterSlope,
    /// The inner slopes of an MShaped roof (facing toward the valley).
    InnerSlope,
    /// Vertical fascia bands hanging below the eaves.
    /// Generated when [`RoofConfig::fascia_depth`] is `> 0`. One panel per perimeter
    /// slope eave; supported for all roof types whose slope panels share a horizontal
    /// eave at the perimeter (Gable, Hip, Pyramid, Shed, Saltbox, Jerkinhead, DutchGable,
    /// Gambrel, Mansard, MShaped, BoxGable, OpenGable, PyramidHip). `Flat` and `Butterfly`
    /// have no perimeter eave at the slope-panel level and produce no fascia panels.
    Fascia,
    /// Matches any selector not otherwise mapped.
    All,
}

impl RoofFaceSelector {
    pub fn parse(s: &str) -> Option<Self> {
        match s {
            "slope" | "Slope" => Some(Self::Slope),
            "gable" | "GableEnd" | "gableEnd" => Some(Self::GableEnd),
            "lowerSlope" | "LowerSlope" => Some(Self::LowerSlope),
            "upperSlope" | "UpperSlope" => Some(Self::UpperSlope),
            "hipEnd" | "HipEnd" => Some(Self::HipEnd),
            "valleySlope" | "ValleySlope" => Some(Self::ValleySlope),
            "outerSlope" | "OuterSlope" => Some(Self::OuterSlope),
            "innerSlope" | "InnerSlope" => Some(Self::InnerSlope),
            "fascia" | "Fascia" => Some(Self::Fascia),
            "all" | "All" | "_" => Some(Self::All),
            _ => None,
        }
    }
}

/// A single mapping in a `Roof` block: a face selector → rule name.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct RoofCase {
    pub selector: RoofFaceSelector,
    pub rule: String,
}

/// Rich parametric configuration for the `Roof` operation.
///
/// All angular values are in degrees. Lengths are in world units.
/// Optional fields default as described; see each field doc.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct RoofConfig {
    pub roof_type: RoofType,
    /// Primary pitch angle in degrees from horizontal. Must be in (0°, 90°).
    pub pitch: f64,
    /// Secondary pitch angle in degrees. Used by `Gambrel` (upper zone) and `Mansard`.
    /// If `None` when required, defaults to `pitch / 2`.
    pub secondary_pitch: Option<f64>,
    /// Extra overhang beyond the scope footprint on each side. Default `0.0`.
    pub overhang: f64,
    /// Ridge offset for `Saltbox`: fraction [0, 1] of the scope depth (Z) where the
    /// ridge is positioned from the front. Default `0.5` (symmetric / centred ridge).
    pub ridge_offset: f64,
    /// Thickness of the roof fascia edge in world units. Default `0.0` (flat panels).
    pub fascia_depth: f64,
    /// Normalised height at which the pitch break occurs for `Gambrel`, `Mansard`,
    /// `Jerkinhead`, and `DutchGable`. `0.5` means the break is at half the eave-to-ridge
    /// distance. `None` uses a type-specific default.
    pub tier_height: Option<f64>,
}

impl RoofConfig {
    /// Creates a minimal config for deterministic types (Pyramid, Shed, Gable, Hip, Flat, …).
    pub fn new(roof_type: RoofType, pitch: f64) -> Self {
        Self {
            roof_type,
            pitch,
            secondary_pitch: None,
            overhang: 0.0,
            ridge_offset: 0.5,
            fascia_depth: 0.0,
            tier_height: None,
        }
    }

    /// Returns the secondary pitch, defaulting to `pitch / 2` if unset.
    pub fn secondary_pitch_or_default(&self) -> f64 {
        self.secondary_pitch.unwrap_or(self.pitch / 2.0)
    }

    /// Returns the tier height, defaulting to `default` if unset.
    pub fn tier_height_or(&self, default: f64) -> f64 {
        self.tier_height.unwrap_or(default)
    }
}

/// Selector for the `Attach` operation.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum AttachSelector {
    /// The projected scope that sits on (or comes out of) the surface.
    Surface,
    /// Matches any selector not otherwise mapped.
    All,
}

impl AttachSelector {
    pub fn parse(s: &str) -> Option<Self> {
        match s {
            "surface" | "Surface" => Some(Self::Surface),
            "all" | "All" | "_" => Some(Self::All),
            _ => None,
        }
    }
}

/// A single mapping in an `Attach` block: a selector → rule name.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct AttachCase {
    pub selector: AttachSelector,
    pub rule: String,
}

/// The atomic CGA operations that the interpreter executes.
///
/// Every operation transforms the current `Scope` into zero or more child scopes,
/// each tagged with a rule name that will be recursively evaluated.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum ShapeOp {
    /// Lifts a 2-D footprint (XZ plane) into a 3-D volume by setting the Y size.
    Extrude(f64),

    /// Pyramidal taper: scales the top face toward the centroid.
    /// `amount` ∈ `[0, 1]`: 0 = no taper, 1 = full pyramid (top collapses to a point).
    Taper(f64),

    /// Applies an additional rotation to the scope (cumulative with existing rotation).
    Rotate(Quat),

    /// Translates the scope origin in local space.
    Translate(Vec3),

    /// Scales the scope size along each axis (multiplicative).
    Scale(Vec3),

    /// Divides the scope along `axis` into ordered slots.
    ///
    /// When `snap` is `Some`, interior slot boundaries are snapped to the
    /// nearest registered snap-plane along `axis` (see [`SnapBinding`]).
    Split {
        axis: Axis,
        slots: Vec<SplitSlot>,
        snap: Option<SnapBinding>,
    },

    /// Tiles the scope along `axis` with child scopes drawn from `tile_sizes`.
    ///
    /// `tile_sizes` is a per-slot pattern that is cycled to fill the axis range.
    /// Tiles are appended greedily (next tile from the cycle is added while it
    /// still fits inside the remaining range), then **all** placed tiles are
    /// scaled by the same factor `total / Σ(placed)` so they fill the scope
    /// exactly with no gap and no overshoot.
    ///
    /// A single-element list `[t]` is the legacy uniform `Repeat(axis, t)`.
    /// A multi-element list `[a, b, c]` produces an `…, a, b, c, a, b, c, …`
    /// cadence where each tile's relative width is preserved.
    Repeat {
        axis: Axis,
        tile_sizes: Vec<f64>,
        rule: String,
    },

    /// Decomposes the scope into its geometric components (faces, edges, vertices).
    Comp(CompTarget),

    /// Terminal: replace the scope with the named mesh asset.
    /// This is the "terminal symbol" — produces a `Terminal` node in the output model.
    I(String),

    /// Sets the material on the current work item.
    /// The material is propagated to the final `Terminal`, allowing downstream
    /// renderers to apply textures / shaders and physics consumers to derive
    /// volumetric mass properties without changing the scope.
    ///
    /// Syntax:
    /// - `Mat("Brick")` / `Mat(Brick)` — id-only material; no density.
    /// - `Mat("Brick", 1800)` — id + density in kg/m³; the interpreter computes
    ///   [`crate::model::MassProperties`] for terminals stamped with this material.
    Mat(Material),

    /// Calls a named sub-rule on the current scope unchanged.
    /// Used for grammar rule references that don't transform the scope themselves.
    Rule(String),

    /// Rotates the scope so that the specified local axis points in the given world direction.
    ///
    /// Applies the shortest-arc rotation from the current world direction of `local_axis`
    /// to `target`. Useful for recovering from accumulated rotations.
    /// Syntax: `Align(Y, Up)`, `Align(Z, Forward)`, etc.
    /// Named targets: `Up`=(0,1,0), `Down`=(0,-1,0), `Right`=(1,0,0), `Left`=(-1,0,0),
    /// `Forward`=(0,0,-1), `Back`=(0,0,1).
    Align { local_axis: Axis, target: Vec3 },

    /// Creates an inset (`distance < 0`) frame on a 2D face scope.
    ///
    /// Produces up to two kinds of child scopes:
    /// - `Inside`: the inset rectangle.
    /// - `Border`: four surrounding strips (bottom, top, left, right), each invoking the same rule.
    ///
    /// Syntax: `Offset(-0.2) { Inside: Glass | Border: Frame }`
    Offset {
        distance: f64,
        cases: Vec<OffsetCase>,
    },

    /// Generates a roof structure above the current scope using rich parametric configuration.
    ///
    /// Operates on a volume scope. The `config` contains the roof type, primary pitch angle,
    /// optional secondary pitch, overhang, ridge offset, fascia depth, and tier height.
    ///
    /// Syntax examples:
    /// - `Roof(Gable, 30) { Slope: Tiles | GableEnd: Bricks }` — basic Gable
    /// - `Roof(Hip, 30, 0.5) { Slope: Tiles }` — Hip with overhang
    /// - `Roof(Gambrel, 45, 20) { LowerSlope: Shingles | UpperSlope: Tiles }` — Gambrel
    /// - `Roof(Saltbox, 45, offset=0.3) { Slope: Tiles | GableEnd: Bricks }` — Saltbox
    /// - `Roof(DutchGable, 45, tier=0.7) { Slope: Tiles | GableEnd: Bricks }` — Dutch Gable
    Roof {
        config: RoofConfig,
        cases: Vec<RoofCase>,
    },

    /// Registers all six face planes of the current scope as snap-planes
    /// under the given label. Subsequent `Split(snap="label")` ops can align
    /// their interior boundaries to these planes. Read-only with respect to
    /// the scope (the scope itself passes through unchanged).
    ///
    /// Syntax: `RegSnap("bays")`
    RegSnap(String),

    /// Conditionally invokes `rule` on the current scope only when no
    /// already-emitted terminal occludes the scope (true OBB overlap test).
    /// Useful for placing decorative elements that should only appear where
    /// no structural element has already been placed.
    ///
    /// The grammar author is responsible for ordering — terminals derived
    /// before this op participate in the test; later terminals do not.
    ///
    /// Syntax: `IfClear { Window }`
    IfClear { rule: String },

    /// Inverse of [`ShapeOp::IfClear`]: invokes `rule` only when the current
    /// scope **is** occluded by an already-emitted terminal.
    ///
    /// Syntax: `IfOccluded { Patch }`
    IfOccluded { rule: String },

    /// Stamps an explicit polygonal `FaceProfile` on the next terminal in this rule.
    ///
    /// Mirrors how [`ShapeOp::Taper`] sets a profile override: the next `I(...)`
    /// (or implicit terminal) emits a `Terminal` whose `face_profile` is
    /// [`crate::model::FaceProfile::Polygon`] with the provided vertex list.
    /// Vertices are 2-D points in the scope's local floor plane (XZ), measured
    /// in world units from the scope origin; the renderer triangulates and
    /// extrudes along the local Y axis.
    ///
    /// Syntax: `Polygon((0,0), (4,0), (4,2), (2,2), (2,4), (0,4))`
    /// (variadic `(x,y)` list, capped at 256 vertices for parser DoS hardening).
    Polygon(Vec<glam::DVec2>),

    /// Projects a new horizontal scope out of a sloped face for attaching dormers or details.
    ///
    /// `world_axis` defines the "up" direction for the attached scope (usually world Y).
    /// The resulting scope sits on the face's surface with its Y axis aligned to `world_axis`,
    /// inheriting the face's width and height but with depth = 0.
    ///
    /// Syntax: `Attach(Up) { Surface: DormerMass }`
    Attach {
        world_axis: Vec3,
        cases: Vec<AttachCase>,
    },
}
