use stm32l4xx_hal::{
    prelude::{
        _embedded_hal_blocking_i2c_Read, _embedded_hal_blocking_i2c_Write,
        _embedded_hal_blocking_i2c_WriteRead,
    },
};

use crate::pca9685;

pub const DS3231_ADDR: u8 = 0x68;
pub const TUSB322_ADDR: u8 = 0x47;
pub const PCA9685_ADDR_1: u8 = 0x41;
pub const PCA9685_ADDR_2: u8 = 0x42;
pub const PCA9685_ADDR_3: u8 = 0x43;

pub const PCA9685_ALL_CALL: u8 = 0x70; // Default enabled
pub const PCA9685_SUB_CALL_1: u8 = 0x71; // Default disabled
pub const PCA9685_SUB_CALL_2: u8 = 0x72; // Default disabled
pub const PCA9685_SUB_CALL_3: u8 = 0x73; // Default disabled

pub const DISPLAY_REFRESH_FPS: u32 = 500;

pub const DIGIT_FADE_DURATION_MS: u32 = 1000;
pub const CYCLE_FADE_DURATION_MS: u32 = 200;
pub const CYCLE_ITERATIONS: usize = 20;
pub const CYCLE_REFRESH_INTERVAL: u32 = 60;
pub const CYCLE_REFRESH_VARIANCE: u32 = 30;

const DOT_MIN_BRIGHTNESS: u32 = 256;
const DOT_MAX_BRIGHTNESS: u32 = 640;
const DOT_FADE_DURATION_US: u32 = 1_000_000;

const DIGIT_MAX_BRIGHTNESS: u32 = 4096;
const DIGIT_MIN_BRIGHTNESS: u32 = 0;

const NUM_TUBES: usize = 4;
const NUM_DIGITS: usize = 10;

const MAP_DOT_ADDR: u8 = PCA9685_ADDR_2;
const MAP_DOT_PIN: u8 = 15;

const MAP_ADDR: usize = 0;
const MAP_PIN: usize = 1;

struct DigitToPin {
    address: u8,
    pin: usize,
}

struct PwmDriver {
    digit: [DigitToPin; 10],
}

struct PwmOutputMap {
    driver: [PwmDriver; 4],
    dot_address: u8,
    dot_pin: usize,
}

