use std::{
    fs::File,
    io::{Cursor, Read, Write},
    ops::RangeInclusive,
    path::{Path, PathBuf},
    time::Instant,
};

use binrw::{BinRead, BinWrite, BinWriterExt};
use clap::{error::ErrorKind, Args, Error, Parser, Subcommand};
use clap_verbosity_flag::{InfoLevel, Verbosity};
use dialoguer::Confirm;
use diff::Diff;
use env_logger::Target;
use ezcad::{file::EzCadHeader, layer::Layer, objects::Object, pen::Pen};
use itertools::Itertools;
use log::{info, trace, warn};
use num_format::{Locale, ToFormattedString};
use regex::Regex;

use crate::config::{Config, Operations};

mod config;

#[derive(Debug, Parser)]
struct Cli {
    #[command(subcommand)]
    command: SubCommands,

    /// Input .mlp file to parse
    #[arg(short = 'i', long)]
    input: PathBuf,

    #[command(flatten)]
    verbose: Verbosity<InfoLevel>,
}

#[derive(Debug, Subcommand)]
enum SubCommands {
    Diff(DiffCmd),
    Query(QueryCmd),
    Apply(ApplyConfig),
}

/// Diff two .mlp files and print differences between the two
#[derive(Debug, Args)]
struct DiffCmd {
    /// File to diff input against
    #[arg(short = 'd', long)]
    diff_file: PathBuf,
}

/// Queries input .mlp file for pen or object info
#[derive(Debug, Args)]
struct QueryCmd {
    /// Print info for pens
    #[arg(short = 'p', long)]
    #[arg(value_parser = parse_range)]
    pen: Option<RangeInclusive<usize>>,

    /// Print info for objects
    #[arg(short = 'o', long)]
    #[arg(value_parser = parse_range)]
    object: Option<RangeInclusive<usize>>,

    /// Object layer to query object on
    #[arg(short = 'l', long)]
    object_layer: Option<usize>,
}

/// Applies configuration YAML to input .mlp file
#[derive(Debug, Args)]
struct ApplyConfig {
    /// Configuration file
    #[arg(short = 'c', long)]
    config: PathBuf,

    /// Output file to write to
    #[arg(short = 'o', long)]
    output: Option<PathBuf>,

    /// Overwrite output file if it exists
    #[arg(short = 'w', long)]
    overwrite: bool,
}

/// Helper function to parse a string as an RangeInclusive<usize>
fn parse_range(arg: &str) -> Result<RangeInclusive<usize>, Error> {
    let re: Regex = Regex::new(r"(\d+)?(\.{2})?(=)?(\d+)?").unwrap();
    const PEN_MAX: usize = 255;

    match re.captures(arg) {
        Some(found) => {
            let range: bool = found.get(2).is_some();
            let inclusive: bool = found.get(3).is_some();
            let d1: Option<usize> = found
                .get(1)
                .map(|x| usize::from_str_radix(x.as_str(), 10).unwrap());
            let d2: Option<usize> = found
                .get(4)
                .map(|x| usize::from_str_radix(x.as_str(), 10).unwrap());

            let ret = |d1: usize, d2: usize, inclusive: bool| -> RangeInclusive<usize> {
                match inclusive {
                    true => d1..=d2,
                    false => d1..=(d2 - 1),
                }
            };

            match (d1, d2) {
                (None, None) if range => Ok(0..=PEN_MAX),
                (None, None) => Err(Error::new(ErrorKind::InvalidValue)),
                (Some(d1), None) if !range => Ok(d1..=d1),
                (Some(d1), None) => Ok(d1..=PEN_MAX),
                (None, Some(d2)) if !range => Ok(d2..=d2),
                (None, Some(d2)) => Ok(ret(0, d2, inclusive)),
                (Some(d1), Some(d2)) if d1 > d2 => Err(Error::new(ErrorKind::InvalidValue)),
                (Some(d1), Some(d2)) => Ok(ret(d1, d2, inclusive)),
            }
        }
        _ => Err(Error::new(ErrorKind::InvalidValue)),
    }
}

