//! Operational Amplifier (OPAMP)
#![macro_use]

use embassy_hal_internal::PeripheralType;

use crate::Peri;
#[cfg(opamp_v5)]
use crate::block_for_us;
use crate::pac::opamp::vals::*;
#[cfg(not(any(stm32g4, stm32f3)))]
use crate::rcc::RccInfo;

/// Gain
#[allow(missing_docs)]
#[derive(Clone, Copy)]
pub enum OpAmpGain {
    Mul2,
    Mul4,
    Mul8,
    Mul16,
    #[cfg(opamp_v5)]
    Mul32,
    #[cfg(opamp_v5)]
    Mul64,
}

#[cfg(opamp_v5)]
enum OpAmpDifferentialPair {
    P,
    N,
}

/// Speed
#[allow(missing_docs)]
#[derive(Clone, Copy, PartialEq)]
pub enum OpAmpSpeed {
    Normal,
    HighSpeed,
}

/// OpAmp external outputs, wired to a GPIO pad.
///
/// This struct can also be used as an ADC input.
pub struct OpAmpOutput<'d, T: Instance> {
    _inner: &'d OpAmp<'d, T>,
}

/// OpAmp internal outputs, wired directly to ADC inputs.
///
/// This struct can be used as an ADC input.
#[cfg(opamp_v5)]
pub struct OpAmpInternalOutput<'d, T: Instance> {
    _inner: &'d OpAmp<'d, T>,
}

/// OpAmp driver.
pub struct OpAmp<'d, T: Instance> {
    _inner: Peri<'d, T>,
}

impl<'d, T: Instance> OpAmp<'d, T> {
    /// Create a new driver instance.
    ///
    /// Does not enable the opamp, but does set the speed mode on some families.
    pub fn new(opamp: Peri<'d, T>, #[cfg(opamp_v5)] speed: OpAmpSpeed) -> Self {
        #[cfg(not(any(stm32g4, stm32f3)))]
        T::info().rcc.enable_and_reset();
        #[cfg(opamp_v5)]
        T::regs().csr().modify(|w| {
            w.set_opahsm(speed == OpAmpSpeed::HighSpeed);
        });

        Self { _inner: opamp }
    }

    /// Configure the OpAmp as a buffer for the provided input pin,
    /// outputting to the provided output pin, and enable the opamp.
    ///
    /// The input pin is configured for analogue mode but not consumed,
    /// so it may subsequently be used for ADC or comparator inputs.
    ///
    /// The output pin is held within the returned [`OpAmpOutput`] struct,
    /// preventing it being used elsewhere. The `OpAmpOutput` can then be
    /// directly used as an ADC input. The opamp will be disabled when the
    /// [`OpAmpOutput`] is dropped.
    pub fn buffer_ext(
        &mut self,
        in_pin: Peri<'_, impl NonInvertingPin<T> + crate::gpio::Pin>,
        out_pin: Peri<'_, impl OutputPin<T> + crate::gpio::Pin>,
    ) -> OpAmpOutput<'_, T> {
        in_pin.set_as_analog();
        out_pin.set_as_analog();

        #[cfg(opamp_v5)]
        let vm_sel = VmSel::OUTPUT;
        #[cfg(not(opamp_v5))]
        let vm_sel = VmSel::from_bits(0b11);

        T::regs().csr().modify(|w| {
            w.set_vp_sel(VpSel::from_bits(in_pin.channel()));
            w.set_vm_sel(vm_sel);
            #[cfg(opamp_v5)]
            w.set_opaintoen(false);
            w.set_opampen(true);
        });