static TUBE_MAPPING: PwmOutputMap = {
    PwmOutputMap {
        driver: [
            PwmDriver {
                digit: [
                    DigitToPin { address: PCA9685_ADDR_1, pin: 8, }, // Tube 0 Digit 0
                    DigitToPin { address: PCA9685_ADDR_1, pin: 9, }, // Tube 0 Digit 1
                    DigitToPin { address: PCA9685_ADDR_1, pin: 10, }, // Tube 0 Digit 2
                    DigitToPin { address: PCA9685_ADDR_1, pin: 12, }, // Tube 0 Digit 3
                    DigitToPin { address: PCA9685_ADDR_1, pin: 15, }, // Tube 0 Digit 4
                    DigitToPin { address: PCA9685_ADDR_1, pin: 14, }, // Tube 0 Digit 5
                    DigitToPin { address: PCA9685_ADDR_1, pin: 11, }, // Tube 0 Digit 6
                    DigitToPin { address: PCA9685_ADDR_1, pin: 0, }, // Tube 0 Digit 7
                    DigitToPin { address: PCA9685_ADDR_1, pin: 1, }, // Tube 0 Digit 8
                    DigitToPin { address: PCA9685_ADDR_1, pin: 13, }, // Tube 0 Digit 9
                ],
            },
            PwmDriver {
                digit: [
                    DigitToPin { address: PCA9685_ADDR_1, pin: 5, }, // Tube 1 Digit 0
                    DigitToPin { address: PCA9685_ADDR_1, pin: 6, }, // Tube 1 Digit 1
                    DigitToPin { address: PCA9685_ADDR_1, pin: 7, }, // Tube 1 Digit 2
                    DigitToPin { address: PCA9685_ADDR_1, pin: 2, }, // Tube 1 Digit 3
                    DigitToPin { address: PCA9685_ADDR_2, pin: 4, }, // Tube 1 Digit 4
                    DigitToPin { address: PCA9685_ADDR_2, pin: 1, }, // Tube 1 Digit 5
                    DigitToPin { address: PCA9685_ADDR_1, pin: 4, }, // Tube 1 Digit 6
                    DigitToPin { address: PCA9685_ADDR_2, pin: 2, }, // Tube 1 Digit 7
                    DigitToPin { address: PCA9685_ADDR_2, pin: 3, }, // Tube 1 Digit 8
                    DigitToPin { address: PCA9685_ADDR_1, pin: 3, }, // Tube 1 Digit 9
                ],
            },
            PwmDriver {
                digit: [
                    DigitToPin { address: PCA9685_ADDR_3, pin: 8, }, // Tube 2 Digit 0
                    DigitToPin { address: PCA9685_ADDR_3, pin: 9, }, // Tube 2 Digit 1
                    DigitToPin { address: PCA9685_ADDR_3, pin: 10, }, // Tube 2 Digit 2
                    DigitToPin { address: PCA9685_ADDR_3, pin: 12, }, // Tube 2 Digit 3
                    DigitToPin { address: PCA9685_ADDR_2, pin: 12, }, // Tube 2 Digit 4
                    DigitToPin { address: PCA9685_ADDR_2, pin: 13, }, // Tube 2 Digit 5
                    DigitToPin { address: PCA9685_ADDR_3, pin: 11, }, // Tube 2 Digit 6
                    DigitToPin { address: PCA9685_ADDR_2, pin: 14, }, // Tube 2 Digit 7
                    DigitToPin { address: PCA9685_ADDR_2, pin: 11, }, // Tube 2 Digit 8
                    DigitToPin { address: PCA9685_ADDR_3, pin: 13, }, // Tube 2 Digit 9
                ],
            },
            PwmDriver {
                digit: [
                    DigitToPin { address: PCA9685_ADDR_3, pin: 5, }, // Tube 3 Digit 0
                    DigitToPin { address: PCA9685_ADDR_3, pin: 6, }, // Tube 3 Digit 1
                    DigitToPin { address: PCA9685_ADDR_3, pin: 7, }, // Tube 3 Digit 2
                    DigitToPin { address: PCA9685_ADDR_3, pin: 2, }, // Tube 3 Digit 3
                    DigitToPin { address: PCA9685_ADDR_3, pin: 14, }, // Tube 3 Digit 4
                    DigitToPin { address: PCA9685_ADDR_3, pin: 15, }, // Tube 3 Digit 5
                    DigitToPin { address: PCA9685_ADDR_3, pin: 4, }, // Tube 3 Digit 6
                    DigitToPin { address: PCA9685_ADDR_3, pin: 1, }, // Tube 3 Digit 7
                    DigitToPin { address: PCA9685_ADDR_3, pin: 0, }, // Tube 3 Digit 8
                    DigitToPin { address: PCA9685_ADDR_3, pin: 3, }, // Tube 3 Digit 9
                ],
            },
        ],
        dot_address: PCA9685_ADDR_2,
        dot_pin: 15,
    }
};

#[derive(Debug, PartialEq)]
enum State {
    Idle,
    Incrementing,
    Decrementing,
}

struct Digit {
    state: State,
    value: u32,
    pwm_start: u32,
    pwm_end: u32,
    fade_duration: Option<u32>,
    updated: bool,
}

impl Digit {
    const fn default() -> Self {
        Self {
            state: State::Idle,
            value: 0,
            pwm_start: 0,
            pwm_end: 0,
            fade_duration: None,
            updated: false,
        }
    }
}

struct CycleSettings {
    last_digit: Option<u32>,
    next_digit: u32,
    iteration: usize,
    last_fade_duration: u32,
}

struct Tube {
    digits: [Digit; NUM_DIGITS],
    last_digit: Option<u32>,
    cycle: Option<CycleSettings>,
}

impl Tube {
    const fn default() -> Self {
        const DIGIT_INIT: Digit = Digit::default();
        Self {
            digits: [DIGIT_INIT; 10],
            last_digit: None,
            cycle: None,
        }
    }

    fn fade_in_out_digit(&mut self, digit: Option<u32>, fade_duration: u32, cycle_cmd: bool) {
        // If the tube is in the middle of a cycle sequence and a call comes
        // in to update the tube digit (for time), override the last value of
        // the cycle sequence with the new digit.
        if let Some(ref mut cycle) = self.cycle {
            if !cycle_cmd {
                cycle.last_digit = digit;
                cycle.last_fade_duration = fade_duration;
            }
        }

        // Dont update if actively cycling tube unless cycle_cmd is set
        if (self.cycle.is_none() && !cycle_cmd) || cycle_cmd {
            // Fade out all digits
            for digit in 0..NUM_DIGITS {
                self.digits[digit].state = State::Decrementing;
                self.digits[digit].fade_duration = Some(fade_duration);
            }

            // Fade in the specified digit
            if let Some(digit) = digit {
                self.digits[digit as usize].state = State::Incrementing;
                self.digits[digit as usize].fade_duration = Some(fade_duration);
            }

            self.last_digit = digit;
        }
    }
}

