use crate::{
    geometry::{
        rect::RectF,
        vector::{vec2f, Vector2F},
    },
    platform::{
        self,
        mac::{platform::NSViewLayerContentsRedrawDuringViewResize, renderer::Renderer},
        Event, FontSystem, WindowBounds,
    },
    Scene,
};
use cocoa::{
    appkit::{NSScreen, NSSquareStatusItemLength, NSStatusBar, NSStatusItem, NSView, NSWindow},
    base::{id, nil, YES},
    foundation::{NSPoint, NSRect, NSSize},
};
use ctor::ctor;
use foreign_types::ForeignTypeRef;
use objc::{
    class,
    declare::ClassDecl,
    msg_send,
    rc::StrongPtr,
    runtime::{Class, Object, Protocol, Sel},
    sel, sel_impl,
};
use std::{
    cell::RefCell,
    ffi::c_void,
    ptr,
    rc::{Rc, Weak},
    sync::Arc,
};

use super::screen::Screen;

static mut VIEW_CLASS: *const Class = ptr::null();
const STATE_IVAR: &str = "state";

#[ctor]
unsafe fn build_classes() {
    VIEW_CLASS = {
        let mut decl = ClassDecl::new("GPUIStatusItemView", class!(NSView)).unwrap();
        decl.add_ivar::<*mut c_void>(STATE_IVAR);

        decl.add_method(sel!(dealloc), dealloc_view as extern "C" fn(&Object, Sel));

        decl.add_method(
            sel!(mouseDown:),
            handle_view_event as extern "C" fn(&Object, Sel, id),
        );
        decl.add_method(
            sel!(mouseUp:),
            handle_view_event as extern "C" fn(&Object, Sel, id),
        );
        decl.add_method(
            sel!(rightMouseDown:),
            handle_view_event as extern "C" fn(&Object, Sel, id),
        );
        decl.add_method(
            sel!(rightMouseUp:),
            handle_view_event as extern "C" fn(&Object, Sel, id),
        );
        decl.add_method(
            sel!(otherMouseDown:),
            handle_view_event as extern "C" fn(&Object, Sel, id),
        );
        decl.add_method(
            sel!(otherMouseUp:),
            handle_view_event as extern "C" fn(&Object, Sel, id),
        );
        decl.add_method(
            sel!(mouseMoved:),
            handle_view_event as extern "C" fn(&Object, Sel, id),
        );
        decl.add_method(
            sel!(mouseDragged:),
            handle_view_event as extern "C" fn(&Object, Sel, id),
        );
        decl.add_method(
            sel!(scrollWheel:),
            handle_view_event as extern "C" fn(&Object, Sel, id),
        );
        decl.add_method(
            sel!(flagsChanged:),
            handle_view_event as extern "C" fn(&Object, Sel, id),
        );
        decl.add_method(
            sel!(makeBackingLayer),
            make_backing_layer as extern "C" fn(&Object, Sel) -> id,
        );
        decl.add_method(
            sel!(viewDidChangeEffectiveAppearance),
            view_did_change_effective_appearance as extern "C" fn(&Object, Sel),
        );

        decl.add_protocol(Protocol::get("CALayerDelegate").unwrap());
        decl.add_method(
            sel!(displayLayer:),
            display_layer as extern "C" fn(&Object, Sel, id),
        );

        decl.register()
    };
}

pub struct StatusItem(Rc<RefCell<StatusItemState>>);

struct StatusItemState {
    native_item: StrongPtr,
    native_view: StrongPtr,
    renderer: Renderer,
    scene: Option<Scene>,
    event_callback: Option<Box<dyn FnMut(Event) -> bool>>,
    appearance_changed_callback: Option<Box<dyn FnMut()>>,
}

