use std::{
    fmt::Debug,
    fs::File,
    io::{Cursor, Read, Write},
    path::PathBuf,
};

use binrw::{BinRead, BinWrite, BinWriterExt, FilePtr64};
use diff::Diff;
use human_repr::HumanCount;
use log::{error, warn};
use num_enum::TryFromPrimitive;

use crate::{
    field_of::FieldOf,
    types::{Bool, Field, PulseWidth, Rgba, WString, WobbleType, F64, U32},
    FP,
};

#[derive(BinRead, Debug)]
pub struct PenHeader {
    pub pen_count: u32,
    #[br(args {
        inner: binrw::args! {
            pen_count
        }
    })]
    pub data: FilePtr64<Pens>,
}

// Manually implement BinWrite as FilePtr does not support serialization
// See: https://github.com/jam1garner/binrw/issues/4
impl BinWrite for PenHeader {
    type Args<'a> = ();

    fn write_options<W: std::io::prelude::Write + std::io::prelude::Seek>(
        &self,
        writer: &mut W,
        endian: binrw::Endian,
        args: Self::Args<'_>,
    ) -> binrw::prelude::BinResult<()> {
        let pen_count: u32 = self.data.pens.len().try_into().unwrap();
        pen_count.write_options(writer, endian, args)?;

        // Write address of data, which is placed after this field
        let data_offset: u64 = writer.stream_position().unwrap() + 8;
        data_offset.write_options(writer, endian, args)?;

        self.data.pens.write_options(writer, endian, args)
    }
}

#[derive(BinRead, BinWrite, Debug, Diff, PartialEq)]
#[diff(attr(
    #[derive(Debug, PartialEq)]
))]
#[br(import {pen_count: u32})]
pub struct Pens {
    #[br(count = pen_count)]
    pub pens: Vec<Pen>,
}

#[cfg_attr(feature = "default-debug", derive(Debug))]
#[derive(BinRead, BinWrite, Clone, Diff, PartialEq)]
#[diff(attr(
    #[derive(Debug, PartialEq)]
))]
// #[brw(magic(236u32))] // Number of fields within this struct
#[brw(magic(370u32))] // Number of fields within this struct
pub struct Pen {
    pub color: FieldOf<Rgba>,
    pub name: WString,
    pub disabled: Bool,
    pub use_default: Bool,
    pub loop_count: U32,
    pub speed: F64, // Changes with wobble relative speed
    pub power: F64,
    pub frequency: U32,
    pub pulse_width: U32,
    pub start_tc: U32,
    pub end_tc: U32,
    pub polygon_tc: U32,
    pub jump_speed: F64,
    _unknown_2: [Field; 10],
    pub laser_off_tc: U32,
    pub wave: U32, // Only available if continue_mode is false
    pub pulse_width_2: F64,
    pub wobble_enable: U32,
    pub wobble_diameter: F64,
    pub wobble_distance: F64,
    _unknown_4: [Field; 8],
    pub min_jump_tc: U32,
    pub max_jump_tc: U32,
    pub jump_limit: F64,
    _unknown_5: [Field; 2],
    pub frequency_2: F64,
    _unknown_6: [Field; 152],
    pub wobble_type: FieldOf<WobbleType>,
    pub continue_mode: U32,
    _unknown_7: [Field; 12],
    pub wobble_diameter_2: F64, // Only with wobble type ellipse
    _unknown_8: [Field; 26],
    _unknown_9: [Field; 134],
}

impl Pen {
    pub fn read_from_file(path: &PathBuf) -> Self {
        let mut input: File = File::open(path).expect("Failed to open input file");

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

        Pen::read_le(&mut buffer).expect("Failed to deserialize input as object")
    }

    pub fn write_to_file(&self, path: &PathBuf) {
        let mut buffer: Cursor<Vec<u8>> = Cursor::new(vec![]);
        buffer.write_le(self).expect("Failed to serialize object");

        let mut output: File = File::create(path).expect("Failed to open output file");
        output
            .write_all(buffer.into_inner().as_slice())
            .expect("Failed to write to output file");
    }

    pub fn valid_settings(&self, warn_limits: bool) -> bool {
        let mut ret: bool = true;

        if *self.frequency != *self.frequency_2 as u32 {
            error!(
                "Mismatch pen internal frequency setting: ({}, {:.FP$})",
                *self.frequency, *self.frequency_2
            );
            ret = false;
        }

        if *self.pulse_width != *self.pulse_width_2 as u32 {
            error!(
                "Mismatch pen internal pulse width setting: ({}, {:.FP$})",
                *self.pulse_width, *self.pulse_width_2
            );
            ret = false;
        }

        match PulseWidth::try_from(*self.pulse_width) {
            Ok(pw) => match *self.frequency {
                freq if freq < pw.min_freq() => {
                    if warn_limits {
                        warn!(
                            "Pen frequency of {} lower than pulse width minimum frequency of {}",
                            *self.frequency,
                            pw.min_freq()
                        );
                    }
                    ret = false;
                }
                freq if freq > pw.max_freq() => {
                    if warn_limits {
                        warn!(
                            "Pen frequency of {} higher than pulse width maximum frequency of {}",
                            *self.frequency,
                            pw.max_freq()
                        );
                    }
                    ret = false;
                }
                _ => (),
            },
            Err(_) => {
                warn!("Invalid pen pulse width value: {}", *self.pulse_width);
                ret = false;
            }
        }

        ret
    }

    /// Estimate the power density from the speed/frequency/power/pulse
    pub fn power_density(&self) -> u64 {
        let power: f64 = *self.power;
        let speed: f64 = *self.speed;
        let frequency: f64 = f64::from(*self.frequency);
        let pulse: f64 = PulseWidth::try_from_primitive(*self.pulse_width)
            .expect("Invalid pen pulse width")
            .power_ratio();

        (power * (1.0 / speed) * frequency * pulse) as u64
    }
}

impl std::fmt::Display for Pen {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(
            f,
            "Speed: {}mm/s, Frequency: {}, Power: {}%, Pulse Width: {}ns, Power Density: {}",
            self.speed,
            self.frequency.human_count("Hz"),
            self.power,
            self.pulse_width,
            self.power_density()
        )
    }
}

// Custom Debug implementation to only print known fields
#[cfg(not(feature = "default-debug"))]
impl Debug for Pen {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("Pen")
            .field("color", &self.color)
            .field("name", &self.name)
            .field("disabled", &self.disabled)
            .field("use_default", &self.use_default)
            .field("loop_count", &self.loop_count)
            .field("speed", &self.speed)
            .field("power", &self.power)
            .field("frequency", &self.frequency)
            .field("start_tc", &self.start_tc)
            .field("end_tc", &self.end_tc)
            .field("polygon_tc", &self.polygon_tc)
            .field("jump_speed", &self.jump_speed)
            .field("laser_off_tc", &self.laser_off_tc)
            .field("wave", &self.wave)
            .field("wobble_enable", &self.wobble_enable)
            .field("wobble_diameter", &self.wobble_diameter)
            .field("wobble_distance", &self.wobble_distance)
            .field("min_jump_tc", &self.min_jump_tc)
            .field("max_jump_tc", &self.max_jump_tc)
            .field("jump_limit", &self.jump_limit)
            .field("frequency_2", &self.frequency_2)
            .field("wobble_type", &self.wobble_type)
            .field("continue_mode", &self.continue_mode)
            .field("wobble_diameter_2", &self.wobble_diameter_2)
            .finish()
    }
}
