use std::path::PathBuf;

use ezcad::{
    array_of::ArrayOf,
    layer::Layer,
    objects::{
        circle::Circle,
        hatch::{Hatch, HatchFlag, HatchPattern, HatchSetting, Hatches},
        rectangle::Rectangle,
        Object, ObjectCore, Translate,
    },
    pen::Pen,
    types::{ObjectType, Point},
};
use log::{debug, error, warn};
use rand::{seq::SliceRandom, thread_rng};
use serde::{Deserialize, Serialize};

use super::{hatch::PatternHatchField, pen::PatternPenField};

#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct DeleteObjects {
    layer: usize,
    object: Option<usize>,
}

impl DeleteObjects {
    pub fn delete(&self, layers: &mut ArrayOf<Layer>) {
        debug!(
            "Deleting layer #{} {}",
            match &self.object {
                Some(object) => format!("object #{}", object),
                None => format!("all objects"),
            },
            self.layer,
        );

        let layer: &mut Layer = layers.get_mut(self.layer).expect("Invalid layer index");

        match self.object {
            Some(index) => {
                assert!(index < layer.objects.len(), "Invalid object index");
                layer.objects.remove(index);
            }
            None => layer.objects.clear(),
        }
    }
}

#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct HatchConfig {
    line_spacing: f64,

    pen: Option<u32>,
    count: Option<u32>,
    edge_offset: Option<f64>,
    start_offset: Option<f64>,
    end_offset: Option<f64>,
    angle: Option<f64>,
    rotate_angle: Option<f64>,
    line_reduction: Option<f64>,
    loop_distance: Option<f64>,
    loop_count: Option<u32>,

    pattern: Option<HatchPattern>,
    follow_edge_once: Option<bool>,
    cross_hatch: Option<bool>,
}

impl From<HatchConfig> for HatchSetting {
    fn from(value: HatchConfig) -> Self {
        let mut flags: HatchFlag = match value.pattern {
            Some(pattern) => pattern.into(),
            None => HatchPattern::Directional.into(),
        };

        value
            .follow_edge_once
            .map(|x| flags.set_follow_edge_once(x.into()));
        value.cross_hatch.map(|x| flags.set_cross_hatch(x.into()));

        if value.start_offset.is_none() && value.end_offset.is_none() {
            flags.set_average_distribute_line(1);
        }

        let mut ret: HatchSetting = Self::default();

        *ret.line_spacing = value.line_spacing;
        value.pen.map(|x| *ret.pen = x.into());
        value.count.map(|x| *ret.count = x.into());
        value.edge_offset.map(|x| *ret.edge_offset = x.into());
        value.start_offset.map(|x| *ret.start_offset = x.into());
        value.end_offset.map(|x| *ret.end_offset = x.into());
        value.angle.map(|x| *ret.angle = x.into());
        value.rotate_angle.map(|x| *ret.rotate_angle = x.into());
        value.line_reduction.map(|x| *ret.line_reduction = x.into());
        value.loop_distance.map(|x| *ret.loop_distance = x.into());
        value.loop_count.map(|x| *ret.loop_count = x.into());

        *ret.flags = flags;
        *ret.enabled = true.into();

        ret
    }
}

#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct ArrayConfig {
    columns: usize,
    rows: usize,
    spacing: f64,
    randomize_order: bool,
    starting_pen: usize,
    pattern_pen_x: Option<PatternPenField>,
    pattern_pen_y: Option<PatternPenField>,
    pattern_hatch_x: Option<PatternHatchField>,
    pattern_hatch_y: Option<PatternHatchField>,
}

#[derive(Debug, Serialize, Deserialize, strum::Display)]
#[serde(rename_all = "PascalCase")]
pub enum InputObject {
    #[serde(rename_all = "PascalCase")]
    Rectangle {
        width: f64,
        height: f64,
        round_corner: Option<f64>,
    },
    #[serde(rename_all = "PascalCase")]
    Circle { radius: f64 },
    #[serde(rename_all = "PascalCase")]
    Import { path: PathBuf },
    #[serde(rename_all = "PascalCase")]
    Existing { layer: usize, object: usize },
}