        OpAmpOutput { _inner: self }
    }

    /// Configure the OpAmp as a PGA for the provided input pin,
    /// outputting to the provided output pin, and enable the opamp.
    ///
    /// The input pin is configured for analogue mode but not consumed,
    /// so it may subsequently be used for ADC or comparator inputs.
    ///
    /// The output pin is held within the returned [`OpAmpOutput`] struct,
    /// preventing it being used elsewhere. The `OpAmpOutput` can then be
    /// directly used as an ADC input. The opamp will be disabled when the
    /// [`OpAmpOutput`] is dropped.
    pub fn pga_ext(
        &mut self,
        in_pin: Peri<'_, impl NonInvertingPin<T> + crate::gpio::Pin>,
        out_pin: Peri<'_, impl OutputPin<T> + crate::gpio::Pin>,
        gain: OpAmpGain,
    ) -> OpAmpOutput<'_, T> {
        in_pin.set_as_analog();
        out_pin.set_as_analog();

        #[cfg(opamp_v5)]
        let vm_sel = VmSel::PGA;
        #[cfg(not(opamp_v5))]
        let vm_sel = VmSel::from_bits(0b10);

        #[cfg(opamp_v5)]
        let pga_gain = match gain {
            OpAmpGain::Mul2 => PgaGain::GAIN2,
            OpAmpGain::Mul4 => PgaGain::GAIN4,
            OpAmpGain::Mul8 => PgaGain::GAIN8,
            OpAmpGain::Mul16 => PgaGain::GAIN16,
            OpAmpGain::Mul32 => PgaGain::GAIN32,
            OpAmpGain::Mul64 => PgaGain::GAIN64,
        };
        #[cfg(not(opamp_v5))]
        let pga_gain = PgaGain::from_bits(match gain {
            OpAmpGain::Mul2 => 0b00,
            OpAmpGain::Mul4 => 0b01,
            OpAmpGain::Mul8 => 0b10,
            OpAmpGain::Mul16 => 0b11,
        });

        T::regs().csr().modify(|w| {
            w.set_vp_sel(VpSel::from_bits(in_pin.channel()));
            w.set_vm_sel(vm_sel);
            w.set_pga_gain(pga_gain);
            #[cfg(opamp_v5)]
            w.set_opaintoen(false);
            w.set_opampen(true);
        });

        OpAmpOutput { _inner: self }
    }

    /// Configure the OpAmp as a buffer for the DAC it is connected to,
    /// outputting to the provided output pin, and enable the opamp.
    ///
    /// The output pin is held within the returned [`OpAmpOutput`] struct,
    /// preventing it being used elsewhere. The `OpAmpOutput` can then be
    /// directly used as an ADC input. The opamp will be disabled when the
    /// [`OpAmpOutput`] is dropped.
    #[cfg(opamp_v5)]
    pub fn buffer_dac(&mut self, out_pin: Peri<'_, impl OutputPin<T> + crate::gpio::Pin>) -> OpAmpOutput<'_, T> {
        out_pin.set_as_analog();

        T::regs().csr().modify(|w| {
            use crate::pac::opamp::vals::*;

            w.set_vm_sel(VmSel::OUTPUT);
            w.set_vp_sel(VpSel::DAC3_CH1);
            w.set_opaintoen(false);
            w.set_opampen(true);
        });

        OpAmpOutput { _inner: self }
    }

    /// Configure the OpAmp as a buffer for the provided input pin,
    /// with the output only used internally, and enable the opamp.
    ///
    /// The input pin is configured for analogue mode but not consumed,
    /// so it may be subsequently used for ADC or comparator inputs.
    ///
    /// The returned `OpAmpInternalOutput` struct may be used as an ADC input.
    /// The opamp output will be disabled when it is dropped.
    #[cfg(opamp_v5)]
    pub fn buffer_int(
        &mut self,
        pin: Peri<'_, impl NonInvertingPin<T> + crate::gpio::Pin>,
    ) -> OpAmpInternalOutput<'_, T> {
        pin.set_as_analog();

        T::regs().csr().modify(|w| {
            w.set_vp_sel(VpSel::from_bits(pin.channel()));
            w.set_vm_sel(VmSel::OUTPUT);
            #[cfg(opamp_v5)]
            w.set_opaintoen(true);
            w.set_opampen(true);
        });

        OpAmpInternalOutput { _inner: self }
    }

    /// Configure the OpAmp as a PGA for the provided input pin,
    /// with the output only used internally, and enable the opamp.
    ///
    /// The input pin is configured for analogue mode but not consumed,
    /// so it may be subsequently used for ADC or comparator inputs.
    ///
    /// The returned `OpAmpInternalOutput` struct may be used as an ADC input.
    /// The opamp output will be disabled when it is dropped.
    #[cfg(opamp_v5)]
    pub fn pga_int(
        &mut self,
        pin: Peri<'_, impl NonInvertingPin<T> + crate::gpio::Pin>,
        gain: OpAmpGain,
    ) -> OpAmpInternalOutput<'_, T> {
        pin.set_as_analog();

        let pga_gain = match gain {
            OpAmpGain::Mul2 => PgaGain::GAIN2,
            OpAmpGain::Mul4 => PgaGain::GAIN4,
            OpAmpGain::Mul8 => PgaGain::GAIN8,
            OpAmpGain::Mul16 => PgaGain::GAIN16,
            OpAmpGain::Mul32 => PgaGain::GAIN32,
            OpAmpGain::Mul64 => PgaGain::GAIN64,
        };

        T::regs().csr().modify(|w| {
            w.set_vp_sel(VpSel::from_bits(pin.channel()));
            w.set_vm_sel(VmSel::PGA);
            w.set_pga_gain(pga_gain);
            w.set_opaintoen(true);
            w.set_opampen(true);
        });

        OpAmpInternalOutput { _inner: self }
    }

    /// Configure the OpAmp as a standalone DAC with the inverting input
    /// connected to the provided pin, and the output is connected
    /// internally to an ADC channel.
    ///
    /// The input pin is configured for analogue mode but not consumed,
    /// so it may be subsequently used for ADC or comparator inputs.
    ///
    /// The returned `OpAmpInternalOutput` struct may be used as an ADC
    /// input. The opamp output will be disabled when it is dropped.
    #[cfg(opamp_v5)]
    pub fn standalone_dac_int(
        &mut self,
        m_pin: Peri<'_, impl InvertingPin<T> + crate::gpio::Pin>,
    ) -> OpAmpInternalOutput<'_, T> {
        m_pin.set_as_analog();

        T::regs().csr().modify(|w| {
            use crate::pac::opamp::vals::*;
            w.set_vp_sel(VpSel::DAC3_CH1); // Actually DAC3_CHx
            w.set_vm_sel(VmSel::from_bits(m_pin.channel()));
            w.set_opaintoen(true);
            w.set_opampen(true);
        });

        OpAmpInternalOutput { _inner: self }
    }

    /// Configure the OpAmp as a standalone DAC with the inverting input
    /// connected to the provided pin, and the output connected to the
    /// provided pin.
    ///
    /// The input pin is configured for analogue mode but not consumed,
    /// so it may be subsequently used for ADC or comparator inputs.
    ///
    /// The output pin is held within the returned [`OpAmpOutput`] struct,
    /// preventing it being used elsewhere. The opamp will be disabled when
    /// the [`OpAmpOutput`] is dropped.
    #[cfg(opamp_v5)]
    pub fn standalone_dac_ext(
        &mut self,
        m_pin: Peri<'_, impl InvertingPin<T> + crate::gpio::Pin>,
        out_pin: Peri<'_, impl OutputPin<T> + crate::gpio::Pin>,
    ) -> OpAmpOutput<'_, T> {
        m_pin.set_as_analog();
        out_pin.set_as_analog();

        T::regs().csr().modify(|w| {
            use crate::pac::opamp::vals::*;
            w.set_vp_sel(VpSel::DAC3_CH1); // Actually DAC3_CHx
            w.set_vm_sel(VmSel::from_bits(m_pin.channel()));
            w.set_opaintoen(false);
            w.set_opampen(true);
        });

        OpAmpOutput { _inner: self }
    }

    /// Configure the OpAmp in standalone mode with the non-inverting input
    /// connected to the provided `p_pin`, the inverting input connected to
    /// the `m_pin`, and output to the provided `out_pin`.
    ///
    /// The input pins are configured for analogue mode but not consumed,
    /// allowing their subsequent use for ADC or comparator inputs.
    ///
    /// The output pin is held within the returned [`OpAmpOutput`] struct,
    /// preventing it being used elsewhere. The opamp will be disabled when
    /// the [`OpAmpOutput`] is dropped.
    #[cfg(opamp_v5)]
    pub fn standalone_ext(
        &mut self,
        p_pin: Peri<'d, impl NonInvertingPin<T> + crate::gpio::Pin>,
        m_pin: Peri<'d, impl InvertingPin<T> + crate::gpio::Pin>,
        out_pin: Peri<'d, impl OutputPin<T> + crate::gpio::Pin>,
    ) -> OpAmpOutput<'_, T> {
        p_pin.set_as_analog();
        m_pin.set_as_analog();
        out_pin.set_as_analog();

        T::regs().csr().modify(|w| {
            use crate::pac::opamp::vals::*;
            w.set_vp_sel(VpSel::from_bits(p_pin.channel()));
            w.set_vm_sel(VmSel::from_bits(m_pin.channel()));
            w.set_opaintoen(false);
            w.set_opampen(true);
        });

        OpAmpOutput { _inner: self }
    }

    /// Configure the OpAmp in standalone mode with the non-inverting input
    /// connected to the provided `p_pin`, the inverting input connected to
    /// the `m_pin`, and output is connected to the DAC.
    ///
    /// The input pins are configured for analogue mode but not consumed,
    /// allowing their subsequent use for ADC or comparator inputs.
    ///
    /// The returned `OpAmpOutput` struct may be used as an ADC
    /// input. The opamp output will be disabled when it is dropped.
    #[cfg(opamp_v5)]
    pub fn standalone_int(
        &mut self,
        p_pin: Peri<'d, impl NonInvertingPin<T> + crate::gpio::Pin>,
        m_pin: Peri<'d, impl InvertingPin<T> + crate::gpio::Pin>,
    ) -> OpAmpOutput<'_, T> {
        p_pin.set_as_analog();
        m_pin.set_as_analog();

        T::regs().csr().modify(|w| {
            use crate::pac::opamp::vals::*;
            w.set_vp_sel(VpSel::from_bits(p_pin.channel()));
            w.set_vm_sel(VmSel::from_bits(m_pin.channel()));
            w.set_opaintoen(true);
            w.set_opampen(true);
        });

        OpAmpOutput { _inner: self }
    }

    /// Calibrates the operational amplifier.
    ///
    /// This function enables the opamp and sets the user trim mode for calibration.
    /// Depending on the speed mode of the opamp, it calibrates the differential pair inputs.
    /// For normal speed, both the P and N differential pairs are calibrated,
    /// while for high-speed mode, only the P differential pair is calibrated.
    ///
    /// Calibrating a differential pair requires waiting 12ms in the worst case (binary method).
    #[cfg(opamp_v5)]
    pub fn calibrate(&mut self) {
        T::regs().csr().modify(|w| {
            w.set_opampen(true);
            w.set_calon(true);
            w.set_usertrim(true);
        });

        if T::regs().csr().read().opahsm() {
            self.calibrate_differential_pair(OpAmpDifferentialPair::P);
        } else {
            self.calibrate_differential_pair(OpAmpDifferentialPair::P);
            self.calibrate_differential_pair(OpAmpDifferentialPair::N);
        }

        T::regs().csr().modify(|w| {
            w.set_calon(false);
            w.set_opampen(false);
        });
    }

    /// Calibrate differential pair.
    ///
    /// The calibration is done by trying different offset values and
    /// measuring the outcal bit.
    ///
    /// The calibration range is from 0 to 31.
    ///
    /// The result is stored in the OPAMP_CSR register.
    #[cfg(opamp_v5)]
    fn calibrate_differential_pair(&mut self, pair: OpAmpDifferentialPair) {
        let mut low = 0;
        let mut high = 31;

        let calsel = match pair {
            OpAmpDifferentialPair::P => Calsel::PERCENT10,
            OpAmpDifferentialPair::N => Calsel::PERCENT90,
        };

        T::regs().csr().modify(|w| {
            w.set_calsel(calsel);
        });

        while low <= high {
            let mid = (low + high) / 2;

            T::regs().csr().modify(|w| match pair {
                OpAmpDifferentialPair::P => {
                    #[cfg(feature = "defmt")]
                    defmt::debug!("opamp p calibration. offset: {}", mid);
                    w.set_trimoffsetp(mid);
                }
                OpAmpDifferentialPair::N => {
                    #[cfg(feature = "defmt")]
                    defmt::debug!("opamp n calibration. offset: {}", mid);
                    w.set_trimoffsetn(mid);
                }
            });

            // The closer the trimming value is to the optimum trimming value, the longer it takes to stabilize
            // (with a maximum stabilization time remaining below 2 ms in any case) -- RM0440 25.3.7
            block_for_us(2_000);

            if !T::regs().csr().read().calout() {
                if mid == 0 {
                    break;
                }
                high = mid - 1;
            } else {
                if mid == 31 {
                    break;
                }
                low = mid + 1;
            }
        }
    }
}

