use core::{mem::MaybeUninit, num::NonZeroUsize, sync::atomic::AtomicU32, time::Duration};

use abi::{
    error::Error,
    mem::{MappingFlags, MappingSource},
    process::{
        options::ProcessOptionVariant, thread::ThreadOptionVariant, ExitCode, MutexOperation,
        ProcessGroupId, ProcessId, ProcessWait, Signal, SpawnFlags, SpawnOption, SpawnOptions,
        ThreadEvent, ThreadSpawnOptions, WaitFlags,
    },
};
use alloc::sync::Arc;
use libk::{
    block,
    task::{binary::LoadOptions, process::Process, runtime, thread::Thread, ThreadId},
    time::monotonic_time,
    vfs::IoContext,
};
use libk_mm::{
    process::VirtualRangeBacking,
    table::{EntryLevelExt, MapAttributes},
};

use crate::{arch::L3, proc, syscall::run_with_io};

// Memory management
pub(crate) fn map_memory(
    _hint: Option<NonZeroUsize>,
    len: usize,
    flags: MappingFlags,
    source: &MappingSource,
) -> Result<usize, Error> {
    let thread = Thread::current();
    let process = thread.process();

    let space = thread.address_space();

    let len = len.page_align_up::<L3>();

    run_with_io(&process, |io| {
        let backing = match source {
            MappingSource::Anonymous => VirtualRangeBacking::anonymous(),
            &MappingSource::File(fd, offset) => {
                let file = io.files.file(fd)?;
                VirtualRangeBacking::file(offset, file.clone())?
            }
        };

        let mut attrs = MapAttributes::NON_GLOBAL | MapAttributes::USER_READ;
        if flags.contains(MappingFlags::WRITE) {
            attrs |= MapAttributes::USER_WRITE;
        }

        space.allocate(None, len, backing, attrs)
    })
    .inspect_err(|error| {
        log::warn!("map_memory({len}) failed: {error:?}");
    })
}

pub(crate) fn unmap_memory(address: usize, len: usize) -> Result<(), Error> {
    let thread = Thread::current();
    let space = thread.address_space();
    let len = len.page_align_up::<L3>();

    unsafe {
        space.unmap(address, len)?;
    }

    Ok(())
}

// Process/thread management
pub(crate) fn create_process_group() -> ProcessGroupId {
    Process::create_group()
}

pub(crate) fn get_process_group_id() -> ProcessGroupId {
    let thread = Thread::current();
    let process = thread.process();
    process.group_id()
}

pub(crate) fn exit_process(code: ExitCode) -> ! {
    let thread = Thread::current();
    thread.exit_process(code)
}

pub(crate) fn spawn_process(options: &SpawnOptions<'_>) -> Result<ProcessId, Error> {
    let thread = Thread::current();
    let process = thread.process();

    let result = run_with_io(&process, |mut io| {
        let attach_trace = options
            .optional
            .iter()
            .any(|opt| matches!(opt, SpawnOption::AttachTrace));

        // Setup a new process from the file
        let load_options = LoadOptions {
            group_id: process.group_id(),
            parent: Some(process.clone()),
            path: options.program,
            args: options.arguments,
            envs: options.environment,
            single_step: attach_trace,
            disable_aslr: options.flags.contains(SpawnFlags::DISABLE_ASLR),
        };
        let (child_process, child_main) = proc::load_binary(io.ioctx_mut(), &load_options)?;
        let pid = child_process.id;

        // Inherit group and session from the creator
        child_process.inherit(&process)?;

        // Inherit root from the creator
        // let child_ioctx = IoContext::new(io.ioctx().root().clone());
        let child_ioctx = IoContext::inherit(io.ioctx_mut());
        let mut child_io = child_process.io.lock();
        child_io.set_ioctx(child_ioctx);

        for opt in options.optional {
            match opt {
                &SpawnOption::MoveFile { source, child } => {
                    if let Ok(src_file) = io.files.take_file(source) {
                        child_io.files.set_file(child, src_file)?;
                    }
                }
                &SpawnOption::CopyFile { source, child } => {
                    if let Ok(src_file) = io.files.file(source) {
                        child_io.files.set_file(child, src_file.send()?)?;
                    }
                }
                &SpawnOption::SetProcessGroup(pgroup) => {
                    let pgroup = if pgroup.into_raw() == 0 {
                        todo!()
                    } else {
                        pgroup
                    };
                    child_process.set_group_id(pgroup);
                }
                _ => (),
            }
        }

        if let Some(fd) = options.optional.iter().find_map(|item| {
            if let &SpawnOption::GainTerminal(fd) = item {
                Some(fd)
            } else {
                None
            }
        }) {
            log::debug!("{} requested terminal {:?}", pid, fd);
            let file = child_io.files.file(fd)?;
            file.set_terminal_group(child_process.group_id())?;

            if let Some(node) = file.node() {
                child_process.set_session_terminal(node.clone());
            }
        }

        drop(child_io);

        child_main.enqueue();
        if attach_trace {
            child_main
                .attach_trace(process.id)
                .expect("BUG: Could not attach to thread trace");
        }

        Ok(pid as _)
    });

    result
}

pub(crate) fn wait_process(
    wait: &ProcessWait,
    status: &mut ExitCode,
    flags: WaitFlags,
) -> Result<ProcessId, Error> {
    let thread = Thread::current();
    let process = thread.process();
    match wait {
        &ProcessWait::Process(id) => {
            *status = process.wait_for_child(id, flags)?;
            Ok(id)
        }
        ProcessWait::Group(_id) => todo!(),
        ProcessWait::AnyChild => {
            let (id, exit) = process.wait_for_any_child(flags)?;
            *status = exit;
            Ok(id)
        }
    }
}

