//! Low-power I2C driver

use crate::{
    gpio::lp_io::LowPowerOutputOpenDrain,
    peripherals::{LP_AON, LP_I2C0, LP_IO, LP_PERI, LPWR},
    time::Rate,
};

const LP_I2C_FILTER_CYC_NUM_DEF: u8 = 7;

/// I2C-specific transmission errors
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub enum Error {
    /// The transmission exceeded the FIFO size.
    ExceedingFifo,
    /// The acknowledgment check failed.
    AckCheckFailed,
    /// A timeout occurred during transmission.
    TimeOut,
    /// The arbitration for the bus was lost.
    ArbitrationLost,
    /// The execution of the I2C command was incomplete.
    ExecIncomplete,
    /// The number of commands issued exceeded the limit.
    CommandNrExceeded,
    /// The response received from the I2C device was invalid.
    InvalidResponse,
}

#[allow(unused)]
enum OperationType {
    Write = 0,
    Read  = 1,
}

#[allow(unused)]
#[derive(Eq, PartialEq, Copy, Clone)]
enum Ack {
    Ack,
    Nack,
}

#[derive(PartialEq)]
#[allow(unused)]
enum Command {
    Start,
    Stop,
    End,
    Write {
        /// This bit is to set an expected ACK value for the transmitter.
        ack_exp: Ack,
        /// Enables checking the ACK value received against the ack_exp
        /// value.
        ack_check_en: bool,
        /// Length of data (in bytes) to be written. The maximum length is
        /// 255, while the minimum is 1.
        length: u8,
    },
    Read {
        /// Indicates whether the receiver will send an ACK after this byte
        /// has been received.
        ack_value: Ack,
        /// Length of data (in bytes) to be read. The maximum length is 255,
        /// while the minimum is 1.
        length: u8,
    },
}

// https://github.com/espressif/esp-idf/blob/master/components/ulp/lp_core/lp_core_i2c.c#L122
// TX/RX RAM size is 16*8 bit
// TX RX FIFO has 16 bit depth
// The clock source of APB_CLK in LP_I2C is CLK_AON_FAST.
// Configure LP_I2C_SCLK_SEL to select the clock source for I2C_SCLK.
// When LP_I2C_SCLK_SEL is 0, select CLK_ROOT_FAST as clock source,
// and when LP_I2C_SCLK_SEL is 1, select CLK _XTALD2 as the clock source.
// Configure LP_EXT_I2C_CK_EN high to enable the clock source of I2C_SCLK.
// Adjust the timing registers accordingly when the clock frequency changes.

/// Represents a Low-Power I2C peripheral.
pub struct LpI2c {
    i2c: LP_I2C0<'static>,
}

