use crate::pac::flash::vals::Latency;
use crate::pac::rcc::vals::{self, Hpre, Hsidiv, Ppre, Sw};
use crate::pac::{FLASH, PWR, RCC};
use crate::rcc::{set_freqs, Clocks};
use crate::time::Hertz;

/// HSI speed
pub const HSI_FREQ: Hertz = Hertz(16_000_000);

/// LSI speed
pub const LSI_FREQ: Hertz = Hertz(32_000);

/// System clock mux source
#[derive(Clone, Copy)]
pub enum ClockSrc {
    HSE(Hertz),
    HSI16(HSI16Prescaler),
    PLL(PllConfig),
    LSI,
}

#[derive(Clone, Copy)]
pub enum HSI16Prescaler {
    NotDivided,
    Div2,
    Div4,
    Div8,
    Div16,
    Div32,
    Div64,
    Div128,
}

impl Into<Hsidiv> for HSI16Prescaler {
    fn into(self) -> Hsidiv {
        match self {
            HSI16Prescaler::NotDivided => Hsidiv::DIV1,
            HSI16Prescaler::Div2 => Hsidiv::DIV2,
            HSI16Prescaler::Div4 => Hsidiv::DIV4,
            HSI16Prescaler::Div8 => Hsidiv::DIV8,
            HSI16Prescaler::Div16 => Hsidiv::DIV16,
            HSI16Prescaler::Div32 => Hsidiv::DIV32,
            HSI16Prescaler::Div64 => Hsidiv::DIV64,
            HSI16Prescaler::Div128 => Hsidiv::DIV128,
        }
    }
}

/// The PLL configuration.
///
/// * `VCOCLK = source / m * n`
/// * `PLLRCLK = VCOCLK / r`
/// * `PLLQCLK = VCOCLK / q`
/// * `PLLPCLK = VCOCLK / p`
#[derive(Clone, Copy)]
pub struct PllConfig {
    /// The source from which the PLL receives a clock signal
    pub source: PllSrc,
    /// The initial divisor of that clock signal
    pub m: Pllm,
    /// The PLL VCO multiplier, which must be in the range `8..=86`.
    pub n: u8,
    /// The final divisor for `PLLRCLK` output which drives the system clock
    pub r: Pllr,

    /// The divisor for the `PLLQCLK` output, if desired
    pub q: Option<Pllr>,

    /// The divisor for the `PLLPCLK` output, if desired
    pub p: Option<Pllr>,
}

impl Default for PllConfig {
    #[inline]
    fn default() -> PllConfig {
        // HSI16 / 1 * 8 / 2 = 64 MHz
        PllConfig {
            source: PllSrc::HSI16,
            m: Pllm::Div1,
            n: 8,
            r: Pllr::Div2,
            q: None,
            p: None,
        }
    }
}

#[derive(Clone, Copy, Eq, PartialEq)]
pub enum PllSrc {
    HSI16,
    HSE(Hertz),
}

#[derive(Clone, Copy)]
pub enum Pllm {
    Div1,
    Div2,
    Div3,
    Div4,
    Div5,
    Div6,
    Div7,
    Div8,
}

impl From<Pllm> for u8 {
    fn from(v: Pllm) -> Self {
        match v {
            Pllm::Div1 => 0b000,
            Pllm::Div2 => 0b001,
            Pllm::Div3 => 0b010,
            Pllm::Div4 => 0b011,
            Pllm::Div5 => 0b100,
            Pllm::Div6 => 0b101,
            Pllm::Div7 => 0b110,
            Pllm::Div8 => 0b111,
        }
    }
}

impl From<Pllm> for u32 {
    fn from(v: Pllm) -> Self {
        match v {
            Pllm::Div1 => 1,
            Pllm::Div2 => 2,
            Pllm::Div3 => 3,
            Pllm::Div4 => 4,
            Pllm::Div5 => 5,
            Pllm::Div6 => 6,
            Pllm::Div7 => 7,
            Pllm::Div8 => 8,
        }
    }
}

#[derive(Clone, Copy)]
pub enum Pllr {
    Div2,
    Div3,
    Div4,
    Div5,
    Div6,
    Div7,
    Div8,
}

impl From<Pllr> for u8 {
    fn from(v: Pllr) -> Self {
        match v {
            Pllr::Div2 => 0b000,
            Pllr::Div3 => 0b001,
            Pllr::Div4 => 0b010,
            Pllr::Div5 => 0b011,
            Pllr::Div6 => 0b101,
            Pllr::Div7 => 0b110,
            Pllr::Div8 => 0b111,
        }
    }
}

impl From<Pllr> for u32 {
    fn from(v: Pllr) -> Self {
        match v {
            Pllr::Div2 => 2,
            Pllr::Div3 => 3,
            Pllr::Div4 => 4,
            Pllr::Div5 => 5,
            Pllr::Div6 => 6,
            Pllr::Div7 => 7,
            Pllr::Div8 => 8,
        }
    }
}

