use acadrust::entities::MLine;

use crate::command::EntityTransform;
use crate::entities::common::{edit_prop as edit, ro_prop as ro, square_grip};
use crate::entities::traits::{Grippable, PropertyEditable, Transformable, TruckConvertible};
use crate::scene::convert::acad_to_truck::{TruckEntity, TruckObject};
use crate::scene::model::object::{GripApply, GripDef, PropSection, PropValue, Property};
use crate::scene::model::wire_model::SnapHint;

/// One drawn line of a multiline: the polyline for a single style element (or
/// an end cap), tagged with the element's colour and linetype so the
/// tessellator can colour-bin and dash each one independently.
pub struct MLineLine {
    pub points: Vec<[f64; 3]>,
    pub color: acadrust::types::Color,
    pub linetype: String,
}

/// Resolve a multiline into its per-element parallel lines in WCS.
///
/// Geometry comes from the referenced MLINESTYLE (element offsets, the
/// justification shift and the entity scale) rather than a fixed ±scale/2 guess,
/// so a custom style's offsets, colours and linetypes render the way the drawing
/// intends. Falls back to a ±0.5 two-line layout only when no MLINESTYLE can be
/// resolved (e.g. the style object is missing).
pub fn mline_lines(m: &MLine, document: &acadrust::CadDocument) -> Vec<MLineLine> {
    use acadrust::entities::{MLineFlags, MLineJustification};
    use acadrust::objects::ObjectType;
    use acadrust::types::Color;

    if m.vertices.is_empty() {
        return Vec::new();
    }

    // MLINESTYLE lookup: prefer the hard-pointer handle, fall back to the name.
    let style = m
        .style_handle
        .and_then(|h| match document.objects.get(&h) {
            Some(ObjectType::MLineStyle(s)) => Some(s),
            _ => None,
        })
        .or_else(|| {
            document.objects.values().find_map(|o| match o {
                ObjectType::MLineStyle(s) if s.name.eq_ignore_ascii_case(&m.style_name) => Some(s),
                _ => None,
            })
        });

    // (offset, colour, linetype) per element.
    let elems: Vec<(f64, Color, String)> = match style {
        Some(s) if !s.elements.is_empty() => s
            .elements
            .iter()
            .map(|e| (e.offset, e.color, e.linetype.clone()))
            .collect(),
        _ => vec![
            (0.5, Color::ByLayer, "ByLayer".to_string()),
            (-0.5, Color::ByLayer, "ByLayer".to_string()),
        ],
    };

    // Justification shifts every element so the picked path runs along the top /
    // centre / bottom element of the style. (Only the fallback path needs it —
    // stored vertex parameters already bake justification in.)
    let mut lo = f64::INFINITY;
    let mut hi = f64::NEG_INFINITY;
    for (o, _, _) in &elems {
        lo = lo.min(*o);
        hi = hi.max(*o);
    }
    let shift = match m.justification {
        MLineJustification::Top => -hi,
        MLineJustification::Bottom => -lo,
        MLineJustification::Zero => 0.0,
    };

    let scale = m.scale_factor;
    let closed = m.flags.contains(MLineFlags::CLOSED);
    let n = m.vertices.len();

    // The element's miter-space offset at vertex `vi`: the stored per-vertex
    // parameter[0] when present — it is measured ALONG THE MITER, so corner
    // vertices already carry the 1/cos(θ/2) miter lengthening and the
    // justification shift (using the flat style offset instead pinched the
    // channel at every corner and made the ends look flared). The style
    // offset × scale is only the fallback for files without parameters.
    let elem_off = |vi: usize, ei: usize| -> f64 {
        m.vertices[vi]
            .segments
            .get(ei)
            .and_then(|sg| sg.parameters.first().copied())
            .unwrap_or_else(|| (elems[ei].0 + shift) * scale)
    };
    let off_pt = |vi: usize, d: f64| -> [f64; 3] {
        let v = &m.vertices[vi];
        [
            v.position.x + v.miter.x * d,
            v.position.y + v.miter.y * d,
            v.position.z + v.miter.z * d,
        ]
    };

    let mut out: Vec<MLineLine> = Vec::with_capacity(elems.len() + 2);
    for (ei, (_, color, linetype)) in elems.iter().enumerate() {
        let mut pts: Vec<[f64; 3]> = Vec::new();
        // Walk each segment (vi → vi+1, wrapping when closed); the vertex's
        // parameters[1..] are draw-toggle distances along the element line —
        // this is how crossing/merged multilines store their gaps. An odd
        // toggle count leaves the final run open to the segment end.
        let seg_count = if closed { n } else { n.saturating_sub(1) };
        let mut pen_at_end = false;
        for k in 0..seg_count {
            let vi = k;
            let wi = (k + 1) % n;
            let a = off_pt(vi, elem_off(vi, ei));
            let b = off_pt(wi, elem_off(wi, ei));
            let seg = [b[0] - a[0], b[1] - a[1], b[2] - a[2]];
            let len = (seg[0] * seg[0] + seg[1] * seg[1] + seg[2] * seg[2]).sqrt();
            if len < 1e-12 {
                continue;
            }
            let dirn = [seg[0] / len, seg[1] / len, seg[2] / len];
            let at = |t: f64| -> [f64; 3] {
                [a[0] + dirn[0] * t, a[1] + dirn[1] * t, a[2] + dirn[2] * t]
            };
            let toggles: &[f64] = m
                .vertices[vi]
                .segments
                .get(ei)
                .map(|sg| sg.parameters.get(1..).unwrap_or(&[]))
                .unwrap_or(&[]);
            // Build (start, end) runs from the toggle list.
            let mut runs: Vec<(f64, f64)> = Vec::new();
            if toggles.is_empty() {
                runs.push((0.0, len));
            } else {
                let mut i = 0;
                while i < toggles.len() {
                    let start = toggles[i].clamp(0.0, len);
                    let end = toggles
                        .get(i + 1)
                        .copied()
                        .unwrap_or(len)
                        .clamp(0.0, len);
                    if end - start > 1e-9 {
                        runs.push((start, end));
                    }
                    i += 2;
                }
            }
            for (ri, (t0, t1)) in runs.iter().enumerate() {
                let continuous = pen_at_end && ri == 0 && *t0 <= 1e-9 && !pts.is_empty();
                if !continuous {
                    if !pts.is_empty() {
                        pts.push([f64::NAN; 3]);
                    }
                    pts.push(at(*t0));
                }
                pts.push(at(*t1));
            }
            pen_at_end = runs.last().is_some_and(|(_, t1)| (len - t1).abs() <= 1e-9);
        }
        if pts.len() < 2 {
            continue;
        }
        out.push(MLineLine {
            points: pts,
            color: *color,
            linetype: linetype.clone(),
        });
    }

    // End caps: a segment across the full style width at the first / last vertex,
    // drawn only when the style requests square caps, the style has width, and
    // the multiline is open.
    if let Some(s) = style {
        if !closed && n >= 2 {
            let mut cap = |vi: usize| {
                let mut dlo = f64::INFINITY;
                let mut dhi = f64::NEG_INFINITY;
                for ei in 0..elems.len() {
                    let d = elem_off(vi, ei);
                    dlo = dlo.min(d);
                    dhi = dhi.max(d);
                }
                if (dhi - dlo).abs() > 1e-9 {
                    out.push(MLineLine {
                        points: vec![off_pt(vi, dlo), off_pt(vi, dhi)],
                        color: Color::ByLayer,
                        linetype: "ByLayer".to_string(),
                    });
                }
            };
            if s.flags.start_square_cap {
                cap(0);
            }
            if s.flags.end_square_cap {
                cap(n - 1);
            }
        }
    }

    out
}

