// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT

// cSpell: ignore cntfrq cntvct conpointer inited mpointer
#![no_main]
#![no_std]
#![cfg(target_os = "uefi")]

extern crate alloc;

use alloc::boxed::Box;
use alloc::format;
use alloc::rc::Rc;
use alloc::string::{String, ToString};
use alloc::vec;
use core::slice;
use core::sync::atomic::{AtomicPtr, Ordering};
use core::time::Duration;
use log::info;
use slint::platform::{PointerEventButton, WindowEvent};
use slint::{SharedString, platform::software_renderer};
use uefi::Char16;
use uefi::boot::ScopedProtocol;
use uefi::prelude::*;
use uefi::proto::console::{gop::BltPixel, pointer::Pointer};

slint::include_modules!();

static MOUSE_POINTER: AtomicPtr<ScopedProtocol<Pointer>> = AtomicPtr::new(core::ptr::null_mut());

fn timer_tick() -> u64 {
    #[cfg(target_arch = "x86")]
    unsafe {
        core::arch::x86::_rdtsc()
    }

    #[cfg(target_arch = "x86_64")]
    unsafe {
        core::arch::x86_64::_rdtsc()
    }

    #[cfg(target_arch = "aarch64")]
    unsafe {
        let mut ticks: u64;
        core::arch::asm!("mrs {}, cntvct_el0", out(reg) ticks);
        ticks
    }
}

fn timer_freq() -> u64 {
    #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
    {
        let start = timer_tick();
        uefi::boot::stall(Duration::from_millis(1));
        let end = timer_tick();
        (end - start) * 1000
    }

    #[cfg(target_arch = "aarch64")]
    unsafe {
        let mut freq: u64;
        core::arch::asm!("mrs {}, cntfrq_el0", out(reg) freq);
        freq
    }
}

fn pointer_init() {
    // mouse pointer
    let handle = uefi::boot::get_handle_for_protocol::<Pointer>().expect("miss Pointer protocol");
    let mut pointer = uefi::boot::open_protocol_exclusive::<Pointer>(handle)
        .expect("can't open Pointer protocol.");
    pointer.reset(false).expect("Failed to reset pointer device.");
    info!("pointer inited, mode = {:?}.", pointer.mode());
    let raw_ptr = Box::into_raw(Box::new(pointer));
    MOUSE_POINTER.store(raw_ptr, Ordering::Relaxed);
}

fn get_key_press() -> Option<char> {
    use slint::platform::Key::*;
    use uefi::proto::console::text::Key as UefiKey;
    use uefi::proto::console::text::ScanCode as Scan;

    let nl = Char16::try_from('\r').unwrap();

    match uefi::system::with_stdin(|stdin| stdin.read_key()) {
        Err(_) | Ok(None) => None,
        Ok(Some(UefiKey::Printable(key))) if key == nl => Some('\n'),
        Ok(Some(UefiKey::Printable(key))) => Some(char::from(key)),
        Ok(Some(UefiKey::Special(key))) => Some(
            match key {
                Scan::UP => UpArrow,
                Scan::DOWN => DownArrow,
                Scan::RIGHT => RightArrow,
                Scan::LEFT => LeftArrow,
                Scan::HOME => Home,
                Scan::END => End,
                Scan::INSERT => Insert,
                Scan::DELETE => Delete,
                Scan::PAGE_UP => PageUp,
                Scan::PAGE_DOWN => PageDown,
                Scan::ESCAPE => Escape,
                Scan::FUNCTION_1 => F1,
                Scan::FUNCTION_2 => F2,
                Scan::FUNCTION_3 => F3,
                Scan::FUNCTION_4 => F4,
                Scan::FUNCTION_5 => F5,
                Scan::FUNCTION_6 => F6,
                Scan::FUNCTION_7 => F7,
                Scan::FUNCTION_8 => F8,
                Scan::FUNCTION_9 => F9,
                Scan::FUNCTION_10 => F10,
                Scan::FUNCTION_11 => F11,
                Scan::FUNCTION_12 => F12,
                Scan::FUNCTION_13 => F13,
                Scan::FUNCTION_14 => F14,
                Scan::FUNCTION_15 => F15,
                Scan::FUNCTION_16 => F16,
                Scan::FUNCTION_17 => F17,
                Scan::FUNCTION_18 => F18,
                Scan::FUNCTION_19 => F19,
                Scan::FUNCTION_20 => F20,
                Scan::FUNCTION_21 => F21,
                Scan::FUNCTION_22 => F22,
                Scan::FUNCTION_23 => F23,
                Scan::FUNCTION_24 => F24,
                _ => return None,
            }
            .into(),
        ),
    }
}