#[cfg(not(any(stm32g4, stm32f3)))]
impl<'d, T: Instance> Drop for OpAmp<'d, T> {
    fn drop(&mut self) {
        T::info().rcc.disable();
    }
}

impl<'d, T: Instance> Drop for OpAmpOutput<'d, T> {
    fn drop(&mut self) {
        T::regs().csr().modify(|w| {
            w.set_opampen(false);
        });
    }
}

#[cfg(opamp_v5)]
impl<'d, T: Instance> Drop for OpAmpInternalOutput<'d, T> {
    fn drop(&mut self) {
        T::regs().csr().modify(|w| {
            w.set_opampen(false);
        });
    }
}

#[cfg(not(any(stm32g4, stm32f3)))]
pub(crate) struct Info {
    rcc: RccInfo,
}

pub(crate) trait SealedInstance {
    #[cfg(not(any(stm32g4, stm32f3)))]
    fn info() -> &'static Info;
    fn regs() -> crate::pac::opamp::Opamp;
}

pub(crate) trait SealedNonInvertingPin<T: Instance> {
    fn channel(&self) -> u8;
}

pub(crate) trait SealedInvertingPin<T: Instance> {
    #[allow(unused)]
    fn channel(&self) -> u8;
}

pub(crate) trait SealedOutputPin<T: Instance> {}