impl InputObject {
    fn new(&self, layers: &ArrayOf<Layer>) -> Object {
        match self {
            InputObject::Rectangle {
                width,
                height,
                round_corner,
            } => Object::Rectangle(Rectangle {
                drawn_corner_a: Point::from((-width / 2.0, -height / 2.0)).into(),
                drawn_corner_b: Point::from((width / 2.0, height / 2.0)).into(),
                round_bottom_left: round_corner.unwrap_or(0.0).into(),
                round_bottom_right: round_corner.unwrap_or(0.0).into(),
                round_top_left: round_corner.unwrap_or(0.0).into(),
                round_top_right: round_corner.unwrap_or(0.0).into(),
                ..Default::default()
            }),
            InputObject::Circle { radius } => Object::Circle(Circle {
                radius: (*radius).into(),
                ..Default::default()
            }),
            InputObject::Import { path } => Object::read_from_file(path),
            InputObject::Existing { layer, object } => {
                let layer: &Layer = layers.get(*layer).expect("Invalid layer index");
                layer
                    .objects
                    .get(*object)
                    .expect("Invalid object index")
                    .clone()
            }
        }
    }
}

#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct ObjectModify {
    z: Option<f64>,
    origin: Option<Point>,
    pen: Option<u32>,
}

impl ObjectModify {
    pub fn process(&self, object: &mut Object, pens: &mut Vec<Pen>) {
        // Process basic transformation
        if self.origin.is_some() || self.z.is_some() {
            debug!(
                "Translating object to origin {:?} and Z {:?}",
                self.origin, self.z
            );
            object.move_absolute(self.origin, self.z);
        }

        self.pen.map(|pen| {
                assert!(pen < pens.len().try_into().unwrap(), "Invalid pen index");
                debug!("Setting object pen to #{}", pen);
                object.set_pen(pen);
           
        });
    }
}

#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct ObjectOperation {
    input: InputObject,
    modify: Option<ObjectModify>,
    layer: Option<usize>,
    array: Option<ArrayConfig>,
    hatch: Option<HatchConfig>,
    export: Option<PathBuf>,
    replace_object: Option<usize>,
}

