// Copyright (c) 2020 Huawei Technologies Co.,Ltd. All rights reserved.
//
// StratoVirt is licensed under Mulan PSL v2.
// You can use this software according to the terms and conditions of the Mulan
// PSL v2.
// You may obtain a copy of Mulan PSL v2 at:
//         http://license.coscl.org.cn/MulanPSL2
// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY
// KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO
// NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
// See the Mulan PSL v2 for more details.

pub mod errors {
    use error_chain::error_chain;

    error_chain! {
        links {
            AddressSpace(address_space::errors::Error, address_space::errors::ErrorKind);
            Hypervisor(hypervisor::errors::Error, hypervisor::errors::ErrorKind);
        }
        foreign_links {
            KvmIoctl(kvm_ioctls::Error);
        }
    }
}

use std::sync::{Arc, Mutex};

use acpi::{AmlBuilder, AmlScope};
use address_space::{AddressSpace, GuestAddress, Region, RegionIoEventFd, RegionOps};
use error_chain::bail;
use hypervisor::kvm::KVM_FDS;
use vmm_sys_util::eventfd::EventFd;

use crate::errors::{Result, ResultExt};

pub struct SysBus {
    #[cfg(target_arch = "x86_64")]
    pub sys_io: Arc<AddressSpace>,
    pub sys_mem: Arc<AddressSpace>,
    pub devices: Vec<Arc<Mutex<dyn SysBusDevOps>>>,
    pub free_irqs: (i32, i32),
    pub min_free_irq: i32,
    pub mmio_region: (u64, u64),
    pub min_free_base: u64,
}

impl SysBus {
    pub fn new(
        #[cfg(target_arch = "x86_64")] sys_io: &Arc<AddressSpace>,
        sys_mem: &Arc<AddressSpace>,
        free_irqs: (i32, i32),
        mmio_region: (u64, u64),
    ) -> Self {
        Self {
            #[cfg(target_arch = "x86_64")]
            sys_io: sys_io.clone(),
            sys_mem: sys_mem.clone(),
            devices: Vec::new(),
            free_irqs,
            min_free_irq: free_irqs.0,
            mmio_region,
            min_free_base: mmio_region.0,
        }
    }

    pub fn build_region_ops<T: 'static + SysBusDevOps>(&self, dev: &Arc<Mutex<T>>) -> RegionOps {
        let cloned_dev = dev.clone();
        let read_ops = move |data: &mut [u8], addr: GuestAddress, offset: u64| -> bool {
            cloned_dev.lock().unwrap().read(data, addr, offset)
        };

        let cloned_dev = dev.clone();
        let write_ops = move |data: &[u8], addr: GuestAddress, offset: u64| -> bool {
            cloned_dev.lock().unwrap().write(data, addr, offset)
        };

        RegionOps {
            read: Arc::new(read_ops),
            write: Arc::new(write_ops),
        }
    }

    pub fn attach_device<T: 'static + SysBusDevOps>(
        &mut self,
        dev: &Arc<Mutex<T>>,
        region_base: u64,
        region_size: u64,
    ) -> Result<()> {
        let region_ops = self.build_region_ops(dev);
        let region = Region::init_io_region(region_size, region_ops);
        let locked_dev = dev.lock().unwrap();

        region.set_ioeventfds(&locked_dev.ioeventfds());
        match locked_dev.get_type() {
            SysBusDevType::Serial if cfg!(target_arch = "x86_64") => {
                #[cfg(target_arch = "x86_64")]
                self.sys_io
                    .root()
                    .add_subregion(region, region_base)
                    .chain_err(|| {
                        format!(
                            "Failed to register region in I/O space: offset={},size={}",
                            region_base, region_size
                        )
                    })?;
            }
            SysBusDevType::FwCfg if cfg!(target_arch = "x86_64") => {
                #[cfg(target_arch = "x86_64")]
                self.sys_io
                    .root()
                    .add_subregion(region, region_base)
                    .chain_err(|| {
                        format!(
                            "Failed to register region in I/O space: offset 0x{:x}, size {}",
                            region_base, region_size
                        )
                    })?;
            }
            SysBusDevType::Rtc if cfg!(target_arch = "x86_64") => {
                #[cfg(target_arch = "x86_64")]
                self.sys_io
                    .root()
                    .add_subregion(region, region_base)
                    .chain_err(|| {
                        format!(
                            "Failed to register region in I/O space: offset 0x{:x}, size {}",
                            region_base, region_size
                        )
                    })?;
            }
            _ => self
                .sys_mem
                .root()
                .add_subregion(region, region_base)
                .chain_err(|| {
                    format!(
                        "Failed to register region in memory space: offset={},size={}",
                        region_base, region_size
                    )
                })?,
        }

        self.devices.push(dev.clone());
        Ok(())
    }
}