impl TruckConvertible for MLine {
    fn to_truck(&self, document: &acadrust::CadDocument) -> Option<TruckEntity> {
        if self.vertices.is_empty() {
            return None;
        }

        // NaN-separated flat list of every element line (single-colour path used
        // by pick and the edit commands; the coloured render is built in
        // `tessellate`, which special-cases MLINE).
        let lines = mline_lines(self, document);
        let mut pts: Vec<[f64; 3]> = Vec::new();
        for (i, l) in lines.iter().enumerate() {
            if i > 0 {
                pts.push([f64::NAN; 3]);
            }
            pts.extend_from_slice(&l.points);
        }

        let key_verts: Vec<[f64; 3]> = self
            .vertices
            .iter()
            .map(|v| [v.position.x, v.position.y, v.position.z])
            .collect();

        let snap_pts = self
            .vertices
            .iter()
            .map(|v| {
                (
                    glam::DVec3::new(v.position.x, v.position.y, v.position.z),
                    SnapHint::Node,
                )
            })
            .collect();

        Some(TruckEntity {
            pick_tris: Vec::new(),
            object: TruckObject::Lines(pts),
            snap_pts,
            tangent_geoms: vec![],
            key_vertices: key_verts,
            fill_tris: vec![],
        })
    }
}

impl Grippable for MLine {
    fn grips(&self) -> Vec<GripDef> {
        self.vertices
            .iter()
            .enumerate()
            .map(|(i, v)| {
                square_grip(
                    i,
                    glam::DVec3::new(v.position.x, v.position.y, v.position.z),
                )
            })
            .collect()
    }

    fn apply_grip(&mut self, grip_id: usize, apply: GripApply) {
        if let Some(v) = self.vertices.get_mut(grip_id) {
            match apply {
                GripApply::Translate(d) => {
                    v.position.x += d.x as f64;
                    v.position.y += d.y as f64;
                    v.position.z += d.z as f64;
                }
                GripApply::Absolute(p) => {
                    v.position.x = p.x as f64;
                    v.position.y = p.y as f64;
                    v.position.z = p.z as f64;
                }
            }
        }
    }