/// Opamp instance trait.
#[allow(private_bounds)]
pub trait Instance: SealedInstance + PeripheralType + 'static {}
/// Non-inverting pin trait.
#[allow(private_bounds)]
pub trait NonInvertingPin<T: Instance>: SealedNonInvertingPin<T> {}
/// Inverting pin trait.
#[allow(private_bounds)]
pub trait InvertingPin<T: Instance>: SealedInvertingPin<T> {}
/// Output pin trait.
#[allow(private_bounds)]
pub trait OutputPin<T: Instance>: SealedOutputPin<T> {}

macro_rules! impl_opamp_external_output {
    ($inst:ident, $adc:ident, $ch:expr) => {
        foreach_adc!(
            ($adc, $common_inst:ident, $adc_clock:ident) => {
                impl<'d> crate::adc::SealedAdcChannel<crate::peripherals::$adc>
                    for OpAmpOutput<'d, crate::peripherals::$inst>
                {
                    fn channel(&self) -> u8 {
                        $ch
                    }
                }

                impl<'d> crate::adc::AdcChannel<crate::peripherals::$adc>
                    for OpAmpOutput<'d, crate::peripherals::$inst>
                {
                }
            };
        );
    };
}

foreach_peripheral!(
    (opamp, OPAMP1) => {
        impl_opamp_external_output!(OPAMP1, ADC1, 3);
    };
    (opamp, OPAMP2) => {
        impl_opamp_external_output!(OPAMP2, ADC2, 3);
    };
    (opamp, OPAMP3) => {
        impl_opamp_external_output!(OPAMP3, ADC1, 12);
        impl_opamp_external_output!(OPAMP3, ADC3, 1);
    };
    // OPAMP4 only in STM32G4 Cat 3 devices
    (opamp, OPAMP4) => {
        impl_opamp_external_output!(OPAMP4, ADC1, 11);
        impl_opamp_external_output!(OPAMP4, ADC4, 3);
    };
    // OPAMP5 only in STM32G4 Cat 3 devices
    (opamp, OPAMP5) => {
        impl_opamp_external_output!(OPAMP5, ADC5, 1);
    };
    // OPAMP6 only in STM32G4 Cat 3/4 devices
    (opamp, OPAMP6) => {
        impl_opamp_external_output!(OPAMP6, ADC1, 14);
        impl_opamp_external_output!(OPAMP6, ADC2, 14);
    };
);

