use libk_mm::{
    address::PhysicalAddress, phys, pointer::PhysicalRef, table::MapAttributes, OnDemandPage,
    PageBox, PageProvider, VirtualPage, L3_PAGE_SIZE,
};
use libk_util::sync::IrqSafeSpinlock;
use yggdrasil_abi::{error::Error, io::SeekFrom};

use super::InstanceData;
use crate::vfs::node::NodeRef;

#[derive(Clone)]
pub struct RegularFile {
    pub(super) node: NodeRef,
    pub(super) read: bool,
    pub(super) write: bool,
    pub(super) instance_data: Option<InstanceData>,
    pub(super) position: IrqSafeSpinlock<u64>,
}

impl RegularFile {
    pub fn read(&self, buf: &mut [u8]) -> Result<usize, Error> {
        if !self.read {
            return Err(Error::InvalidFile);
        }
        let mut position = self.position.lock();
        let reg = self.node.as_regular()?;
        let count = reg.read(&self.node, self.instance_data.as_ref(), *position, buf)?;
        *position += count as u64;
        Ok(count)
    }

    pub fn write(&self, buf: &[u8]) -> Result<usize, Error> {
        if !self.write {
            return Err(Error::InvalidFile);
        }
        let mut position = self.position.lock();
        let reg = self.node.as_regular()?;
        let count = reg.write(&self.node, self.instance_data.as_ref(), *position, buf)?;
        *position += count as u64;
        Ok(count)
    }

    pub fn read_at(&self, pos: u64, buf: &mut [u8]) -> Result<usize, Error> {
        if !self.read {
            return Err(Error::InvalidFile);
        }
        let reg = self.node.as_regular()?;
        reg.read(&self.node, self.instance_data.as_ref(), pos, buf)
    }

    pub fn write_at(&self, pos: u64, buf: &[u8]) -> Result<usize, Error> {
        if !self.write {
            return Err(Error::InvalidFile);
        }
        let reg = self.node.as_regular()?;
        reg.write(&self.node, self.instance_data.as_ref(), pos, buf)
    }

    pub fn truncate(&self, size: u64) -> Result<(), Error> {
        if !self.write {
            return Err(Error::InvalidFile);
        }
        let reg = self.node.as_regular()?;
        reg.truncate(&self.node, size)
    }

    // TODO should seek beyond the end of a read-only file be allowed?
    pub fn seek(&self, from: SeekFrom) -> Result<u64, Error> {
        let mut position = self.position.lock();

        let newpos = match from {
            SeekFrom::Current(off) => {
                let newpos = i64::try_from(*position).unwrap() + off;
                if newpos < 0 {
                    return Err(Error::InvalidArgument);
                }
                newpos as u64
            }
            SeekFrom::Start(pos) => pos,
            SeekFrom::End(off) => {
                let reg = self.node.as_regular()?;
                let size = i64::try_from(reg.size(&self.node)?).unwrap();
                let newpos = size + off;

                if newpos < 0 {
                    return Err(Error::InvalidArgument);
                }

                newpos as u64
            }
        };

        *position = newpos;
        Ok(newpos)
    }
}

impl Drop for RegularFile {
    fn drop(&mut self) {
        let reg = match self.node.as_regular() {
            Ok(reg) => reg,
            Err(err) => {
                log::warn!(
                    "RegularFile::Drop: self.node.as_regular() failed: {:?}",
                    err
                );
                return;
            }
        };

        if let Err(err) = reg.close(&self.node, self.instance_data.as_ref()) {
            log::warn!("RegularFile::Drop: close() failed: {:?}", err);
        }
    }
}

impl PageProvider for RegularFile {
    fn ondemand_fetch(&self, opaque: u64) -> Result<OnDemandPage, Error> {
        assert_eq!(opaque as usize % L3_PAGE_SIZE, 0);
        let reg = self.node.as_regular()?;
        let mut page = PageBox::new_slice(0, L3_PAGE_SIZE)?;
        let len = reg.read(
            &self.node,
            self.instance_data.as_ref(),
            opaque,
            &mut page[..],
        )?;
        if len != page.len() {
            page[len..].fill(0);
        }
        let page = PageBox::into_physical_raw(page);
        Ok(OnDemandPage {
            physical: page,
            writeable: self.write,
        })
    }

    fn get_page(&self, _offset: u64) -> Result<VirtualPage, Error> {
        Ok(VirtualPage::OnDemand)
    }

    fn release_page(&self, offset: u64, phys: PhysicalAddress, dirty: bool) -> Result<(), Error> {
        if dirty && self.write {
            let page = unsafe { PhysicalRef::map_slice(phys, 0x1000) };
            let reg = self.node.as_regular()?;
            let size = reg.size(&self.node)?;
            if offset < size {
                let amount = (size - offset).min(0x1000) as usize;
                reg.write(
                    &self.node,
                    self.instance_data.as_ref(),
                    offset,
                    &page[..amount],
                )?;
            } else {
                log::warn!("Writeback {phys:#x} @ {offset} beyond the end of the file {size}");
            }
        }
        unsafe { phys::free_page(phys) };
        Ok(())
    }

    fn clone_page(
        &self,
        _offset: u64,
        _src_phys: PhysicalAddress,
        _src_attrs: MapAttributes,
    ) -> Result<PhysicalAddress, Error> {
        unimplemented!()
    }
}