pub(crate) fn get_pid() -> ProcessId {
    let thread = Thread::current();
    let process = thread.process();
    process.id
}

pub(crate) fn get_tid() -> u32 {
    let thread = Thread::current();
    thread.id.as_user().try_into().unwrap()
}

pub(crate) fn nanosleep(
    duration: &Duration,
    remaining: &mut MaybeUninit<Duration>,
) -> Result<(), Error> {
    let duration = *duration;
    let now = monotonic_time();
    let deadline = now + duration;
    let result = block! { runtime::sleep_until(deadline).await };
    match result {
        Ok(()) => Ok(()),
        Err(Error::Interrupted) => {
            let now = monotonic_time();
            let rem = deadline.checked_sub_time(&now).unwrap_or_default();
            remaining.write(rem);
            Err(Error::Interrupted)
        }
        Err(_) => unreachable!(),
    }
}

pub(crate) fn send_signal(pid: ProcessId, signal: Signal) -> Result<(), Error> {
    // Debug is only issued by kernel for internal purposes of interrupting a thread when someone
    // attaches to it
    if signal == Signal::Debug {
        return Err(Error::InvalidArgument);
    }
    let thread = Thread::current();
    let target = Process::get(pid).ok_or(Error::DoesNotExist)?;
    target.raise_signal(Some(thread.id), signal);
    Ok(())
}

// TODO this would've been much simpler if physical pages behaved like Rc<...>'s and kernel could
//      keep a copy of it, guaranteeing it cannot be freed and remapped into another process.
pub(crate) fn mutex(mutex: &AtomicU32, op: &MutexOperation) -> Result<(), Error> {
    let thread = Thread::current();
    let process = thread.process();

    let mutex = process.get_or_insert_mutex((mutex as *const AtomicU32).addr())?;

    match op {
        &MutexOperation::WaitWhileEqual(value, _timeout) => {
            block! { mutex.wait_until(|v| v != value).await }?
        }
        &MutexOperation::WaitUntilEqual(value, _timeout) => {
            block! { mutex.wait_until(|v| v == value).await }?
        }
        &MutexOperation::WaitWhileSet(mask, _timeout) => {
            block! { mutex.wait_until(|v| v & mask != v).await }?
        }
        &MutexOperation::WaitUntilSet(mask, _timeout) => {
            block! { mutex.wait_until(|v| v & mask == v).await }?
        }
        MutexOperation::Wake(limit) => {
            mutex.wake(*limit as _);
            Ok(())
        }
    }
}

pub(crate) fn start_session() -> Result<(), Error> {
    let thread = Thread::current();
    let process = thread.process();

    let session_terminal = process.clear_session_terminal();

    if let Some(ctty) = session_terminal {
        // Drop all FDs referring to the old session terminal
        run_with_io(&process, |mut io| {
            io.files.retain(|_, f| {
                f.node()
                    .map(|node| !Arc::ptr_eq(node, &ctty))
                    .unwrap_or(true)
            });
        });
    }

    let group_id = Process::create_group();

    process.set_session_id(process.id);
    process.set_group_id(group_id);

    Ok(())
}

pub(crate) fn spawn_thread(options: &ThreadSpawnOptions) -> Result<u32, Error> {
    let thread = Thread::current();
    let process = thread.process();

    process
        .spawn_thread(options)
        .map(|tid| tid.as_user() as u32)
}

pub(crate) fn exit_thread() -> ! {
    let thread = Thread::current();
    // TODO exit codes are not supported in here
    thread.exit(ExitCode::SUCCESS)
}

pub(crate) fn wait_thread(id: u32, event: &mut MaybeUninit<ThreadEvent>) -> Result<(), Error> {
    let tid = ThreadId::User(id as u64);
    let this_thread = Thread::current();
    let process = this_thread.process();

    let ev = block!(process.wait_for_thread(tid).await)??;
    event.write(ev);
    Ok(())
}

pub(crate) fn get_thread_option(option: u32, buffer: &mut [u8]) -> Result<usize, Error> {
    let option = ThreadOptionVariant::try_from(option)?;
    let thread = Thread::current();
    thread.get_option(option, buffer)
}

pub(crate) fn set_thread_option(option: u32, buffer: &[u8]) -> Result<(), Error> {
    let option = ThreadOptionVariant::try_from(option)?;
    let thread = Thread::current();
    thread.set_option(option, buffer)
}

pub(crate) fn get_process_option(
    pid: Option<ProcessId>,
    option: u32,
    buffer: &mut [u8],
) -> Result<usize, Error> {
    let option = ProcessOptionVariant::try_from(option)?;
    let thread = Thread::current();
    let process = thread.process();
    let process = if let Some(pid) = pid {
        // TODO check that the process is actually a child of the current one
        Process::get(pid).ok_or(Error::DoesNotExist)?
    } else {
        process
    };
    process.get_option(option, buffer)
}

pub(crate) fn set_process_option(
    pid: Option<ProcessId>,
    option: u32,
    buffer: &[u8],
) -> Result<(), Error> {
    let option = ProcessOptionVariant::try_from(option)?;
    let thread = Thread::current();
    let process = thread.process();
    let process = if let Some(pid) = pid {
        // TODO check that the process is actually a child of the current one
        Process::get(pid).ok_or(Error::DoesNotExist)?
    } else {
        process
    };
    process.set_option(option, buffer)
}