    fn grip_menu(&self, _grip_id: usize) -> Vec<crate::scene::model::object::GripMenuItem> {
        use crate::scene::model::object::{GripMenuAction, GripMenuItem};
        vec![
            GripMenuItem {
                label: "Stretch",
                action: GripMenuAction::Stretch,
            },
            GripMenuItem {
                label: "Add Vertex",
                action: GripMenuAction::AddVertex,
            },
            GripMenuItem {
                label: "Remove Vertex",
                action: GripMenuAction::RemoveVertex,
            },
        ]
    }

    fn apply_grip_menu(&mut self, grip_id: usize, action: crate::scene::model::object::GripMenuAction) {
        use crate::scene::model::object::GripMenuAction as A;
        let n = self.vertices.len();
        match action {
            A::AddVertex if grip_id < n => {
                let i1 = (grip_id + 1).min(n - 1);
                if i1 == grip_id {
                    return;
                }
                let v0 = &self.vertices[grip_id];
                let v1 = &self.vertices[i1];
                let mut new_v = v0.clone();
                new_v.position.x = (v0.position.x + v1.position.x) * 0.5;
                new_v.position.y = (v0.position.y + v1.position.y) * 0.5;
                new_v.position.z = (v0.position.z + v1.position.z) * 0.5;
                self.vertices.insert(i1, new_v);
            }
            A::RemoveVertex if grip_id < n && n > 2 => {
                self.vertices.remove(grip_id);
            }
            _ => {}
        }
    }
}

impl PropertyEditable for MLine {
    fn geometry_properties(&self, _text_style_names: &[String]) -> Vec<PropSection> {
        let just_str = match self.justification {
            acadrust::entities::MLineJustification::Top => "Top",
            acadrust::entities::MLineJustification::Zero => "Zero",
            acadrust::entities::MLineJustification::Bottom => "Bottom",
        };
        let cur = self.vertices.first();
        let cur_x = cur.map(|v| v.position.x).unwrap_or(0.0);
        let cur_y = cur.map(|v| v.position.y).unwrap_or(0.0);
        let cur_z = cur.map(|v| v.position.z).unwrap_or(0.0);
        vec![
            PropSection {
                title: "Geometry".into(),
                props: vec![
                    ro("Vertex", "ml_vertex", if self.vertices.is_empty() { String::new() } else { "1".to_string() }),
                    edit("Vertex X", "ml_vertex_x", cur_x),
                    edit("Vertex Y", "ml_vertex_y", cur_y),
                    edit("Vertex Z", "ml_vertex_z", cur_z),
                ],
            },
            PropSection {
                title: "Misc".into(),
                props: vec![
                    Property {
                        label: "Style".into(),
                        field: "ml_style",
                        value: PropValue::EditText(self.style_name.clone()),
                    },
                    Property {
                        label: "Style justification".into(),
                        field: "ml_justification",
                        value: PropValue::Choice {
                            selected: just_str.to_string(),
                            options: ["Top", "Zero", "Bottom"]
                                .into_iter()
                                .map(str::to_string)
                                .collect(),
                        },
                    },
                    edit("Style scale", "ml_scale", self.scale_factor),
                ],
            },
        ]
    }

    fn apply_geom_prop(&mut self, field: &str, value: &str) {
        match field {
            "ml_closed" => {
                let closed = if value == "toggle" {
                    !self.flags.contains(acadrust::entities::MLineFlags::CLOSED)
                } else {
                    value == "true"
                };
                self.flags
                    .set(acadrust::entities::MLineFlags::CLOSED, closed);
                return;
            }
            "ml_justification" => {
                self.justification = match value {
                    "Top" => acadrust::entities::MLineJustification::Top,
                    "Bottom" => acadrust::entities::MLineJustification::Bottom,
                    _ => acadrust::entities::MLineJustification::Zero,
                };
                return;
            }
            "ml_style" => {
                self.style_name = value.to_string();
                return;
            }
            _ => {}
        }
        let Ok(v) = value.trim().parse::<f64>() else {
            return;
        };
        match field {
            "ml_scale" if v != 0.0 => self.scale_factor = v,
            "ml_vertex_x" => {
                if let Some(vx) = self.vertices.first_mut() {
                    vx.position.x = v;
                }
            }
            "ml_vertex_y" => {
                if let Some(vx) = self.vertices.first_mut() {
                    vx.position.y = v;
                }
            }
            "ml_vertex_z" => {
                if let Some(vx) = self.vertices.first_mut() {
                    vx.position.z = v;
                }
            }
            _ => {}
        }
    }
}

impl Transformable for MLine {
    fn apply_transform(&mut self, t: &EntityTransform) {
        crate::scene::view::transform::apply_standard_entity_transform(self, t, |entity, p1, p2| {
            for v in &mut entity.vertices {
                crate::scene::view::transform::reflect_xy_point(
                    &mut v.position.x,
                    &mut v.position.y,
                    p1,
                    p2,
                );
            }
            crate::scene::view::transform::reflect_xy_point(
                &mut entity.start_point.x,
                &mut entity.start_point.y,
                p1,
                p2,
            );
        });
    }
}