impl ObjectOperation {
    pub fn process(&self, pens: &mut Vec<Pen>, layers: &mut ArrayOf<Layer>) {
        debug!("Begin processing of object {:?}", self.input);

        let mut object: Object = self.input.new(layers);

        // Modify object properties
        self.modify.as_ref().map(|modify| modify.process(&mut object, pens));

        // Process conversion to hatch object
        let object = self.hatch.as_ref().map_or(object.clone(), |hatch| {
            if hatch.pen.is_some() && self.array.is_some() {
                warn!("Ignoring hatch pen setting as values will be overridden by array setting");
            }
            let hatch_setting: HatchSetting = (*hatch).into();

            match object {
                Object::Curve(_) | Object::Point(_) => {
                    error!("Points, lines, and curves cannot be hatched");
                    object
                }
                Object::Rectangle(_)
                | Object::Circle(_)
                | Object::Ellipse(_)
                | Object::Polygon(_) => {
                    debug!("Converting to hatch object");
                    // Build new hatch object (no hatch lines)
                    Object::Hatch(Hatch {
                        core: ObjectCore {
                            origin: object.core_mut().origin,
                            z: object.core_mut().z,
                            ..ObjectCore::default(ObjectType::Hatch)
                        },
                        outline: vec![object].into(),
                        legacy_setting: vec![hatch_setting.clone()].into(),
                        _unknown_1: vec![0; 960].into(),
                        hatch_settings: vec![hatch_setting.clone()].into(),
                        hatches: Some(Hatches {
                            core: ObjectCore::default(ObjectType::HatchLine),
                            hatch_lines: vec![].into(),
                        }),
                    })
                }
                Object::Hatch(hatch) => {
                    debug!("Modifying existing hatch settings");
                    // Modify existing hatch (delete existing hatch lines)
                    Object::Hatch(Hatch {
                        legacy_setting: vec![hatch_setting.clone()].into(),
                        hatch_settings: vec![hatch_setting.clone()].into(),
                        hatches: Some(Hatches {
                            core: ObjectCore::default(ObjectType::HatchLine),
                            hatch_lines: vec![].into(),
                        }),
                        ..hatch
                    })
                }
            }
        });

        // Process array generation of object
        let new_objects = self.array.as_ref().map_or_else(
            || vec![object.clone()],
            |array| {
                let mut new_obj: Vec<Object> = vec![];

                let bottom_left: Point = Point {
                    x: (array.columns - 1) as f64 * array.spacing / -2.0,
                    y: (array.rows - 1) as f64 * array.spacing / -2.0,
                };

                // Generate objects
                for y in 0..array.rows {
                    for x in 0..array.columns {
                        let delta: Point = bottom_left
                            + Point {
                                x: array.spacing * x as f64,
                                y: array.spacing * y as f64,
                            };

                        let pen: usize = array.starting_pen + y * array.columns + x;

                        let mut object: Object = object.clone();
                        object.move_relative(Some(delta), None);
                        object.set_pen(pen.try_into().unwrap());

                        debug!(
                            "Adding new object at {} with pen #{}",
                            object.core_mut().origin,
                            pen
                        );
                        new_obj.push(object);
                    }
                }

                if (array.pattern_hatch_x.is_some() && array.pattern_pen_x.is_some())
                    || (array.pattern_hatch_y.is_some() && array.pattern_pen_y.is_some())
                {
                    panic!("Conflict with X or Y axis patterning options");
                }

                let pattern_x: bool =
                    array.pattern_hatch_x.is_some() || array.pattern_pen_x.is_some();
                let pattern_y: bool =
                    array.pattern_hatch_y.is_some() || array.pattern_pen_y.is_some();

                if let Some(pen_x) = &array.pattern_pen_x {
                    if pattern_y {
                        for y in 0..array.rows {
                            pen_x.pattern(
                                &mut pens
                                    .iter_mut()
                                    .enumerate()
                                    .skip(array.starting_pen)
                                    .skip(y * array.columns)
                                    .take(array.columns),
                            )
                        }
                    } else {
                        pen_x.pattern(
                            &mut pens
                                .iter_mut()
                                .enumerate()
                                .skip(array.starting_pen)
                                .take(array.columns * array.rows),
                        );
                    }
                }

                if let Some(hatch_x) = &array.pattern_hatch_x {
                    if pattern_y {
                        for y in 0..array.rows {
                            hatch_x.pattern(
                                &mut new_obj
                                    .iter_mut()
                                    .enumerate()
                                    .skip(y * array.columns)
                                    .take(array.columns),
                            )
                        }
                    } else {
                        hatch_x.pattern(
                            &mut new_obj
                                .iter_mut()
                                .enumerate()
                                .take(array.columns * array.rows),
                        );
                    }
                }

                if let Some(pen_y) = &array.pattern_pen_y {
                    if pattern_x {
                        for x in 0..array.columns {
                            pen_y.pattern(
                                &mut pens
                                    .iter_mut()
                                    .enumerate()
                                    .skip(x + array.starting_pen)
                                    .step_by(array.columns)
                                    .take(array.rows),
                            );
                        }
                    } else {
                        for x in 0..array.columns {
                            pen_y.pattern(
                                &mut pens
                                    .iter_mut()
                                    .enumerate()
                                    .skip(x + array.starting_pen)
                                    .step_by(array.columns)
                                    .take(array.rows),
                            );
                        }
                    }
                }

                if let Some(hatch_y) = &array.pattern_hatch_y {
                    if pattern_x {
                        for x in 0..array.columns {
                            hatch_y.pattern(
                                &mut new_obj
                                    .iter_mut()
                                    .enumerate()
                                    .skip(x)
                                    .step_by(array.columns)
                                    .take(array.rows),
                            );
                        }
                    } else {
                        for x in 0..array.columns {
                            hatch_y.pattern(
                                &mut new_obj
                                    .iter_mut()
                                    .enumerate()
                                    .skip(x)
                                    .step_by(array.columns)
                                    .take(array.rows),
                            );
                        }
                    }
                }

                if array.randomize_order {
                    debug!("Randomizing draw order of array objects");
                    new_obj.shuffle(&mut thread_rng());
                }

                new_obj
            },
        );

        let layer_id: usize = self.layer.unwrap_or(0);
        let layer: &mut Layer = layers.get_mut(layer_id).expect("Invalid layer index");

        // Either export the object, replace an existing object, or append if neither
        if let Some(path) = &self.export {
            if new_objects.len() > 1 {
                warn!(
                    "Exporting only object #0 from layer #{} in list of objects",
                    layer_id
                );
            } else {
                debug!(
                    "Exporting object {} in layer #{} to '{}'",
                    new_objects[0],
                    layer_id,
                    path.to_string_lossy()
                );
            }
            new_objects[0].write_to_file(path);
        } else if let Some(object) = self.replace_object {
            assert!(object < layer.objects.len(), "Invalid object index");
            debug!(
                "Replacing object #{} in layer #{} with new objects",
                object, layer_id
            );
            layer.objects.splice(object..=object, new_objects);
        } else {
            debug!("Extending layer #{} with new objects", layer_id);
            layer.objects.extend(new_objects);
        }
    }
}