/// AHB prescaler
#[derive(Clone, Copy, PartialEq)]
pub enum AHBPrescaler {
    NotDivided,
    Div2,
    Div4,
    Div8,
    Div16,
    Div64,
    Div128,
    Div256,
    Div512,
}

/// APB prescaler
#[derive(Clone, Copy)]
pub enum APBPrescaler {
    NotDivided,
    Div2,
    Div4,
    Div8,
    Div16,
}

impl Into<Ppre> for APBPrescaler {
    fn into(self) -> Ppre {
        match self {
            APBPrescaler::NotDivided => Ppre::DIV1,
            APBPrescaler::Div2 => Ppre::DIV2,
            APBPrescaler::Div4 => Ppre::DIV4,
            APBPrescaler::Div8 => Ppre::DIV8,
            APBPrescaler::Div16 => Ppre::DIV16,
        }
    }
}

impl Into<Hpre> for AHBPrescaler {
    fn into(self) -> Hpre {
        match self {
            AHBPrescaler::NotDivided => Hpre::DIV1,
            AHBPrescaler::Div2 => Hpre::DIV2,
            AHBPrescaler::Div4 => Hpre::DIV4,
            AHBPrescaler::Div8 => Hpre::DIV8,
            AHBPrescaler::Div16 => Hpre::DIV16,
            AHBPrescaler::Div64 => Hpre::DIV64,
            AHBPrescaler::Div128 => Hpre::DIV128,
            AHBPrescaler::Div256 => Hpre::DIV256,
            AHBPrescaler::Div512 => Hpre::DIV512,
        }
    }
}

/// Clocks configutation
pub struct Config {
    pub mux: ClockSrc,
    pub ahb_pre: AHBPrescaler,
    pub apb_pre: APBPrescaler,
    pub low_power_run: bool,
}

impl Default for Config {
    #[inline]
    fn default() -> Config {
        Config {
            mux: ClockSrc::HSI16(HSI16Prescaler::NotDivided),
            ahb_pre: AHBPrescaler::NotDivided,
            apb_pre: APBPrescaler::NotDivided,
            low_power_run: false,
        }
    }
}

impl PllConfig {
    pub(crate) unsafe fn init(self) -> u32 {
        assert!(self.n >= 8 && self.n <= 86);
        let (src, input_freq) = match self.source {
            PllSrc::HSI16 => (vals::Pllsrc::HSI16, HSI_FREQ.0),
            PllSrc::HSE(freq) => (vals::Pllsrc::HSE, freq.0),
        };

        let m_freq = input_freq / u32::from(self.m);
        // RM0454 § 5.4.4:
        // > Caution: The software must set these bits so that the PLL input frequency after the
        // > /M divider is between 2.66 and 16 MHz.
        debug_assert!(m_freq >= 2_660_000 && m_freq <= 16_000_000);

        let n_freq = m_freq * self.n as u32;
        // RM0454 § 5.4.4:
        // > Caution: The software must set these bits so that the VCO output frequency is between
        // > 64 and 344 MHz.
        debug_assert!(n_freq >= 64_000_000 && n_freq <= 344_000_000);

        let r_freq = n_freq / u32::from(self.r);
        // RM0454 § 5.4.4:
        // > Caution: The software must set this bitfield so as not to exceed 64 MHz on this clock.
        debug_assert!(r_freq <= 64_000_000);

        // RM0454 § 5.2.3:
        // > To modify the PLL configuration, proceed as follows:
        // > 1. Disable the PLL by setting PLLON to 0 in Clock control register (RCC_CR).
        RCC.cr().modify(|w| w.set_pllon(false));

        // > 2. Wait until PLLRDY is cleared. The PLL is now fully stopped.
        while RCC.cr().read().pllrdy() {}

        // > 3. Change the desired parameter.
        // Enable whichever clock source we're using, and wait for it to become ready
        match self.source {
            PllSrc::HSI16 => {
                RCC.cr().write(|w| w.set_hsion(true));
                while !RCC.cr().read().hsirdy() {}
            }
            PllSrc::HSE(_) => {
                RCC.cr().write(|w| w.set_hseon(true));
                while !RCC.cr().read().hserdy() {}
            }
        }

        // Configure PLLSYSCFGR
        RCC.pllsyscfgr().modify(|w| {
            w.set_pllr(u8::from(self.r));
            w.set_pllren(false);

            if let Some(q) = self.q {
                w.set_pllq(u8::from(q));
            }
            w.set_pllqen(false);

            if let Some(p) = self.p {
                w.set_pllp(u8::from(p));
            }
            w.set_pllpen(false);

            w.set_plln(self.n);

            w.set_pllm(self.m as u8);

            w.set_pllsrc(src)
        });

        // > 4. Enable the PLL again by setting PLLON to 1.
        RCC.cr().modify(|w| w.set_pllon(true));

        // Wait for the PLL to become ready
        while !RCC.cr().read().pllrdy() {}

        // > 5. Enable the desired PLL outputs by configuring PLLPEN, PLLQEN, and PLLREN in PLL
        // > configuration register (RCC_PLLCFGR).
        RCC.pllsyscfgr().modify(|w| {
            // We'll use R for system clock, so enable that unconditionally
            w.set_pllren(true);

            // We may also use Q or P
            w.set_pllqen(self.q.is_some());
            w.set_pllpen(self.p.is_some());
        });

        r_freq
    }
}