fn wait_for_input(max_timeout: Option<Duration>) {
    use uefi::boot::*;

    let watchdog_timeout = Duration::from_secs(120);
    let timeout = watchdog_timeout.min(max_timeout.unwrap_or(watchdog_timeout));

    // SAFETY: The event is closed before returning from this function.
    let timer = unsafe {
        uefi::boot::create_event(EventType::TIMER, Tpl::APPLICATION, None, None).unwrap()
    };
    uefi::boot::set_timer(&timer, TimerTrigger::Periodic((timeout.as_nanos() / 100) as u64))
        .unwrap();

    uefi::boot::set_watchdog_timer(2 * watchdog_timeout.as_micros() as usize, 0x10000, None)
        .unwrap();

    uefi::system::with_stdin(|stdin| {
        // SAFETY: The cloned handles are only used to wait for further input events and
        // are then immediately dropped.
        let ptr = MOUSE_POINTER.load(Ordering::Relaxed);
        let pointer_ref = unsafe { &*ptr };
        let mut events = unsafe {
            [
                stdin.wait_for_key_event().unwrap(),
                pointer_ref.wait_for_input_event().unwrap(),
                timer.unsafe_clone(),
            ]
        };
        uefi::boot::wait_for_event(&mut events).unwrap();
    });

    uefi::boot::set_watchdog_timer(2 * watchdog_timeout.as_micros() as usize, 0x10000, None)
        .unwrap();
    uefi::boot::close_event(timer).unwrap();
}

#[repr(transparent)]
#[derive(Clone, Copy)]
struct SlintBltPixel(BltPixel);

impl software_renderer::TargetPixel for SlintBltPixel {
    fn blend(&mut self, color: software_renderer::PremultipliedRgbaColor) {
        let a = (u8::MAX - color.alpha) as u16;
        self.0.red = (self.0.red as u16 * a / 255) as u8 + color.red;
        self.0.green = (self.0.green as u16 * a / 255) as u8 + color.green;
        self.0.blue = (self.0.blue as u16 * a / 255) as u8 + color.blue;
    }

    fn from_rgb(red: u8, green: u8, blue: u8) -> Self {
        SlintBltPixel(BltPixel::new(red, green, blue))
    }
}

#[repr(transparent)]
#[derive(Clone, Copy)]
/// RGBA-8-8-8-8
struct PngRGBAPixel([u8; 4]);

impl PngRGBAPixel {
    fn new() -> Self {
        PngRGBAPixel([254, 254, 254, 0])
    }
    fn from_rgba(&mut self, r: u8, g: u8, b: u8, a: u8) {
        self.0 = [r, g, b, a];
    }

    fn blend_blt_pixel(&self, background: &mut BltPixel) {
        // Alpha Blending
        // Result = Foreground×α + Background×(1−α)
        let alpha = self.0[3] as f32 / 255.0;
        let r = self.0[0] as f32;
        let g = self.0[1] as f32;
        let b = self.0[2] as f32;

        let blended_r = ((1.0 - alpha) * background.red as f32 + alpha * r) as u8;
        let blended_g = ((1.0 - alpha) * background.green as f32 + alpha * g) as u8;
        let blended_b = ((1.0 - alpha) * background.blue as f32 + alpha * b) as u8;

        background.red = blended_r;
        background.green = blended_g;
        background.blue = blended_b;
    }
}

struct Platform {
    window: Rc<software_renderer::MinimalSoftwareWindow>,
    timer_freq: f64,
    timer_start: f64,
}

