use std::sync::Arc;
use kittycad::types::{ModelingCmd, Point3D};
use crate::{
    ast::types::{
        ArrayExpression, CallExpression, ConstraintLevel, FormatOptions, Literal, PipeExpression, PipeSubstitution,
        Program, VariableDeclarator,
    },
    engine::EngineManager,
    errors::{KclError, KclErrorDetails},
    executor::{Point2d, SourceRange},
};
#[derive(Debug)]
pub struct ControlPointData {
    pub points: Vec<kittycad::types::Point3D>,
    pub command: kittycad::types::PathCommand,
    pub id: uuid::Uuid,
}
const EPSILON: f64 = 0.015625; pub async fn modify_ast_for_sketch(
    engine: &Arc<Box<dyn EngineManager>>,
    program: &mut Program,
    sketch_name: &str,
    plane: crate::executor::PlaneType,
    sketch_id: uuid::Uuid,
) -> Result<String, KclError> {
    if let Some(ast_sketch) = program.get_variable(sketch_name) {
        let constraint_level = ast_sketch.get_constraint_level();
        match &constraint_level {
            ConstraintLevel::None { source_ranges: _ } => {}
            ConstraintLevel::Ignore { source_ranges: _ } => {}
            ConstraintLevel::Partial {
                source_ranges: _,
                levels,
            } => {
                return Err(KclError::Engine(KclErrorDetails {
                    message: format!(
                        "Sketch {} is constrained `{}` and cannot be modified",
                        sketch_name, constraint_level
                    ),
                    source_ranges: levels.get_all_partial_or_full_source_ranges(),
                }));
            }
            ConstraintLevel::Full { source_ranges } => {
                return Err(KclError::Engine(KclErrorDetails {
                    message: format!(
                        "Sketch {} is constrained `{}` and cannot be modified",
                        sketch_name, constraint_level
                    ),
                    source_ranges: source_ranges.clone(),
                }));
            }
        }
    }
    let resp = engine
        .send_modeling_cmd(
            uuid::Uuid::new_v4(),
            SourceRange::default(),
            ModelingCmd::PathGetInfo { path_id: sketch_id },
        )
        .await?;
    let kittycad::types::OkWebSocketResponseData::Modeling {
        modeling_response: kittycad::types::OkModelingCmdResponse::PathGetInfo { data: path_info },
    } = &resp
    else {
        return Err(KclError::Engine(KclErrorDetails {
            message: format!("Get path info response was not as expected: {:?}", resp),
            source_ranges: vec![SourceRange::default()],
        }));
    };
    let mut control_points = Vec::new();
    for segment in &path_info.segments {
        if let Some(command_id) = &segment.command_id {
            let h = engine.send_modeling_cmd(
                uuid::Uuid::new_v4(),
                SourceRange::default(),
                ModelingCmd::CurveGetControlPoints { curve_id: *command_id },
            );
            let kittycad::types::OkWebSocketResponseData::Modeling {
                modeling_response: kittycad::types::OkModelingCmdResponse::CurveGetControlPoints { data },
            } = h.await?
            else {
                return Err(KclError::Engine(KclErrorDetails {
                    message: format!("Curve get control points response was not as expected: {:?}", resp),
                    source_ranges: vec![SourceRange::default()],
                }));
            };
            control_points.push(ControlPointData {
                points: data.control_points.clone(),
                command: segment.command.clone(),
                id: *command_id,
            });
        }
    }
    if control_points.is_empty() {
        return Err(KclError::Engine(KclErrorDetails {
            message: format!("No control points found for sketch {}", sketch_name),
            source_ranges: vec![SourceRange::default()],
        }));
    }
    let first_control_points = control_points.first().ok_or_else(|| {
        KclError::Engine(KclErrorDetails {
            message: format!("No control points found for sketch {}", sketch_name),
            source_ranges: vec![SourceRange::default()],
        })
    })?;
    let mut additional_lines = Vec::new();
    let mut last_point = first_control_points.points[1];
    for control_point in control_points[1..].iter() {
        additional_lines.push([
            (control_point.points[1].x - last_point.x),
            (control_point.points[1].y - last_point.y),
        ]);
        last_point = Point3D {
            x: control_point.points[1].x,
            y: control_point.points[1].y,
            z: control_point.points[1].z,
        };
    }
    let start_sketch_at_end = Point3D {
        x: (first_control_points.points[1].x - first_control_points.points[0].x),
        y: (first_control_points.points[1].y - first_control_points.points[0].y),
        z: (first_control_points.points[1].z - first_control_points.points[0].z),
    };
    let sketch = create_start_sketch_on(
        sketch_name,
        [first_control_points.points[0].x, first_control_points.points[0].y],
        [start_sketch_at_end.x, start_sketch_at_end.y],
        plane,
        additional_lines,
    )?;
    program.replace_variable(sketch_name, sketch);
    let recasted = program.recast(&FormatOptions::default(), 0);
    let tokens = crate::token::lexer(&recasted)?;
    let parser = crate::parser::Parser::new(tokens);
    *program = parser.ast()?;
    Ok(recasted)
}
fn create_start_sketch_on(
    name: &str,
    start: [f64; 2],
    end: [f64; 2],
    plane: crate::executor::PlaneType,
    additional_lines: Vec<[f64; 2]>,
) -> Result<VariableDeclarator, KclError> {
    let start_sketch_on = CallExpression::new("startSketchOn", vec![Literal::new(plane.to_string().into()).into()])?;
    let start_profile_at = CallExpression::new(
        "startProfileAt",
        vec![
            ArrayExpression::new(vec![
                Literal::new(round_before_recast(start[0]).into()).into(),
                Literal::new(round_before_recast(start[1]).into()).into(),
            ])
            .into(),
            PipeSubstitution::new().into(),
        ],
    )?;
    let mut current_position = Point2d {
        x: start[0],
        y: start[1],
    };
    current_position.x += end[0];
    current_position.y += end[1];
    let initial_line = CallExpression::new(
        "line",
        vec![
            ArrayExpression::new(vec![
                Literal::new(round_before_recast(end[0]).into()).into(),
                Literal::new(round_before_recast(end[1]).into()).into(),
            ])
            .into(),
            PipeSubstitution::new().into(),
        ],
    )?;
    let mut pipe_body = vec![start_sketch_on.into(), start_profile_at.into(), initial_line.into()];
    for (index, line) in additional_lines.iter().enumerate() {
        current_position.x += line[0];
        current_position.y += line[1];
        if index == additional_lines.len() - 1 {
            let diff_x = (current_position.x - start[0]).abs();
            let diff_y = (current_position.y - start[1]).abs();
            if diff_x <= EPSILON && diff_y <= EPSILON {
                let close = CallExpression::new("close", vec![PipeSubstitution::new().into()])?;
                pipe_body.push(close.into());
                break;
            }
        }
        let line = CallExpression::new(
            "line",
            vec![
                ArrayExpression::new(vec![
                    Literal::new(round_before_recast(line[0]).into()).into(),
                    Literal::new(round_before_recast(line[1]).into()).into(),
                ])
                .into(),
                PipeSubstitution::new().into(),
            ],
        )?;
        pipe_body.push(line.into());
    }
    Ok(VariableDeclarator::new(name, PipeExpression::new(pipe_body).into()))
}
fn round_before_recast(num: f64) -> f64 {
    (num * 100.0).round() / 100.0
}