//! General helpers and utilities for device tree drivers

use alloc::sync::Arc;
use device_api::{
    gpio::{GpioPinConfig, GpioPinLevel, InputPinBias, OutputPinBias},
    interrupt::{IrqHandle, MessageInterruptController},
};
use fdt_rs::prelude::PropReader;
use yggdrasil_abi::net::MacAddress;

use crate::{
    driver::{lookup_phandle, map_interrupt_at, DeviceTreeGpioPins},
    DeviceTreePropertyRead, TProp,
};

use super::Node;

/// Defines how push-pull is configured for a pin
#[derive(Debug, Clone, Copy)]
pub enum GenericPinctrlBiasConfig {
    /// Disable push-pull
    Disable,
    /// Pull pin up
    PullUp,
    /// Pull pin down
    PullDown,
}

/// Represents pin config parameters most commonly used in FDTs
#[derive(Debug, Clone, Copy)]
pub struct GenericPinctrlConfig {
    /// Whether and how to configure the push-pull on the pin
    pub bias: Option<GenericPinctrlBiasConfig>,
    /// Drive strength in pinmux-specific units
    pub drive_strength: Option<u32>,
    /// Whether to enable the pin as an input
    pub input_enable: Option<bool>,
    /// Whether to enable the Schmitt trigger on the pin
    pub input_schmitt_enable: Option<bool>,
    /// Whether to enable the pin as an output
    pub output_enable: Option<bool>,
    /// GPIO rise/fall time
    pub slew_rate: Option<u32>,
}

/// Represents an entry in a PCIe-controller's `interrupt-map` field
pub struct PcieInterruptEntry {
    /// PCI address bus
    pub bus: u8,
    /// PCI address device
    pub device: u8,
    /// PCI address function
    pub function: u8,
    /// PCI pin number
    pub pin: u8,
    /// Destination interrupt controller
    pub interrupt_controller: Arc<Node>,
    /// Destination IRQ options
    pub irq: IrqHandle,
}

/// Represents a single PCI address (Requester ID for a MSI(-x))
#[derive(Debug)]
pub struct PcieMsiRequesterId(u32);

/// Represents an entry in a PCIe-controller's `msi-map` field
pub struct PcieMsiEntry {
    /// Base requester ID
    pub base_rid: PcieMsiRequesterId,
    /// Number of consecutive Requester IDs this entry describes
    pub length: u32,
    /// MSI controller this entry maps to
    pub msi_controller: Arc<dyn MessageInterruptController>,
    /// Base vector this entry translates to in the MSI controller
    pub msi_base: u32,
}

/// An iterator over a generic PCIe interrupt map
pub struct PcieInterruptMapIter<'a> {
    interrupt_map: TProp<'a>,
    offset: usize,
    child_address_cells: usize,
}

/// An iterator over a generic PCIe MSI map
pub struct PcieMsiMapIter<'a> {
    msi_map: TProp<'a>,
    offset: usize,
}

impl GenericPinctrlConfig {
    fn tristate(enable: bool, disable: bool) -> Option<bool> {
        if enable {
            Some(true)
        } else if disable {
            Some(false)
        } else {
            None
        }
    }

    /// Retrieves a generic pin configuration from a device tree node
    pub fn from_node(node: &Node) -> Self {
        let bias_disable = node.has_property("bias-disable");
        let bias_pull_up = node.has_property("bias-pull-up");
        let bias_pull_down = node.has_property("bias-pull-down");
        let drive_strength = node.prop_usize("drive-strength").map(|p| p as u32);
        let input_disable = node.has_property("input-disable");
        let input_enable = node.has_property("input-enable");
        let input_schmitt_enable = node.has_property("input-schmitt-enable");
        let input_schmitt_disable = node.has_property("input-schmitt-disable");
        let output_disable = node.has_property("output-disable");
        let output_enable = node.has_property("output-enable");
        let slew_rate = node.prop_usize("slew-rate").map(|p| p as u32);

        let bias = if bias_disable {
            Some(GenericPinctrlBiasConfig::Disable)
        } else if bias_pull_up {
            Some(GenericPinctrlBiasConfig::PullUp)
        } else if bias_pull_down {
            Some(GenericPinctrlBiasConfig::PullDown)
        } else {
            None
        };
        let input_enable = Self::tristate(input_enable, input_disable);
        let input_schmitt_enable = Self::tristate(input_schmitt_enable, input_schmitt_disable);
        let output_enable = Self::tristate(output_enable, output_disable);

        Self {
            bias,
            drive_strength,
            input_enable,
            input_schmitt_enable,
            output_enable,
            slew_rate,
        }
    }
}

impl Iterator for PcieInterruptMapIter<'_> {
    type Item = PcieInterruptEntry;

    fn next(&mut self) -> Option<Self::Item> {
        if self.offset >= self.interrupt_map.len() / size_of::<u32>() {
            return None;
        }

        // TODO handle interrupt-map-mask
        // interrupt-map:
        //  * PCI address (#address-cells of PCI node)
        //  * Pin (1 cell)
        //  * Interrupt Controller Handle phandle
        //  * Interrupt Address (unused, #address-cells of interrupt-controller)
        //  * Interrupt Controller Data (#interrupt-cells of interrupt-controller)

        let mut offset = self.offset;

        let address = self.interrupt_map.read_cell(offset, 1)? as u32;
        offset += 1 + self.child_address_cells;

        let bus = (address >> 16) as u8;
        let device = ((address >> 11) & 0x1F) as u8;
        let function = ((address >> 8) & 0x7) as u8;

        let (pin, phandle) = self.interrupt_map.read_cells_at(offset, (1, 1))?;
        offset += 2;

        let interrupt_controller = lookup_phandle(phandle as u32, true)?;
        let interrupt_address_cells = interrupt_controller.self_address_cells().unwrap_or(0);
        let interrupt_cells = interrupt_controller.self_interrupt_cells()?;

        // #address-cells of intc unused and assumed to be zeroes for now
        offset += interrupt_address_cells;

        let irq = map_interrupt_at(&interrupt_controller, &self.interrupt_map, offset)?;

        offset += interrupt_cells;

        let pin = pin as u8;

        self.offset = offset;

        Some(PcieInterruptEntry {
            bus,
            device,
            function,
            pin,
            interrupt_controller,
            irq,
        })
    }
}