impl LpI2c {
    /// Creates a new instance of the `LpI2c` peripheral.
    pub fn new(
        i2c: LP_I2C0<'static>,
        _sda: LowPowerOutputOpenDrain<'_, 6>,
        _scl: LowPowerOutputOpenDrain<'_, 7>,
        frequency: Rate,
    ) -> Self {
        let me = Self { i2c };

        // Configure LP I2C GPIOs
        // Initialize IO Pins
        let lp_io = LP_IO::regs();
        let lp_aon = LP_AON::regs();
        let lp_peri = LP_PERI::regs();

        unsafe {
            // FIXME: use GPIO APIs to configure pins
            lp_aon
                .gpio_mux()
                .modify(|r, w| w.sel().bits(r.sel().bits() | (1 << 6)));
            lp_aon
                .gpio_mux()
                .modify(|r, w| w.sel().bits(r.sel().bits() | (1 << 7)));
            lp_io.gpio(6).modify(|_, w| w.mcu_sel().bits(1)); // TODO

            lp_io.gpio(7).modify(|_, w| w.mcu_sel().bits(1));

            // Set output mode to Normal
            lp_io.pin(6).modify(|_, w| w.pad_driver().set_bit());
            // Enable output (writing to write-1-to-set register, then internally the
            // `GPIO_OUT_REG` will be set)
            lp_io
                .out_enable_w1ts()
                .write(|w| w.enable_w1ts().bits(1 << 6));
            // Enable input
            lp_io.gpio(6).modify(|_, w| w.fun_ie().set_bit());

            // Disable pulldown (enable internal weak pull-down)
            lp_io.gpio(6).modify(|_, w| w.fun_wpd().clear_bit());
            // Enable pullup
            lp_io.gpio(6).modify(|_, w| w.fun_wpu().set_bit());

            // Same process for SCL pin
            lp_io.pin(7).modify(|_, w| w.pad_driver().set_bit());
            // Enable output (writing to write-1-to-set register, then internally the
            // `GPIO_OUT_REG` will be set)
            lp_io
                .out_enable_w1ts()
                .write(|w| w.enable_w1ts().bits(1 << 7));
            // Enable input
            lp_io.gpio(7).modify(|_, w| w.fun_ie().set_bit());
            // Disable pulldown (enable internal weak pull-down)
            lp_io.gpio(7).modify(|_, w| w.fun_wpd().clear_bit());
            // Enable pullup
            lp_io.gpio(7).modify(|_, w| w.fun_wpu().set_bit());

            // Select LP I2C function for the SDA and SCL pins
            lp_io.gpio(6).modify(|_, w| w.mcu_sel().bits(1));
            lp_io.gpio(7).modify(|_, w| w.mcu_sel().bits(1));
        }

        // Initialize LP I2C HAL */
        me.i2c
            .register_block()
            .clk_conf()
            .modify(|_, w| w.sclk_active().set_bit());

        // Enable LP I2C controller clock
        lp_peri
            .clk_en()
            .modify(|_, w| w.lp_ext_i2c_ck_en().set_bit());

        lp_peri
            .reset_en()
            .modify(|_, w| w.lp_ext_i2c_reset_en().set_bit());
        lp_peri
            .reset_en()
            .modify(|_, w| w.lp_ext_i2c_reset_en().clear_bit());

        // Set LP I2C source clock
        LPWR::regs()
            .lpperi()
            .modify(|_, w| w.lp_i2c_clk_sel().clear_bit());

        // Initialize LP I2C Master mode
        me.i2c.register_block().ctr().modify(|_, w| unsafe {
            // Clear register
            w.bits(0);
            // Use open drain output for SDA and SCL
            w.sda_force_out().set_bit();
            w.scl_force_out().set_bit();
            // Ensure that clock is enabled
            w.clk_en().set_bit()
        });

        // First, reset the fifo buffers
        me.i2c
            .register_block()
            .fifo_conf()
            .modify(|_, w| w.nonfifo_en().clear_bit());

        me.i2c.register_block().ctr().modify(|_, w| {
            w.tx_lsb_first().clear_bit();
            w.rx_lsb_first().clear_bit()
        });

        me.reset_fifo();

        // Set LP I2C source clock
        LPWR::regs()
            .lpperi()
            .modify(|_, w| w.lp_i2c_clk_sel().clear_bit());

        // Configure LP I2C timing paramters. source_clk is ignored for LP_I2C in this
        // call

        let source_clk = 16_000_000;
        let bus_freq = frequency.as_hz();

        let clkm_div: u32 = source_clk / (bus_freq * 1024) + 1;
        let sclk_freq: u32 = source_clk / clkm_div;
        let half_cycle: u32 = sclk_freq / bus_freq / 2;

        // SCL
        let scl_low = half_cycle;
        // default, scl_wait_high < scl_high
        // Make 80KHz as a boundary here, because when working at lower frequency, too
        // much scl_wait_high will faster the frequency according to some
        // hardware behaviors.
        let scl_wait_high = if bus_freq >= 80 * 1000 {
            half_cycle / 2 - 2
        } else {
            half_cycle / 4
        };
        let scl_high = half_cycle - scl_wait_high;
        let sda_hold = half_cycle / 4;
        let sda_sample = half_cycle / 2; // TODO + scl_wait_high;
        let setup = half_cycle;
        let hold = half_cycle;
        // default we set the timeout value to about 10 bus cycles
        // log(20*half_cycle)/log(2) = log(half_cycle)/log(2) +  log(20)/log(2)
        let tout = (4 * 8 - (5 * half_cycle).leading_zeros()) + 2;

        // According to the Technical Reference Manual, the following timings must be
        // subtracted by 1. However, according to the practical measurement and
        // some hardware behaviour, if wait_high_period and scl_high minus one.
        // The SCL frequency would be a little higher than expected. Therefore, the
        // solution here is not to minus scl_high as well as scl_wait high, and
        // the frequency will be absolutely accurate to all frequency
        // to some extent.
        let scl_low_period = scl_low - 1;
        let scl_high_period = scl_high;
        let scl_wait_high_period = scl_wait_high;
        // sda sample
        let sda_hold_time = sda_hold - 1;
        let sda_sample_time = sda_sample - 1;
        // setup
        let scl_rstart_setup_time = setup - 1;
        let scl_stop_setup_time = setup - 1;
        // hold
        let scl_start_hold_time = hold - 1;
        let scl_stop_hold_time = hold - 1;
        let time_out_value = tout;
        let time_out_en = true;

        // Write data to registers
        unsafe {
            me.i2c.register_block().clk_conf().modify(|_, w| {
                w.sclk_sel().clear_bit();
                w.sclk_div_num().bits((clkm_div - 1) as u8)
            });

            // scl period
            me.i2c
                .register_block()
                .scl_low_period()
                .write(|w| w.scl_low_period().bits(scl_low_period as u16));

            me.i2c.register_block().scl_high_period().write(|w| {
                w.scl_high_period().bits(scl_high_period as u16);
                w.scl_wait_high_period().bits(scl_wait_high_period as u8)
            });
            // sda sample
            me.i2c
                .register_block()
                .sda_hold()
                .write(|w| w.time().bits(sda_hold_time as u16));
            me.i2c
                .register_block()
                .sda_sample()
                .write(|w| w.time().bits(sda_sample_time as u16));

            // setup
            me.i2c
                .register_block()
                .scl_rstart_setup()
                .write(|w| w.time().bits(scl_rstart_setup_time as u16));
            me.i2c
                .register_block()
                .scl_stop_setup()
                .write(|w| w.time().bits(scl_stop_setup_time as u16));

            // hold
            me.i2c
                .register_block()
                .scl_start_hold()
                .write(|w| w.time().bits(scl_start_hold_time as u16));
            me.i2c
                .register_block()
                .scl_stop_hold()
                .write(|w| w.time().bits(scl_stop_hold_time as u16));

            me.i2c.register_block().to().write(|w| {
                w.time_out_en().bit(time_out_en);
                w.time_out_value().bits(time_out_value.try_into().unwrap())
            });
        }

        // Enable SDA and SCL filtering. This configuration matches the HP I2C filter
        // config

        me.i2c
            .register_block()
            .filter_cfg()
            .modify(|_, w| unsafe { w.sda_filter_thres().bits(LP_I2C_FILTER_CYC_NUM_DEF) });
        me.i2c
            .register_block()
            .filter_cfg()
            .modify(|_, w| unsafe { w.scl_filter_thres().bits(LP_I2C_FILTER_CYC_NUM_DEF) });

        me.i2c
            .register_block()
            .filter_cfg()
            .modify(|_, w| w.sda_filter_en().set_bit());
        me.i2c
            .register_block()
            .filter_cfg()
            .modify(|_, w| w.scl_filter_en().set_bit());

        // Configure the I2C master to send a NACK when the Rx FIFO count is full
        me.i2c
            .register_block()
            .ctr()
            .modify(|_, w| w.rx_full_ack_level().set_bit());

        // Synchronize the config register values to the LP I2C peripheral clock
        me.lp_i2c_update();

        me
    }

    /// Update I2C configuration
    fn lp_i2c_update(&self) {
        self.i2c
            .register_block()
            .ctr()
            .modify(|_, w| w.conf_upgate().set_bit());
    }

    /// Resets the transmit and receive FIFO buffers.
    fn reset_fifo(&self) {
        self.i2c
            .register_block()
            .fifo_conf()
            .modify(|_, w| w.tx_fifo_rst().set_bit());

        self.i2c
            .register_block()
            .fifo_conf()
            .modify(|_, w| w.tx_fifo_rst().clear_bit());

        self.i2c
            .register_block()
            .fifo_conf()
            .modify(|_, w| w.rx_fifo_rst().set_bit());

        self.i2c
            .register_block()
            .fifo_conf()
            .modify(|_, w| w.rx_fifo_rst().clear_bit());
    }
}