pub struct Clock {
    tubes: [Tube; NUM_TUBES],
    dot: Digit,
    minute: Option<u32>,
    hour: Option<u32>,
}

impl Clock {
    pub const fn default() -> Self {
        const TUBE_INIT: Tube = Tube::default();
        Self {
            tubes: [TUBE_INIT; NUM_TUBES],
            dot: Digit::default(),
            minute: None,
            hour: None,
        }
    }

    // Sets a new time to be displayed
    pub fn rtc_tick(&mut self, second: u32, minute: u32, hour: u32) {

        // Update digit for each tube if value has changed
        match self.hour {
            Some(prev_hour) if prev_hour / 10 == hour / 10 => {
                if hour / 10 == 0 {
                    self.tubes[0].fade_in_out_digit(None, DIGIT_FADE_DURATION_MS, false);
                }
            }
            _ => {
                self.tubes[0].fade_in_out_digit(Some(hour / 10), DIGIT_FADE_DURATION_MS, false);
            }
        }

        match self.hour {
            Some(prev_hour) if prev_hour % 10 == hour % 10 => {}
            _ => {
                self.tubes[1].fade_in_out_digit(Some(hour % 10), DIGIT_FADE_DURATION_MS, false);
            }
        }

        match self.minute {
            Some(prev_minute) if prev_minute / 10 == minute / 10 => {}
            _ => {
                self.tubes[2].fade_in_out_digit(Some(minute / 10), DIGIT_FADE_DURATION_MS, false);
            }
        }

        match self.minute {
            Some(prev_minute) if prev_minute % 10 == minute % 10 => {}
            _ => {
                self.tubes[3].fade_in_out_digit(Some(minute % 10), DIGIT_FADE_DURATION_MS, false);
            }
        }

        #[cfg(test)]
        println!(
            "RTC tick: {}{}:{}{}",
            hour / 10,
            hour % 10,
            minute / 10,
            minute % 10
        );

        // Set fade direction for dot
        self.dot.state = match second % 2 {
            0 => State::Incrementing,
            1 => State::Decrementing,
            _ => State::Idle,
        };
        self.dot.fade_duration = Some(DIGIT_FADE_DURATION_MS);

        #[cfg(test)]
        println!("RTC tick: dot state is {:?}", self.dot.state);

        // Store the last set value for the next update
        self.hour = Some(hour);
        self.minute = Some(minute);
    }

    // Updates the display with values due to fade in/out
    // Returns true if values have changed
    pub fn fps_tick(&mut self) -> bool {
        let mut pending_refresh: bool = false;

        let mut update_fn = |digit: &mut Digit, min: u32, max: u32, steps: u32| {
            match digit.state {
                State::Incrementing => {
                    if digit.value >= max {
                        digit.value = max;
                        digit.state = State::Idle;
                    } else {
                        digit.value = digit.value.saturating_add(steps).clamp(min, max);
                        digit.updated = true;
                        pending_refresh = true;
                    }
                }
                State::Decrementing => {
                    if digit.value <= min {
                        digit.value = min;
                        digit.state = State::Idle;
                    } else {
                        digit.value = digit.value.saturating_sub(steps).clamp(min, max);
                        digit.updated = true;
                        pending_refresh = true;
                    }
                }
                State::Idle => {
                    digit.fade_duration = None;
                }
            };
        };

        #[cfg(not(test))]
        self.tubes.iter_mut().for_each(|tube| {
            tube.digits.iter_mut().for_each(|digit| {
                if let Some(fade_duration) = digit.fade_duration {
                    let ticks = fade_duration * 1000 / (1000 / DISPLAY_REFRESH_FPS * 1000);
                    let steps = ((DIGIT_MAX_BRIGHTNESS - DIGIT_MIN_BRIGHTNESS) + ticks - 1) / ticks;
                    update_fn(digit, DIGIT_MIN_BRIGHTNESS, DIGIT_MAX_BRIGHTNESS, steps);
                }
            });
        });

        #[cfg(test)]
        for (t, tube) in self.tubes.iter_mut().enumerate() {
            for (d, digit) in tube.digits.iter_mut().enumerate() {
                if let Some(fade_duration) = digit.fade_duration {
                    let ticks = fade_duration * 1000 / (1000 / DISPLAY_REFRESH_FPS * 1000);
                    let steps = ((DIGIT_MAX_BRIGHTNESS - DIGIT_MIN_BRIGHTNESS) + ticks - 1) / ticks;
                    update_fn(digit, DIGIT_MIN_BRIGHTNESS, DIGIT_MAX_BRIGHTNESS, steps);
                }

                if digit.updated {
                    println!(
                        "Refresh tick: updated tube {} digit {} to value {}",
                        t, d, digit.value
                    );
                }
            }
        }

        // Update dot values
        if let Some(fade_duration) = self.dot.fade_duration {
            let ticks = fade_duration * 1000 / (1000 / DISPLAY_REFRESH_FPS * 1000);
            let steps = ((DOT_MAX_BRIGHTNESS - DOT_MIN_BRIGHTNESS) + ticks - 1) / ticks;
            update_fn(&mut self.dot, DOT_MIN_BRIGHTNESS, DOT_MAX_BRIGHTNESS, steps);
        }

        #[cfg(test)]
        if self.dot.updated {
            println!("Refresh tick: updated dot to value {}", self.dot.value);
        }

        // Compute actual PWM values if display values have changed
        if pending_refresh {
            self.distribute_pwm();
        }

        pending_refresh
    }