#[derive(Copy, Clone)]
pub struct SysRes {
    pub region_base: u64,
    pub region_size: u64,
    pub irq: i32,
}

impl Default for SysRes {
    fn default() -> Self {
        Self {
            region_base: 0,
            region_size: 0,
            irq: -1,
        }
    }
}

#[allow(clippy::upper_case_acronyms)]
#[derive(Eq, PartialEq)]
pub enum SysBusDevType {
    Serial,
    Rtc,
    VirtioMmio,
    #[cfg(target_arch = "aarch64")]
    PL011,
    FwCfg,
    Flash,
    Others,
}

/// Operations for sysbus devices.
pub trait SysBusDevOps: Send + AmlBuilder {
    /// Read function of device.
    ///
    /// # Arguments
    ///
    /// * `data` - A u8-type array.
    /// * `base` - Base address of this device.
    /// * `offset` - Offset from base address.
    fn read(&mut self, data: &mut [u8], base: GuestAddress, offset: u64) -> bool;

    /// Write function of device.
    ///
    /// # Arguments
    ///
    /// * `data` - A u8-type array.
    /// * `base` - Base address of this device.
    /// * `offset` - Offset from base address.
    fn write(&mut self, data: &[u8], base: GuestAddress, offset: u64) -> bool;

    fn ioeventfds(&self) -> Vec<RegionIoEventFd> {
        Vec::new()
    }

    fn interrupt_evt(&self) -> Option<&EventFd> {
        None
    }

    fn set_irq(&mut self, sysbus: &mut SysBus) -> Result<i32> {
        let irq = sysbus.min_free_irq;
        if irq > sysbus.free_irqs.1 {
            bail!("IRQ number exhausted.");
        }

        match self.interrupt_evt() {
            None => Ok(-1_i32),
            Some(evt) => {
                KVM_FDS.load().register_irqfd(evt, irq as u32)?;
                sysbus.min_free_irq = irq + 1;
                Ok(irq)
            }
        }
    }

    fn get_sys_resource(&mut self) -> Option<&mut SysRes> {
        None
    }

    fn set_sys_resource(
        &mut self,
        sysbus: &mut SysBus,
        region_base: u64,
        region_size: u64,
    ) -> Result<()> {
        let irq = self.set_irq(sysbus)?;
        if let Some(res) = self.get_sys_resource() {
            res.region_base = region_base;
            res.region_size = region_size;
            res.irq = irq;
            return Ok(());
        }
        bail!("Failed to get sys resource.");
    }

    fn get_type(&self) -> SysBusDevType {
        SysBusDevType::Others
    }

    fn reset(&mut self) -> Result<()> {
        Ok(())
    }
}

impl AmlBuilder for SysBus {
    fn aml_bytes(&self) -> Vec<u8> {
        let mut scope = AmlScope::new("_SB");
        self.devices.iter().for_each(|dev| {
            scope.append(&dev.lock().unwrap().aml_bytes());
        });

        scope.aml_bytes()
    }
}