fn main() {
    let cli: Cli = Cli::parse();

    env_logger::builder()
        .format_timestamp(None)
        .format_target(false)
        .filter_level(cli.verbose.log_level_filter())
        .target(Target::Stdout)
        .init();

    info!("Reading input file '{}'", cli.input.to_string_lossy());
    let mut input: File = File::open(cli.input).expect("Failed to open input file");

    // Deserialize to memory buffer for perf
    let mut input_file: Cursor<Vec<u8>> = Cursor::new(vec![]);
    input
        .read_to_end(input_file.get_mut())
        .expect("Failed to read input file");

    // Deserialize as EZCAD format
    let time: Instant = Instant::now();
    let mut file: EzCadHeader =
        EzCadHeader::read_le(&mut input_file).expect("Failed to parse input file as EZCAD format");
    trace!("Input file decode time: {:?}", time.elapsed());

    // Sanity check that re-encoded input file matches original
    info!("Validating re-encoding of input file");
    let mut buffer: Cursor<Vec<u8>> = Cursor::new(vec![]);

    let time: Instant = Instant::now();
    file.write_le(&mut buffer)
        .expect("Failed to reserialize input file as EZCAD format");
    trace!("Input file re-encode time: {:?}", time.elapsed());

    if buffer.into_inner().as_slice().ne(input_file.get_ref()) {
        warn!("Re-encoded input file does not match original file!");
    }

    match cli.command {
        SubCommands::Diff(args) => {
            info!(
                "Processing diff file '{}'",
                args.diff_file.to_string_lossy()
            );
            let mut diff: File = File::open(args.diff_file).expect("Failed to open diff file");
            let diff_file: EzCadHeader =
                EzCadHeader::read_le(&mut diff).expect("Failed to parse diff file as EZCAD format");

            // Diff pens
            info!(
                "{:#?}",
                file.pens_offset
                    .data
                    .pens
                    .diff(&diff_file.pens_offset.data.pens)
            );

            // Diff objects
            info!(
                "{:#?}",
                file.layers_offset
                    .value
                    .diff(&diff_file.layers_offset.value)
            );
        }
        SubCommands::Query(args) => {
            let pens: &Vec<Pen> = &file.pens_offset.data.pens;

            // Print info on pens with non-default settings
            for (index, pen) in pens.iter().filter(|x| *x.use_default == 0).enumerate() {
                trace!("Pen {}: {:#?}", index, pen);
            }

            // Print all objects
            for (layer_index, layer) in file.layers_offset.iter().enumerate() {
                for (object_index, object) in layer.objects.iter().enumerate() {
                    trace!(
                        "Layer {}, Object {}: {:#?}",
                        layer_index,
                        object_index,
                        object
                    );
                }
            }

            // Process pen query
            args.pen.map(|pen_range| {
                for pen in pen_range {
                    info!(
                        "Pen #{}: {}",
                        pen,
                        pens.get(pen).expect("Invalid pen index")
                    );
                }
            });

            // Process object query
            let layer_idx: usize = args.object_layer.unwrap_or_else(|| {
                (*file.layers_offset)
                    .iter()
                    .find_position(|x| x.objects.len() != 0)
                    .expect("No objects defined in any layer")
                    .0
            });
            let layer: &Layer = file
                .layers_offset
                .get(layer_idx)
                .expect("Invalid layer index");

            warn!("Object origin, width, and height values may be incorrect if object is skewed or rotated");
            let objects: Vec<(usize, &Object)> = if let Some(obj_range) = args.object {
                obj_range
                    .clone()
                    .zip(
                        layer
                            .objects
                            .get(obj_range)
                            .expect("Invalid object query range")
                            .iter(),
                    )
                    .collect_vec()
            } else {
                layer.objects.iter().enumerate().collect_vec()
            };

            for (obj_idx, object) in objects {
                let pen_idx: u32 = *object.core().pen;
                let pen: &Pen = pens
                    .get(pen_idx as usize)
                    .expect("Invalid pen index in object");

                // Calculate estimated power density if object is hatched
                let hatch_power: Option<String> = match object {
                    Object::Hatch(hatch) => Some(
                        hatch
                            .calc_power_density(pens)
                            .to_formatted_string(&Locale::en),
                    ),
                    _ => None,
                };

                // Print info
                info!(
                    "Layer #{}, Object #{}:\n{}\nPen: #{}: {}{}",
                    layer_idx,
                    obj_idx,
                    object,
                    pen_idx,
                    pen,
                    hatch_power.map_or(String::new(), |p| format!(
                        "\nEstimated hatch power density: {p}"
                    ))
                );
            }
        }
        SubCommands::Apply(args) => {
            // Process config
            info!("Processing config file '{}'", args.config.to_string_lossy());
            let config: String =
                std::fs::read_to_string(args.config).expect("Failed to open config file");
            let config: Config =
                serde_yaml::from_str(&config).expect("Failed to parse config file");

            // Patch pen settings
            let time: Instant = Instant::now();
            config
                .ops
                .apply(&mut file.pens_offset.data.pens, &mut file.layers_offset);
            trace!("Config processing time: {:?}", time.elapsed());

            // Process output
            args.output.map(|output| {
                // Serialize to memory buffer for perf
                let mut buffer: Cursor<Vec<u8>> = Cursor::new(vec![]);
                let time: Instant = Instant::now();
                buffer
                    .write_le(&file)
                    .expect("Failed to serialize contents as EZCAD format");
                trace!("Output file encode time: {:?}", time.elapsed());

                // Write buffer to output file
                if !Path::new(&output).exists()
                    || args.overwrite
                    || Confirm::new()
                        .with_prompt(format!(
                            "File '{}' exists! Overwrite?",
                            &output.to_string_lossy()
                        ))
                        .interact()
                        .unwrap()
                {
                    info!("Writing output file '{}'", output.to_string_lossy());
                    let mut output: File =
                        File::create(output).expect("Failed to open output file");
                    output
                        .write_all(buffer.into_inner().as_slice())
                        .expect("Failed to write to output file");
                } else {
                    warn!("Skipping write due to file conflict");
                }
            });
        }
    }
}