impl StatusItem {
    pub fn add(fonts: Arc<dyn FontSystem>) -> Self {
        unsafe {
            let renderer = Renderer::new(false, fonts);
            let status_bar = NSStatusBar::systemStatusBar(nil);
            let native_item =
                StrongPtr::retain(status_bar.statusItemWithLength_(NSSquareStatusItemLength));

            let button = native_item.button();
            let _: () = msg_send![button, setHidden: YES];

            let native_view = msg_send![VIEW_CLASS, alloc];
            let state = Rc::new(RefCell::new(StatusItemState {
                native_item,
                native_view: StrongPtr::new(native_view),
                renderer,
                scene: None,
                event_callback: None,
                appearance_changed_callback: None,
            }));

            let parent_view = button.superview().superview();
            NSView::initWithFrame_(
                native_view,
                NSRect::new(NSPoint::new(0., 0.), NSView::frame(parent_view).size),
            );
            (*native_view).set_ivar(
                STATE_IVAR,
                Weak::into_raw(Rc::downgrade(&state)) as *const c_void,
            );
            native_view.setWantsBestResolutionOpenGLSurface_(YES);
            native_view.setWantsLayer(YES);
            let _: () = msg_send![
                native_view,
                setLayerContentsRedrawPolicy: NSViewLayerContentsRedrawDuringViewResize
            ];

            parent_view.addSubview_(native_view);

            {
                let state = state.borrow();
                let layer = state.renderer.layer();
                let scale_factor = state.scale_factor();
                let size = state.content_size() * scale_factor;
                layer.set_contents_scale(scale_factor.into());
                layer.set_drawable_size(metal::CGSize::new(size.x().into(), size.y().into()));
            }

            Self(state)
        }
    }
}

impl platform::Window for StatusItem {
    fn bounds(&self) -> WindowBounds {
        self.0.borrow().bounds()
    }

    fn content_size(&self) -> Vector2F {
        self.0.borrow().content_size()
    }

    fn scale_factor(&self) -> f32 {
        self.0.borrow().scale_factor()
    }

    fn titlebar_height(&self) -> f32 {
        0.
    }

    fn appearance(&self) -> platform::Appearance {
        unsafe {
            let appearance: id =
                msg_send![self.0.borrow().native_item.button(), effectiveAppearance];
            platform::Appearance::from_native(appearance)
        }
    }

    fn screen(&self) -> Rc<dyn platform::Screen> {
        unsafe {
            Rc::new(Screen {
                native_screen: self.0.borrow().native_window().screen(),
            })
        }
    }

    fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
        self
    }

    fn set_input_handler(&mut self, _: Box<dyn platform::InputHandler>) {}

    fn prompt(
        &self,
        _: crate::platform::PromptLevel,
        _: &str,
        _: &[&str],
    ) -> postage::oneshot::Receiver<usize> {
        unimplemented!()
    }

    fn activate(&self) {
        unimplemented!()
    }

    fn set_title(&mut self, _: &str) {
        unimplemented!()
    }

    fn set_edited(&mut self, _: bool) {
        unimplemented!()
    }

    fn show_character_palette(&self) {
        unimplemented!()
    }

    fn minimize(&self) {
        unimplemented!()
    }

    fn zoom(&self) {
        unimplemented!()
    }

    fn present_scene(&mut self, scene: Scene) {
        self.0.borrow_mut().scene = Some(scene);
        unsafe {
            let _: () = msg_send![*self.0.borrow().native_view, setNeedsDisplay: YES];
        }
    }

    fn toggle_full_screen(&self) {
        unimplemented!()
    }

    fn on_event(&mut self, callback: Box<dyn FnMut(platform::Event) -> bool>) {
        self.0.borrow_mut().event_callback = Some(callback);
    }

    fn on_active_status_change(&mut self, _: Box<dyn FnMut(bool)>) {}

    fn on_resize(&mut self, _: Box<dyn FnMut()>) {}

    fn on_fullscreen(&mut self, _: Box<dyn FnMut(bool)>) {}

    fn on_moved(&mut self, _: Box<dyn FnMut()>) {}

    fn on_should_close(&mut self, _: Box<dyn FnMut() -> bool>) {}

    fn on_close(&mut self, _: Box<dyn FnOnce()>) {}

    fn on_appearance_changed(&mut self, callback: Box<dyn FnMut()>) {
        self.0.borrow_mut().appearance_changed_callback = Some(callback);
    }

    fn is_topmost_for_position(&self, _: Vector2F) -> bool {
        true
    }
}