    // Updates the digit displayed during a cycle sequence
    // Returns true if the cycle sequence has completed
    pub fn cycle_tick(&mut self) -> bool {
        let mut cycle_ended = true;

        self.tubes.iter_mut().for_each(|tube| {
            if let Some(cycle) = tube.cycle.as_mut() {
                #[cfg(test)]
                println!("Cycle tick: iteration {}", cycle.iteration);

                if cycle.iteration > 0 {
                    let next_digit = cycle.next_digit;
                    cycle.next_digit = if cycle.next_digit == 9 { 0 } else { cycle.next_digit + 1 };
                    cycle.iteration = cycle.iteration - 1;
                    tube.fade_in_out_digit(Some(next_digit), CYCLE_FADE_DURATION_MS, true);
                    cycle_ended = false;
                } else {
                    let last_digit = cycle.last_digit;
                    let last_fade = cycle.last_fade_duration;
                    tube.cycle = None;
                    tube.fade_in_out_digit(last_digit, last_fade, false);
                }
            }
        });

        cycle_ended
    }

    // Start cycling sequence for the given tube to prevent long term damage from cathode poisoning
    pub fn cycle_start(&mut self, tube: usize) {
        self.tubes[tube].cycle = Some(CycleSettings {
            last_digit: self.tubes[tube].last_digit,
            next_digit: 0,
            iteration: CYCLE_ITERATIONS,
            last_fade_duration: DIGIT_FADE_DURATION_MS,
        });
    }

    // Writes updated PWM values to each PCA9685 
    pub fn write_i2c<T>(&mut self, i2c: &mut T)
    where
        T: _embedded_hal_blocking_i2c_WriteRead
            + _embedded_hal_blocking_i2c_Read
            + _embedded_hal_blocking_i2c_Write,
    {
        for (t, tube) in self.tubes.iter_mut().enumerate() {
            for (d, digit) in tube.digits.iter_mut().enumerate() {
                if digit.updated {
                    pca9685::set_digit(
                        i2c,
                        TUBE_MAPPING.driver[t].digit[d].address,
                        TUBE_MAPPING.driver[t].digit[d].pin,
                        digit.pwm_start,
                        digit.pwm_end,
                    );
                    digit.updated = false;
                }
            }
        }

        if self.dot.updated {
            pca9685::set_digit(
                i2c,
                TUBE_MAPPING.dot_address,
                TUBE_MAPPING.dot_pin,
                self.dot.pwm_start,
                self.dot.pwm_end,
            );
            self.dot.updated = false;
        }
    }