impl Iterator for PcieMsiMapIter<'_> {
    type Item = PcieMsiEntry;

    fn next(&mut self) -> Option<Self::Item> {
        if self.offset >= self.msi_map.len() {
            return None;
        }

        // Each entry is 4 cells:
        //
        // * Requester ID (PCI address) base
        // * msi-controller node phandle
        // * msi-base
        // * Number of consecutive Requester ID for this entry

        let (base_rid, phandle, msi_base, length) =
            self.msi_map.read_cells_at(self.offset, (1, 1, 1, 1))?;
        let base_rid = PcieMsiRequesterId(base_rid as u32);
        let msi_controller = lookup_phandle(phandle as u32, true)?;
        let msi_controller = msi_controller.msi_controller.try_get()?.clone();
        let msi_base = msi_base as u32;
        let length = length as u32;

        self.offset += 4;

        Some(PcieMsiEntry {
            base_rid,
            msi_base,
            msi_controller,
            length,
        })
    }
}

impl PcieMsiRequesterId {
    /// Splits the struct into bus:device:function parts
    pub fn as_parts(&self) -> (u8, u8, u8) {
        let bus = (self.0 >> 8) as u8;
        let device = ((self.0 >> 3) & 0x1F) as u8;
        let function = (self.0 & 0xF) as u8;

        (bus, device, function)
    }

    /// Applies an offset to this requester ID, returning the result
    pub fn offset(&self, offset: u32) -> Self {
        Self(self.0 + offset - 1)
    }
}

/// Returns an iterator over the PCIe-controller's `interrupt-map`
pub fn pcie_interrupt_map(node: &Arc<Node>) -> Option<PcieInterruptMapIter<'static>> {
    let interrupt_map = node.property("interrupt-map")?;
    let child_address_cells = node.prop_usize("#address-cells")?.checked_sub(1)?;

    Some(PcieInterruptMapIter {
        interrupt_map,
        child_address_cells,
        offset: 0,
    })
}

/// Returns an iterator over the PCIe-controller's `msi-map`
pub fn pcie_msi_map(node: &Arc<Node>) -> Option<PcieMsiMapIter<'static>> {
    let msi_map = node.property("msi-map")?;
    Some(PcieMsiMapIter { msi_map, offset: 0 })
}

/// Reads the configured MAC address from the node's fields in the following order:
///
/// * "mac-address"
/// * "local-mac-address"
/// * "address"
///
/// If none are present, [None] is returned.
pub fn read_mac_address(node: &Arc<Node>) -> Option<MacAddress> {
    let property = node
        .property("mac-address")
        .or_else(|| node.property("local-mac-address"))
        .or_else(|| node.property("address"))?;
    let mac_bytes: [u8; 6] = property.raw().try_into().ok()?;
    Some(MacAddress::from(mac_bytes))
}

/// Parse a generic gpio configuration from the device tree 1-cell format
pub fn generic_gpio_config(cell: u32, gpio: &DeviceTreeGpioPins) -> GpioPinConfig {
    const GPIO_ACTIVE_LOW: u32 = 1 << 0;
    const GPIO_SINGLE_ENDED: u32 = 1 << 1;
    const GPIO_LINE_OPEN_DRAIN: u32 = 1 << 2;
    const GPIO_PULL_UP: u32 = 1 << 4;
    const GPIO_PULL_DOWN: u32 = 1 << 5;
    const GPIO_PULL_DISABLE: u32 = 1 << 6;
    const GPIO_OPEN_DRAIN: u32 = GPIO_LINE_OPEN_DRAIN | GPIO_SINGLE_ENDED;

    let active_level = if cell & GPIO_ACTIVE_LOW != 0 {
        GpioPinLevel::Low
    } else {
        GpioPinLevel::High
    };
    let output_bias = match cell & (GPIO_SINGLE_ENDED | GPIO_LINE_OPEN_DRAIN) {
        0 => Some(OutputPinBias::PushPull),
        GPIO_OPEN_DRAIN | GPIO_SINGLE_ENDED => Some(OutputPinBias::OpenDrain),
        _ => None,
    };
    let input_bias = match cell & (GPIO_PULL_UP | GPIO_PULL_DOWN | GPIO_PULL_DISABLE) {
        GPIO_PULL_DISABLE => Some(InputPinBias::Floating),
        GPIO_PULL_UP => Some(InputPinBias::PullUp),
        GPIO_PULL_DOWN => Some(InputPinBias::PullDown),
        _ => None,
    };

    GpioPinConfig {
        input: gpio.input,
        output: gpio.output,
        initial_level: gpio.initial_level,
        input_bias,
        output_bias,
        active_level,
    }
}