impl StatusItemState {
    fn bounds(&self) -> WindowBounds {
        unsafe {
            let window: id = self.native_window();
            let screen_frame = window.screen().visibleFrame();
            let window_frame = NSWindow::frame(window);
            let origin = vec2f(
                window_frame.origin.x as f32,
                (window_frame.origin.y - screen_frame.size.height - window_frame.size.height)
                    as f32,
            );
            let size = vec2f(
                window_frame.size.width as f32,
                window_frame.size.height as f32,
            );
            WindowBounds::Fixed(RectF::new(origin, size))
        }
    }

    fn content_size(&self) -> Vector2F {
        unsafe {
            let NSSize { width, height, .. } =
                NSView::frame(self.native_item.button().superview().superview()).size;
            vec2f(width as f32, height as f32)
        }
    }

    fn scale_factor(&self) -> f32 {
        unsafe {
            let window: id = msg_send![self.native_item.button(), window];
            NSScreen::backingScaleFactor(window.screen()) as f32
        }
    }

    pub fn native_window(&self) -> id {
        unsafe { msg_send![self.native_item.button(), window] }
    }
}

extern "C" fn dealloc_view(this: &Object, _: Sel) {
    unsafe {
        drop_state(this);

        let _: () = msg_send![super(this, class!(NSView)), dealloc];
    }
}

extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) {
    unsafe {
        if let Some(state) = get_state(this).upgrade() {
            let mut state_borrow = state.as_ref().borrow_mut();
            if let Some(event) =
                Event::from_native(native_event, Some(state_borrow.content_size().y()))
            {
                if let Some(mut callback) = state_borrow.event_callback.take() {
                    drop(state_borrow);
                    callback(event);
                    state.borrow_mut().event_callback = Some(callback);
                }
            }
        }
    }
}

extern "C" fn make_backing_layer(this: &Object, _: Sel) -> id {
    if let Some(state) = unsafe { get_state(this).upgrade() } {
        let state = state.borrow();
        state.renderer.layer().as_ptr() as id
    } else {
        nil
    }
}

extern "C" fn display_layer(this: &Object, _: Sel, _: id) {
    unsafe {
        if let Some(state) = get_state(this).upgrade() {
            let mut state = state.borrow_mut();
            if let Some(scene) = state.scene.take() {
                state.renderer.render(&scene);
            }
        }
    }
}

extern "C" fn view_did_change_effective_appearance(this: &Object, _: Sel) {
    unsafe {
        if let Some(state) = get_state(this).upgrade() {
            let mut state_borrow = state.as_ref().borrow_mut();
            if let Some(mut callback) = state_borrow.appearance_changed_callback.take() {
                drop(state_borrow);
                callback();
                state.borrow_mut().appearance_changed_callback = Some(callback);
            }
        }
    }
}

unsafe fn get_state(object: &Object) -> Weak<RefCell<StatusItemState>> {
    let raw: *mut c_void = *object.get_ivar(STATE_IVAR);
    let weak1 = Weak::from_raw(raw as *mut RefCell<StatusItemState>);
    let weak2 = weak1.clone();
    let _ = Weak::into_raw(weak1);
    weak2
}

unsafe fn drop_state(object: &Object) {
    let raw: *const c_void = *object.get_ivar(STATE_IVAR);
    Weak::from_raw(raw as *const RefCell<StatusItemState>);
}