#[cfg(opamp_v5)]
macro_rules! impl_opamp_internal_output {
    ($inst:ident, $adc:ident, $ch:expr) => {
        foreach_adc!(
            ($adc, $common_inst:ident, $adc_clock:ident) => {
                impl<'d> crate::adc::SealedAdcChannel<crate::peripherals::$adc>
                    for OpAmpInternalOutput<'d, crate::peripherals::$inst>
                {
                    fn channel(&self) -> u8 {
                        $ch
                    }
                }

                impl<'d> crate::adc::AdcChannel<crate::peripherals::$adc>
                    for OpAmpInternalOutput<'d, crate::peripherals::$inst>
                {
                }
            };
        );
    };
}

#[cfg(opamp_v5)]
foreach_peripheral!(
    (opamp, OPAMP1) => {
        impl_opamp_internal_output!(OPAMP1, ADC1, 13);
    };
    (opamp, OPAMP2) => {
        impl_opamp_internal_output!(OPAMP2, ADC2, 16);
    };
    (opamp, OPAMP3) => {
        impl_opamp_internal_output!(OPAMP3, ADC2, 18);
        // Only in Cat 3/4 devices
        impl_opamp_internal_output!(OPAMP3, ADC3, 13);
    };
    // OPAMP4 only in Cat 3 devices
    (opamp, OPAMP4) => {
        impl_opamp_internal_output!(OPAMP4, ADC5, 5);
    };
    // OPAMP5 only in Cat 3 devices
    (opamp, OPAMP5) => {
        impl_opamp_internal_output!(OPAMP5, ADC5, 3);
    };
    // OPAMP6 only in Cat 3/4 devices
    (opamp, OPAMP6) => {
        // Only in Cat 3 devices
        impl_opamp_internal_output!(OPAMP6, ADC4, 17);
        // Only in Cat 4 devices
        impl_opamp_internal_output!(OPAMP6, ADC3, 17);
    };
);