    // In the event that there are multiple PWM outputs at less than 100% duty cycle,
    // stagger the start time of each PWM to reduce the switch on surge current. If the
    // duty cycle is greater than 100%, distribute the PWM outputs as much as possible
    // to keep the current consumption at a minimum.
    fn distribute_pwm(&mut self) {
        let mut last_pwm: u32 = 0;
        let mut incrementing: bool = true;

        // Closure to avoid duplicate code
        let mut update_digit = |digit: &mut Digit| {
            if digit.value == DIGIT_MIN_BRIGHTNESS {
                digit.pwm_start = 0;
                digit.pwm_end = 0;
            } else if digit.value == DIGIT_MAX_BRIGHTNESS {
                digit.pwm_start = 0;
                digit.pwm_end = DIGIT_MAX_BRIGHTNESS;
            } else {
                if incrementing {
                    if last_pwm + digit.value > DIGIT_MAX_BRIGHTNESS {
                        digit.pwm_start = DIGIT_MAX_BRIGHTNESS - digit.value;
                        digit.pwm_end = DIGIT_MAX_BRIGHTNESS;
                        last_pwm = digit.pwm_start;
                        incrementing = false;
                    } else {
                        digit.pwm_start = last_pwm;
                        digit.pwm_end = digit.pwm_start + digit.value;
                        last_pwm = digit.pwm_end;
                    }
                } else {
                    if last_pwm - DIGIT_MIN_BRIGHTNESS < digit.value {
                        digit.pwm_start = DIGIT_MIN_BRIGHTNESS;
                        digit.pwm_end = digit.pwm_start + digit.value;
                        last_pwm = digit.pwm_end;
                        incrementing = true;
                    } else {
                        digit.pwm_end = last_pwm;
                        digit.pwm_start = digit.pwm_end - digit.value;
                        last_pwm = digit.pwm_start;
                    }
                }
                digit.updated = true;
            }
        };

        #[cfg(not(test))]
        self.tubes.iter_mut().for_each(|tube| {
            tube.digits.iter_mut().for_each(|digit| {
                update_digit(digit);
            });
        });

        #[cfg(test)]
        for (t, tube) in self.tubes.iter_mut().enumerate() {
            for (d, digit) in tube.digits.iter_mut().enumerate() {
                update_digit(digit);

                if digit.updated {
                    println!(
                        "Distribute PWM: tube {} digit {} start {} end {}",
                        t, d, digit.pwm_start, digit.pwm_end
                    );
                }
            }
        }

        // The dot is somewhat sensitive to changes to PWM signal, so for the sake
        // of consistency, keep the signal on time at the end of the PWM period.
        // Otherwise the distribution algorithm will sometimes place the on time at
        // the end of the PWM period in one cycle and at the start of the period for
        // the next, which results in visual flickers.
        // update_digit(&mut self.dot);
        self.dot.pwm_start = DIGIT_MAX_BRIGHTNESS - self.dot.value;
        self.dot.pwm_end = DIGIT_MAX_BRIGHTNESS;

        #[cfg(test)]
        println!(
            "Distribute PWM: dot start {} end {}",
            self.dot.pwm_start, self.dot.pwm_end
        );
    }
}

#[cfg(test)]
mod test {
    use super::*;
    use std::println;

    #[test]
    fn pwm_calc_test() {
        let mut clock: Clock = Clock::default();

        // Initialize clock to arbitrary time
        clock.rtc_tick(10, 8, 12);

        // Iterate and print output values for each display refresh tick
        for tick in 0..1000 {
            println!("\nRefresh tick: {}", tick);
            if !clock.fps_tick() {
                println!("Refresh halted");
                break;
            }

            // Reset the updated field for each digit
            clock.tubes.iter_mut().for_each(|tube| {
                tube.digits.iter_mut().for_each(|digit| {
                    digit.updated = false;
                });
            });
            clock.dot.updated = false;
        }
    }

    #[test]
    fn cycle_test() {
        let mut clock: Clock = Clock::default();

        // Initialize clock to arbitrary time
        clock.rtc_tick(00, 27, 8);

        // Simulate a cycle refresh sequence on tube 0
        clock.tubes[0].cycle = Some(CycleSettings {
            last_digit: clock.tubes[0].last_digit,
            next_digit: 0,
            iteration: CYCLE_ITERATIONS,
            last_fade_duration: DIGIT_FADE_DURATION_MS,
        });

        // Iterate and print debug values for each cycle
        for cycle in 0..1000 {
            println!("\nCycle tick: {}", cycle);
            if clock.cycle_tick() {
                println!("Cycle halted");
                break;
            }

            // Iterate and print output values for each display refresh tick
            for tick in 0..1000 {
                println!("\nRefresh tick: {}", tick);
                if !clock.fps_tick() {
                    println!("Refresh halted");
                    break;
                }

                // Reset the updated field for each digit
                clock.tubes.iter_mut().for_each(|tube| {
                    tube.digits.iter_mut().for_each(|digit| {
                        digit.updated = false;
                    });
                });
                clock.dot.updated = false;
            }
        }
    }
}