impl Default for Platform {
    fn default() -> Self {
        pointer_init();
        Self {
            window: software_renderer::MinimalSoftwareWindow::new(
                software_renderer::RepaintBufferType::ReusedBuffer,
            ),
            timer_freq: timer_freq() as f64,
            timer_start: timer_tick() as f64,
        }
    }
}

impl slint::platform::Platform for Platform {
    fn create_window_adapter(
        &self,
    ) -> Result<Rc<dyn slint::platform::WindowAdapter>, slint::PlatformError> {
        Ok(self.window.clone())
    }

    fn duration_since_start(&self) -> Duration {
        Duration::from_secs_f64((timer_tick() as f64 - self.timer_start) / self.timer_freq)
    }

    fn run_event_loop(&self) -> Result<(), slint::PlatformError> {
        use uefi::{boot::*, proto::console::gop::*};

        let gop_handle = uefi::boot::get_handle_for_protocol::<GraphicsOutput>().unwrap();

        // SAFETY: uefi-rs wants us to use open_protocol_exclusive(), which will not work
        // on real hardware. We can only hope that any other users of this
        // handle/protocol behave and don't interfere with our uses of it.
        let mut gop = unsafe {
            uefi::boot::open_protocol::<GraphicsOutput>(
                OpenProtocolParams {
                    handle: gop_handle,
                    agent: uefi::boot::image_handle(),
                    controller: None,
                },
                OpenProtocolAttributes::GetProtocol,
            )
            .unwrap()
        };

        let info = gop.current_mode_info();
        let mut fb = alloc::vec![SlintBltPixel(BltPixel::new(0, 0, 0)); info.resolution().0 * info.resolution().1];

        //mouse pixel
        let png: &[u8] = &include_bytes!("resource/cursor.png")[..];
        let header = minipng::decode_png_header(png).expect("bad PNG");
        let mut buffer = vec![0; header.required_bytes_rgba8bpc()];
        let mut image = minipng::decode_png(png, &mut buffer).expect("bad PNG");
        image.convert_to_rgba8bpc().expect("Failed to convert to RGBA8bit");
        info!("pointer png image size: {}x{} ", image.width(), image.height());
        let pointer_x = image.width() as usize;
        let pointer_y = image.height() as usize;
        let image_size: usize = (image.width() * image.height()) as usize;
        let mut vec_png = alloc::vec![PngRGBAPixel::new(); image_size];
        let mut mfb = alloc::vec![BltPixel::new(254, 254, 254); image_size];
        for i in 0..image_size {
            vec_png[i].from_rgba(
                image.pixels()[4 * i + 0], //r
                image.pixels()[4 * i + 1], //g
                image.pixels()[4 * i + 2], //b
                image.pixels()[4 * i + 3], //a
            );
            vec_png[i].blend_blt_pixel(&mut mfb[i]);
        }

        self.window.set_size(slint::PhysicalSize::new(
            info.resolution().0.try_into().unwrap(),
            info.resolution().1.try_into().unwrap(),
        ));

        let mut position = slint::LogicalPosition::new(0.0, 0.0);

        let ptr = MOUSE_POINTER.load(Ordering::Relaxed);
        let mpointer = unsafe { &mut *ptr };
        let conpointer = unsafe { &*ptr };
        let mouse_mode = conpointer.mode();
        let mut is_mouse_move = false;

        loop {
            slint::platform::update_timers_and_animations();

            // key handle until no input
            while let Some(key) = get_key_press() {
                // EFI does not distinguish between pressed and released events.
                let text = SharedString::from(key);
                self.window.dispatch_event_with_result(WindowEvent::KeyPressed { text: text.clone() })?;
                self.window.dispatch_event_with_result(WindowEvent::KeyReleased { text })?;
            }
            // mouse handle until no input
            while let Some(mut mouse) =
                mpointer.read_state().expect("Failed to read state from Pointer.")
            {
                position.x +=
                    (mouse.relative_movement[0] as f32) / (mouse_mode.resolution[0] as f32);
                position.y +=
                    (mouse.relative_movement[1] as f32) / (mouse_mode.resolution[1] as f32);

                let button: PointerEventButton = match mouse.button {
                    [true, true] => PointerEventButton::Left,
                    [true, false] => PointerEventButton::Left,
                    [false, true] => PointerEventButton::Right,
                    [false, false] => PointerEventButton::Other,
                };

                if position.x < 0.0 {
                    position.x = 0.0;
                } else if position.x > (info.resolution().0 - pointer_x) as f32 {
                    position.x = (info.resolution().0 - pointer_x) as f32;
                    mouse.relative_movement[0] = (info.resolution().0) as i32;
                }

                if position.y < 0.0 {
                    position.y = 0.0;
                } else if position.y > (info.resolution().1 - pointer_y) as f32 {
                    position.y = (info.resolution().1 - pointer_y) as f32;
                    mouse.relative_movement[1] = (info.resolution().1) as i32;
                }

                self.window.dispatch_event_with_result(WindowEvent::PointerMoved { position })?;
                self.window.dispatch_event_with_result(WindowEvent::PointerExited {})?;
                self.window.dispatch_event_with_result(WindowEvent::PointerPressed { position, button })?;
                self.window
                    .dispatch_event_with_result(WindowEvent::PointerReleased { position, button })?;
                is_mouse_move = true;
            }

            if is_mouse_move {
                self.window.request_redraw();
                is_mouse_move = false;
            };

            self.window.draw_if_needed(|renderer| {
                renderer.render(&mut fb, info.resolution().0);

                // SAFETY: SlintBltPixel is a repr(transparent) BltPixel so it is safe to transform.
                let blt_fb =
                    unsafe { slice::from_raw_parts(fb.as_ptr() as *const BltPixel, fb.len()) };
                let blt_mfb = unsafe {
                    slice::from_raw_parts_mut(mfb.as_mut_ptr() as *mut BltPixel, mfb.len())
                };

                // We could let the software renderer draw to gop.frame_buffer() directly, but that
                // requires dealing with different frame buffer formats. The blit buffer is easier to
                // deal with and guaranteed to be available by the UEFI spec. This also reduces tearing
                // by quite a bit.
                gop.blt(BltOp::BufferToVideo {
                    buffer: blt_fb,
                    src: BltRegion::Full,
                    dest: (0, 0),
                    dims: info.resolution(),
                })
                .unwrap();

                // get framebuffer from UEFI.
                gop.blt(BltOp::VideoToBltBuffer {
                    buffer: blt_mfb,
                    src: (position.x as usize, position.y as usize),
                    dest: BltRegion::Full,
                    dims: (pointer_x, pointer_y),
                })
                .unwrap();

                // mouse cursor RGBA render to framebuffer.
                for y in 0..pointer_y {
                    for x in 0..pointer_x {
                        vec_png[x + y * pointer_x].blend_blt_pixel(&mut mfb[x + y * pointer_x]);
                    }
                }

                // write framebuffer to UEFI.
                gop.blt(BltOp::BufferToVideo {
                    buffer: blt_mfb,
                    src: BltRegion::Full,
                    dest: (position.x as usize, position.y as usize),
                    dims: (pointer_x, pointer_y),
                })
                .unwrap();
            });

            if !self.window.has_active_animations() {
                wait_for_input(slint::platform::duration_until_next_timer_update());
            }
        }
    }
}

#[entry]
fn main() -> Status {
    slint::platform::set_platform(Box::<Platform>::default()).unwrap();

    let ui = Demo::new().unwrap();

    ui.set_firmware_vendor(
        String::from_utf16_lossy(uefi::system::firmware_vendor().to_u16_slice()).into(),
    );
    ui.set_firmware_version(
        format!(
            "{}.{:02}",
            uefi::system::firmware_revision() >> 16,
            uefi::system::firmware_revision() & 0xffff
        )
        .into(),
    );
    ui.set_uefi_version(uefi::system::uefi_revision().to_string().into());

    let mut buf = [0u8; 1];
    let guid = uefi::runtime::VariableVendor::GLOBAL_VARIABLE;
    let sb = uefi::runtime::get_variable(cstr16!("SecureBoot"), &guid, &mut buf);
    ui.set_secure_boot(if sb.is_ok() { buf[0] == 1 } else { false });

    ui.run().unwrap();

    Status::SUCCESS
}
