// Copyright lowRISC contributors.
// Licensed under the Apache License, Version 2.0, see LICENSE for details.
// SPDX-License-Identifier: Apache-2.0

use anyhow::{ensure, Result};
use thiserror::Error;

pub mod constants {
    /// In `pmpNcfg`, bit 7 represents a locked entry.
    pub const EPMP_CFG_LOCKED: u8 = 1 << 7;
    /// In `pmpNcfg`, bit 2 represents an executable region.
    pub const EPMP_CFG_EXEC: u8 = 1 << 2;
    /// In `pmpNcfg`, bit 2 represents an writable region.
    pub const EPMP_CFG_WRITE: u8 = 1 << 1;
    /// In `pmpNcfg`, bit 2 represents an readable region.
    pub const EPMP_CFG_READ: u8 = 1 << 0;
    /// Some common configurations.
    pub const EPMP_CFG_UNLOCKED: u8 = 0;
    pub const EPMP_CFG_LOCKED_NONE: u8 = EPMP_CFG_LOCKED;
    pub const EPMP_CFG_LRO: u8 = EPMP_CFG_LOCKED | EPMP_CFG_READ;
    pub const EPMP_CFG_LRW: u8 = EPMP_CFG_LOCKED | EPMP_CFG_READ | EPMP_CFG_WRITE;
    pub const EPMP_CFG_LRX: u8 = EPMP_CFG_LOCKED | EPMP_CFG_READ | EPMP_CFG_EXEC;
    pub const EPMP_CFG_LRWX: u8 = EPMP_CFG_LOCKED | EPMP_CFG_READ | EPMP_CFG_WRITE | EPMP_CFG_EXEC;

    pub const EPMP_CFG_RO: u8 = EPMP_CFG_READ;
    pub const EPMP_CFG_RW: u8 = EPMP_CFG_READ | EPMP_CFG_WRITE;
    pub const EPMP_CFG_RX: u8 = EPMP_CFG_READ | EPMP_CFG_EXEC;
    pub const EPMP_CFG_RWX: u8 = EPMP_CFG_READ | EPMP_CFG_WRITE | EPMP_CFG_EXEC;

    /// Machine Mode Lockdown: ePMP regions with the L bit are machine-mode only.
    pub const EPMP_MSECCFG_MML: u32 = 1;
    /// Machine Mode Whitelist Policy: deny instead of ignore memory accesses with no matching
    /// rule.
    pub const EPMP_MSECCFG_MMWP: u32 = 2;
    /// Rule Locking Bypass: allow modification of locked ePMP entries.
    pub const EPMP_MSECCFG_RLB: u32 = 4;
}

#[derive(Debug, Error)]
pub enum EpmpError {
    #[error("Invalid raw EPMP length (cfg={0}, addr={1})")]
    InvalidLength(usize, usize),
}

/// Represents the different EPMP region kinds.
#[derive(Default, Copy, Clone, Debug, PartialEq, Eq)]
#[repr(u8)]
pub enum EpmpRegionKind {
    #[default]
    Off = 0,
    Tor = 1,
    Na4 = 2,
    Napot = 3,
}

const EPMP_ADDR_SHIFT: u8 = 3;
const EPMP_ADDR_MASK: u8 = 3;

/// Represents the address range associated with the PMP entry.
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct EpmpAddressRange(pub u64, pub u64);

impl EpmpAddressRange {
    pub fn start(&self) -> u64 {
        self.0
    }
    pub fn end(&self) -> u64 {
        self.1
    }
}

/// A decoded ePMP entry.
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct EpmpEntry {
    /// The bit pattern present in `pmpNcfg`.
    pub cfg: u8,
    /// The region kind encoded ni the `cfg` field.
    pub kind: EpmpRegionKind,
    /// The address range for this entry.
    pub range: EpmpAddressRange,
}

#[derive(Clone, Debug, Default)]
pub struct Epmp {
    pub entry: Vec<EpmpEntry>,
}

