use core::arch::asm;

use crate::io;
use crate::num::NonZero;
use crate::os::xous::ffi::{
    MemoryFlags, Syscall, ThreadId, blocking_scalar, create_thread, do_yield, join_thread,
    map_memory, update_memory_flags,
};
use crate::os::xous::services::{TicktimerScalar, ticktimer_server};
use crate::thread::ThreadInit;
use crate::time::Duration;

pub struct Thread {
    tid: ThreadId,
}

pub const DEFAULT_MIN_STACK_SIZE: usize = 131072;
const MIN_STACK_SIZE: usize = 4096;
pub const GUARD_PAGE_SIZE: usize = 4096;

impl Thread {
    // unsafe: see thread::Builder::spawn_unchecked for safety requirements
    pub unsafe fn new(stack: usize, init: Box<ThreadInit>) -> io::Result<Thread> {
        let data = Box::into_raw(init);
        let mut stack_size = crate::cmp::max(stack, MIN_STACK_SIZE);

        if (stack_size & 4095) != 0 {
            stack_size = (stack_size + 4095) & !4095;
        }

        // Allocate the whole thing, then divide it up after the fact. This ensures that
        // even if there's a context switch during this function, the whole stack plus
        // guard pages will remain contiguous.
        let stack_plus_guard_pages: &mut [u8] = unsafe {
            map_memory(
                None,
                None,
                GUARD_PAGE_SIZE + stack_size + GUARD_PAGE_SIZE,
                MemoryFlags::R | MemoryFlags::W | MemoryFlags::X,
            )
        }
        .map_err(|code| io::Error::from_raw_os_error(code as i32))?;

        // No access to this page. Note: Write-only pages are illegal, and will
        // cause an access violation.
        unsafe {
            update_memory_flags(&mut stack_plus_guard_pages[0..GUARD_PAGE_SIZE], MemoryFlags::W)
                .map_err(|code| io::Error::from_raw_os_error(code as i32))?
        };

        // No access to this page. Note: Write-only pages are illegal, and will
        // cause an access violation.
        unsafe {
            update_memory_flags(
                &mut stack_plus_guard_pages[(GUARD_PAGE_SIZE + stack_size)..],
                MemoryFlags::W,
            )
            .map_err(|code| io::Error::from_raw_os_error(code as i32))?
        };

        let guard_page_pre = stack_plus_guard_pages.as_ptr() as usize;
        let tid = create_thread(
            thread_start as *mut usize,
            &mut stack_plus_guard_pages[GUARD_PAGE_SIZE..(stack_size + GUARD_PAGE_SIZE)],
            data as usize,
            guard_page_pre,
            stack_size,
            0,
        )
        .map_err(|code| io::Error::from_raw_os_error(code as i32))?;

        #[inline(never)]
        fn rust_main_thread_not_inlined(init: Box<ThreadInit>) {
            let rust_start = init.init();
            rust_start();
        }

        extern "C" fn thread_start(
            data: *mut usize,
            guard_page_pre: usize,
            stack_size: usize,
        ) -> ! {
            // SAFETY: we are simply recreating the box that was leaked earlier.
            let init = unsafe { Box::from_raw(data as *mut ThreadInit) };

            // Run the main thread with an inline(never) barrier to prevent
            // dealloc calls from being reordered to after the TLS has been destroyed.
            // See https://github.com/rust-lang/rust/pull/144465#pullrequestreview-3289729950
            // for more context.
            run_main_thread_not_inlined(init);

            // Destroy TLS, which will free the TLS page and call the destructor for
            // any thread local storage (if any).
            unsafe {
                crate::sys::thread_local::key::destroy_tls();
            }

            // Deallocate the stack memory, along with the guard pages. Afterwards,
            // exit the thread by returning to the magic address 0xff80_3000usize,
            // which tells the kernel to deallocate this thread.
            let mapped_memory_base = guard_page_pre;
            let mapped_memory_length = GUARD_PAGE_SIZE + stack_size + GUARD_PAGE_SIZE;
            unsafe {
                asm!(
                    "ecall",
                    "ret",
                                        in("a0") Syscall::UnmapMemory as usize,
                                        in("a1") mapped_memory_base,
                                        in("a2") mapped_memory_length,
                                        in("ra") 0xff80_3000usize,
                                        options(nomem, nostack, noreturn)
                );
            }
        }

        Ok(Thread { tid })
    }

    pub fn join(self) {
        join_thread(self.tid).unwrap();
    }
}

pub fn available_parallelism() -> io::Result<NonZero<usize>> {
    // We're unicore right now.
    Ok(unsafe { NonZero::new_unchecked(1) })
}

pub fn yield_now() {
    do_yield();
}

pub fn sleep(dur: Duration) {
    // Because the sleep server works on units of `usized milliseconds`, split
    // the messages up into these chunks. This means we may run into issues
    // if you try to sleep a thread for more than 49 days on a 32-bit system.
    let mut millis = dur.as_millis();
    while millis > 0 {
        let sleep_duration = if millis > (usize::MAX as _) { usize::MAX } else { millis as usize };
        blocking_scalar(ticktimer_server(), TicktimerScalar::SleepMs(sleep_duration).into())
            .expect("failed to send message to ticktimer server");
        millis -= sleep_duration as u128;
    }
}