pub(crate) unsafe fn init(config: Config) {
    let (sys_clk, sw) = match config.mux {
        ClockSrc::HSI16(div) => {
            // Enable HSI16
            let div: Hsidiv = div.into();
            RCC.cr().write(|w| {
                w.set_hsidiv(div);
                w.set_hsion(true)
            });
            while !RCC.cr().read().hsirdy() {}

            (HSI_FREQ.0 >> div.0, Sw::HSI)
        }
        ClockSrc::HSE(freq) => {
            // Enable HSE
            RCC.cr().write(|w| w.set_hseon(true));
            while !RCC.cr().read().hserdy() {}

            (freq.0, Sw::HSE)
        }
        ClockSrc::PLL(pll) => {
            let freq = pll.init();
            (freq, Sw::PLLRCLK)
        }
        ClockSrc::LSI => {
            // Enable LSI
            RCC.csr().write(|w| w.set_lsion(true));
            while !RCC.csr().read().lsirdy() {}
            (LSI_FREQ.0, Sw::LSI)
        }
    };

    // Determine the flash latency implied by the target clock speed
    // RM0454 § 3.3.4:
    let target_flash_latency = if sys_clk <= 24_000_000 {
        Latency::WS0
    } else if sys_clk <= 48_000_000 {
        Latency::WS1
    } else {
        Latency::WS2
    };

    // Increase the number of cycles we wait for flash if the new value is higher
    // There's no harm in waiting a little too much before the clock change, but we'll
    // crash immediately if we don't wait enough after the clock change
    let mut set_flash_latency_after = false;
    FLASH.acr().modify(|w| {
        // Is the current flash latency less than what we need at the new SYSCLK?
        if w.latency().0 <= target_flash_latency.0 {
            // We must increase the number of wait states now
            w.set_latency(target_flash_latency)
        } else {
            // We may decrease the number of wait states later
            set_flash_latency_after = true;
        }

        // RM0454 § 3.3.5:
        // > Prefetch is enabled by setting the PRFTEN bit of the FLASH access control register
        // > (FLASH_ACR). This feature is useful if at least one wait state is needed to access the
        // > Flash memory.
        //
        // Enable flash prefetching if we have at least one wait state, and disable it otherwise.
        w.set_prften(target_flash_latency.0 > 0);
    });

    if !set_flash_latency_after {
        // Spin until the effective flash latency is compatible with the clock change
        while FLASH.acr().read().latency().0 < target_flash_latency.0 {}
    }

    // Configure SYSCLK source, HCLK divisor, and PCLK divisor all at once
    let (sw, hpre, ppre) = (sw.into(), config.ahb_pre.into(), config.apb_pre.into());
    RCC.cfgr().modify(|w| {
        w.set_sw(sw);
        w.set_hpre(hpre);
        w.set_ppre(ppre);
    });

    if set_flash_latency_after {
        // We can make the flash require fewer wait states
        // Spin until the SYSCLK changes have taken effect
        loop {
            let cfgr = RCC.cfgr().read();
            if cfgr.sw() == sw && cfgr.hpre() == hpre && cfgr.ppre() == ppre {
                break;
            }
        }

        // Set the flash latency to require fewer wait states
        FLASH.acr().modify(|w| w.set_latency(target_flash_latency));
    }

    let ahb_div = match config.ahb_pre {
        AHBPrescaler::NotDivided => 1,
        AHBPrescaler::Div2 => 2,
        AHBPrescaler::Div4 => 4,
        AHBPrescaler::Div8 => 8,
        AHBPrescaler::Div16 => 16,
        AHBPrescaler::Div64 => 64,
        AHBPrescaler::Div128 => 128,
        AHBPrescaler::Div256 => 256,
        AHBPrescaler::Div512 => 512,
    };
    let ahb_freq = sys_clk / ahb_div;

    let (apb_freq, apb_tim_freq) = match config.apb_pre {
        APBPrescaler::NotDivided => (ahb_freq, ahb_freq),
        pre => {
            let pre: Ppre = pre.into();
            let pre: u8 = 1 << (pre.0 - 3);
            let freq = ahb_freq / pre as u32;
            (freq, freq * 2)
        }
    };

    if config.low_power_run {
        assert!(sys_clk <= 2_000_000);
        PWR.cr1().modify(|w| w.set_lpr(true));
    }

    set_freqs(Clocks {
        sys: Hertz(sys_clk),
        ahb1: Hertz(ahb_freq),
        apb1: Hertz(apb_freq),
        apb1_tim: Hertz(apb_tim_freq),
    });
}