impl Epmp {
    /// Creates an `Epmp` from the raw RISCV32 form of the ePMP.
    pub fn from_raw_rv32(pmpcfg: &[u32], pmpaddr: &[u32]) -> Result<Self> {
        ensure!(
            pmpcfg.len() * 4 == pmpaddr.len(),
            EpmpError::InvalidLength(pmpcfg.len(), pmpaddr.len())
        );
        let pmpcfg = pmpcfg
            .iter()
            .flat_map(|word| word.to_le_bytes())
            .collect::<Vec<u8>>();
        let mut entry = Vec::with_capacity(pmpaddr.len());
        for i in 0..pmpaddr.len() {
            let region_kind = match (pmpcfg[i] >> EPMP_ADDR_SHIFT) & EPMP_ADDR_MASK {
                0 => EpmpRegionKind::Off,
                1 => EpmpRegionKind::Tor,
                2 => EpmpRegionKind::Na4,
                3 => EpmpRegionKind::Napot,
                _ => unreachable!(),
            };
            let addr = match region_kind {
                EpmpRegionKind::Off => EpmpAddressRange(0, 0),
                EpmpRegionKind::Tor => {
                    if i == 0 {
                        EpmpAddressRange(0, (pmpaddr[i] as u64) << 2)
                    } else {
                        EpmpAddressRange((pmpaddr[i - 1] as u64) << 2, (pmpaddr[i] as u64) << 2)
                    }
                }
                EpmpRegionKind::Na4 => {
                    EpmpAddressRange((pmpaddr[i] as u64) << 2, ((pmpaddr[i] as u64) << 2) + 4)
                }
                EpmpRegionKind::Napot => {
                    let size = pmpaddr[i].trailing_ones();
                    let addr = ((pmpaddr[i] & (pmpaddr[i] + 1)) as u64) << 2;
                    let length = (1 << (size + 3)) as u64;
                    EpmpAddressRange(addr, addr + length)
                }
            };
            entry.push(EpmpEntry {
                cfg: pmpcfg[i] & !(EPMP_ADDR_MASK << EPMP_ADDR_SHIFT),
                kind: region_kind,
                range: addr,
            });
        }
        Ok(Epmp { entry })
    }
}

#[cfg(test)]
mod tests {
    use super::constants::*;
    use super::*;

    // This particular set of PMPCFG / PMPADDR values was captured from the ROM
    // and manually modified to encompass all of the decode options.
    // The modifications are:
    // - Addition of entry 6: a duplicate of entry 2 with the perms set to RWX.
    const PMPCFG: [u32; 4] = [0x998d00, 0x1f998d, 0x8b000000, 0x9b909f00];
    const PMPADDR: [u32; 16] = [
        0x2000, 0x3411, 0x2fff, 0x8000100, 0x8001203, 0x801ffff, 0x2fff, 0x0, 0x0, 0x0, 0x10000000,
        0x14000000, 0x0, 0x41ff, 0x4007000, 0x4003fff,
    ];

    #[test]
    fn test_epmp_decode() -> Result<()> {
        let epmp = Epmp::from_raw_rv32(&PMPCFG, &PMPADDR)?;

        // The 0th entry is the bottom of a TOR range.  The decoded version of entry 0 should
        // appear as an "off" region.
        assert!(matches!(
            epmp.entry[0],
            EpmpEntry {
                cfg: 0,
                kind: EpmpRegionKind::Off,
                range: EpmpAddressRange(0, 0)
            }
        ));
        // The first entry is a TOR region representing the .text section of the ROM.
        assert!(matches!(
            epmp.entry[1],
            EpmpEntry {
                cfg: EPMP_CFG_LRX,
                kind: EpmpRegionKind::Tor,
                range: EpmpAddressRange(0x8000, 0xd044)
            }
        ));
        // The second entry is a Napot region representing the entire 32K of ROM.
        assert!(matches!(
            epmp.entry[2],
            EpmpEntry {
                cfg: EPMP_CFG_LRO,
                kind: EpmpRegionKind::Napot,
                range: EpmpAddressRange(0x8000, 0x10000)
            }
        ));
        // The sixth entry is a Napot region representing the entire 32K of ROM, but is set to
        // RWX (tests the decode of the `cfg` field with lock bit clear).
        assert!(matches!(
            epmp.entry[6],
            EpmpEntry {
                cfg: EPMP_CFG_RWX,
                kind: EpmpRegionKind::Napot,
                range: EpmpAddressRange(0x8000, 0x10000)
            }
        ));

        // The thirteenth entry is a Napot region represening the 4K RVDM RAM (LRWX).
        assert!(matches!(
            epmp.entry[13],
            EpmpEntry {
                cfg: EPMP_CFG_LRWX,
                kind: EpmpRegionKind::Napot,
                range: EpmpAddressRange(0x10000, 0x11000)
            }
        ));

        // The 14th entry is an Na4 region in RAM representing the no-access stack guard.
        eprintln!("entry14 = {:?}", epmp.entry[14]);
        assert!(matches!(
            epmp.entry[14],
            EpmpEntry {
                cfg: EPMP_CFG_LOCKED_NONE,
                kind: EpmpRegionKind::Na4,
                range: EpmpAddressRange(0x1001_c000, 0x1001_c004)
            }
        ));
        // The 15th entry is an Napot region in RAM representing a read-write region.
        assert!(matches!(
            epmp.entry[15],
            EpmpEntry {
                cfg: EPMP_CFG_LRW,
                kind: EpmpRegionKind::Napot,
                range: EpmpAddressRange(0x1000_0000, 0x1002_0000)
            }
        ));
        Ok(())
    }
}