foreach_peripheral! {
    (opamp, $inst:ident) => {
        impl SealedInstance for crate::peripherals::$inst {
            // G4 and F3 use SYSCFGEN, which is always enabled
            #[cfg(not(any(stm32g4, stm32f3)))]
            fn info() -> &'static Info {
                use crate::rcc::SealedRccPeripheral;
                static INFO: Info = Info {
                    rcc: crate::peripherals::$inst::RCC_INFO,
                };
                &INFO
            }
            fn regs() -> crate::pac::opamp::Opamp {
                crate::pac::$inst
            }
        }

        impl Instance for crate::peripherals::$inst {
        }
    };
}

#[allow(unused_macros)]
macro_rules! impl_opamp_vp_pin {
    ($inst:ident, $pin:ident, $ch:expr) => {
        impl crate::opamp::NonInvertingPin<peripherals::$inst> for crate::peripherals::$pin {}
        impl crate::opamp::SealedNonInvertingPin<peripherals::$inst> for crate::peripherals::$pin {
            fn channel(&self) -> u8 {
                $ch
            }
        }
    };
}

#[allow(unused_macros)]
macro_rules! impl_opamp_vn_pin {
    ($inst:ident, $pin:ident, $ch:expr) => {
        impl crate::opamp::InvertingPin<peripherals::$inst> for crate::peripherals::$pin {}
        impl crate::opamp::SealedInvertingPin<peripherals::$inst> for crate::peripherals::$pin {
            fn channel(&self) -> u8 {
                $ch
            }
        }
    };
}

#[allow(unused_macros)]
macro_rules! impl_opamp_vout_pin {
    ($inst:ident, $pin:ident) => {
        impl crate::opamp::OutputPin<peripherals::$inst> for crate::peripherals::$pin {}
        impl crate::opamp::SealedOutputPin<peripherals::$inst> for crate::peripherals::$pin {}
    };
}
