use std::{future::Future, sync::Arc};

use ribir_core::{
  prelude::*,
  window::{
    BoxShellWindow, RedrawDemand, Shell, ShellWindow, UiEvent, WindowAttributes, WindowId,
    WindowLevel,
  },
};
use winit::dpi::{LogicalPosition, LogicalSize};

#[cfg(target_arch = "wasm32")]
pub const RIBIR_CANVAS: &str = "ribir_canvas";
#[cfg(target_arch = "wasm32")]
pub const RIBIR_CONTAINER: &str = "ribir_container";

#[cfg(feature = "debug")]
use ribir_core::debug_tool::{FRAME_TX, FramePacket, now_unix_ms};

use crate::{
  app::{App, CmdSender},
  backends::*,
};

pub enum ShellCmd {
  Exit,
  RequestDraw {
    id: WindowId,
    demand: RedrawDemand,
  },
  Draw {
    id: WindowId,
    wnd_size: Size,
    viewport: Rect,
    surface_color: Color,
    commands: Vec<PaintCommand>,
    glyph_provider: GlyphRasterSourceRef,
  },
  Close {
    id: WindowId,
  },
  RunAsync {
    fut: BoxFuture<'static, ()>,
  },
  /// Create a new window. The winit window is created synchronously in the
  /// event loop (where active_event_loop is available), then the backend is
  /// initialized asynchronously.
  NewWindow {
    attrs: Box<WindowAttributes>,
    sender: tokio::sync::oneshot::Sender<BoxShellWindow>,
  },
}

impl ShellCmd {
  pub fn wnd_id(&self) -> Option<WindowId> {
    match self {
      ShellCmd::RequestDraw { id, .. } | ShellCmd::Draw { id, .. } | ShellCmd::Close { id } => {
        Some(*id)
      }
      ShellCmd::RunAsync { .. } | ShellCmd::Exit | ShellCmd::NewWindow { .. } => None,
    }
  }
}

pub(crate) struct RibirShell {
  pub(crate) cmd_sender: CmdSender,
}

impl Shell for RibirShell {
  fn new_shell_window(
    &self, attrs: ribir_core::window::WindowAttributes,
  ) -> BoxFuture<'static, BoxShellWindow> {
    let (sender, receiver) = tokio::sync::oneshot::channel::<BoxShellWindow>();

    // Send NewWindow command to create winit window synchronously in the event
    // loop, where active_event_loop is available. This fixes the WASM bug where
    // spawn_local schedules futures outside the event loop callback.
    self
      .cmd_sender
      .send(ShellCmd::NewWindow { attrs: Box::new(attrs), sender });

    Box::pin(async move { receiver.await.unwrap() })
  }

  fn exit(&self) { self.cmd_sender.send(ShellCmd::Exit); }

  fn run_in_shell(&self, fut: BoxFuture<'static, ()>) {
    self.cmd_sender.send(ShellCmd::RunAsync { fut });
  }
}

pub(crate) struct ShellWndHandle {
  pub(crate) sender: CmdSender,
  pub(crate) winit_wnd: Arc<winit::window::Window>,
  pub(crate) cursor: CursorIcon,
}

pub trait WinitBackend<'a>: Sized {
  fn new(window: &'a winit::window::Window) -> impl Future<Output = Self>;

  fn on_resize(&mut self, size: DeviceSize);

  fn begin_frame(&mut self, surface_color: Color);

  fn draw_commands(
    &mut self, viewport: DeviceRect, global_matrix: &Transform, commands: &[PaintCommand],
    glyph_provider: &dyn GlyphRasterSource,
  );

  fn end_frame(&mut self);

  /// Capture the current frame
  #[cfg(feature = "debug")]
  fn capture_screenshot(
    &mut self, viewport: DeviceRect, global_matrix: &Transform, commands: &[PaintCommand],
    glyph_provider: &dyn GlyphRasterSource,
  ) -> Option<BoxFuture<'static, Option<PixelImage>>>;
}

pub(crate) struct WinitShellWnd {
  pub(crate) winit_wnd: Arc<winit::window::Window>,
  backend: Backend<'static>,
}

fn window_size(winit_wnd: &winit::window::Window) -> Size {
  let size = winit_wnd
    .inner_size()
    .to_logical(winit_wnd.scale_factor());
  Size::new(size.width, size.height)
}

impl WinitShellWnd {
  pub(crate) fn id(&self) -> WindowId { new_id(self.winit_wnd.id()) }

  pub(crate) fn deal_cmd(&mut self, cmd: ShellCmd) {
    match cmd {
      ShellCmd::RequestDraw { demand, .. } => {
        if demand.requires_forced_draw() {
          App::send_event(UiEvent::RedrawRequest { wnd_id: self.id(), demand });
        } else {
          self.winit_wnd.request_redraw();
        }
      }
      ShellCmd::Draw { viewport, surface_color, commands, glyph_provider, wnd_size, .. } => {
        if wnd_size == window_size(&self.winit_wnd) {
          self.backend.begin_frame(surface_color);
          let scale_factor = self.winit_wnd.scale_factor() as f32;
          let global_matrix = Transform::scale(scale_factor, scale_factor);

          let viewport: DeviceRect = viewport
            .scale(scale_factor, scale_factor)
            .round_out()
            .to_i32()
            .cast_unit();
          self.backend.draw_commands(
            viewport,
            &global_matrix,
            &commands,
            glyph_provider.as_ref().as_ref(),
          );

          #[cfg(feature = "debug")]
          self.capture_debug_frame(
            viewport,
            &global_matrix,
            &commands,
            glyph_provider.as_ref().as_ref(),
          );

          self.backend.end_frame();
        } else {
          self.winit_wnd.request_redraw();
        }
      }
      ShellCmd::Close { id } => {
        App::remove_shell_window(id);
      }
      _ => (),
    }
  }

  pub(crate) fn on_resize(&mut self, size: DeviceSize) { self.backend.on_resize(size); }

  /// Capture and send frame to debug server if enabled.
  #[cfg(feature = "debug")]
  fn capture_debug_frame(
    &mut self, viewport: DeviceRect, global_matrix: &Transform, commands: &[PaintCommand],
    glyph_provider: &dyn GlyphRasterSource,
  ) {
    if FRAME_TX.get().is_some()
      && let Some(fut) =
        self
          .backend
          .capture_screenshot(viewport, global_matrix, commands, glyph_provider)
    {
      let wnd_id = self.id();
      App::spawn_local(async move {
        if let Some(img) = fut.await
          && let Some(frame_tx) = FRAME_TX.get()
        {
          let ts_unix_ms = now_unix_ms();
          static SEQ: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
          let seq = SEQ.fetch_add(1, std::sync::atomic::Ordering::Relaxed);

          let _ = frame_tx.send(FramePacket { wnd_id, ts_unix_ms, seq, image: Arc::new(img) });
        }
      });
    }
  }
}

impl ShellWindow for ShellWndHandle {
  fn id(&self) -> WindowId { new_id(self.winit_wnd.id()) }

  fn inner_size(&self) -> Size { window_size(&self.winit_wnd) }

  fn as_any(&self) -> &dyn Any { self }

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

  fn set_cursor(&mut self, cursor: CursorIcon) {
    self.cursor = cursor;
    self.winit_wnd.set_cursor(cursor);
  }

  fn cursor(&self) -> CursorIcon { self.cursor }

  fn focus_window(&mut self) {
    self.winit_wnd.focus_window();
    self.winit_wnd.request_redraw();
  }

  fn set_minimized(&mut self, minimized: bool) { self.winit_wnd.set_minimized(minimized); }

  fn is_minimized(&self) -> bool { self.winit_wnd.is_minimized().unwrap_or_default() }

  fn set_min_size(&mut self, size: Size) {
    self
      .winit_wnd
      .set_min_inner_size(Some(LogicalSize::new(size.width, size.height)));
  }

  fn set_icon(&mut self, icon: &PixelImage) {
    self
      .winit_wnd
      .set_window_icon(Some(img_to_winit_icon(icon)));
  }

  fn close(&self) {
    self
      .sender
      .send(ShellCmd::Close { id: self.id() });
  }

  fn set_title(&mut self, str: &str) { self.winit_wnd.set_title(str); }

  fn set_resizable(&mut self, resizable: bool) { self.winit_wnd.set_resizable(resizable); }

  fn is_resizable(&self) -> bool { self.winit_wnd.is_resizable() }

  fn set_visible(&mut self, visible: bool) { self.winit_wnd.set_visible(visible); }

  fn is_visible(&self) -> Option<bool> { self.winit_wnd.is_visible() }

  fn set_ime_allowed(&mut self, allowed: bool) { self.winit_wnd.set_ime_allowed(allowed); }

  fn set_window_level(&mut self, level: WindowLevel) { self.winit_wnd.set_window_level(level); }

  fn set_ime_cursor_area(&mut self, rect: &Rect) {
    let position: LogicalPosition<f32> = LogicalPosition::new(rect.origin.x, rect.origin.y);
    let size: LogicalSize<f32> = LogicalSize::new(rect.size.width, rect.size.height);
    self.winit_wnd.set_ime_cursor_area(position, size);
  }

  fn request_resize(&mut self, size: Size) {
    let _ = self
      .winit_wnd
      .request_inner_size(LogicalSize::new(size.width, size.height));
  }

  fn draw_commands(
    &mut self, wnd_size: Size, viewport: Rect, surface_color: Color, commands: &[PaintCommand],
  ) {
    let glyph_provider = AppCtx::text_services().raster_source();
    self.sender.send(ShellCmd::Draw {
      id: self.id(),
      wnd_size,
      viewport,
      surface_color,
      commands: commands.to_vec(),
      glyph_provider,
    });
  }

  fn request_draw(&self, demand: RedrawDemand) {
    self
      .sender
      .send(ShellCmd::RequestDraw { id: self.id(), demand });
  }

  fn position(&self) -> Point {
    let scale_factor = self.winit_wnd.scale_factor() as f32;
    self
      .winit_wnd
      .outer_position()
      .map(|pos| Point::new(pos.x as f32 / scale_factor, pos.y as f32 / scale_factor))
      .unwrap_or_default()
  }

  fn set_position(&mut self, point: Point) {
    let pos = self.position();
    if pos != point {
      self
        .winit_wnd
        .set_outer_position(LogicalPosition::new(point.x, point.y));
    }
  }
}

pub(crate) fn new_id(id: winit::window::WindowId) -> WindowId {
  let id: u64 = id.into();
  id.into()
}

impl WinitShellWnd {
  /// Synchronously create a winit window. This must be called from within the
  /// event loop callback where `active_event_loop` is available.
  #[cfg(target_arch = "wasm32")]
  pub(crate) fn create_winit_window(mut attrs: WindowAttributes) -> Arc<winit::window::Window> {
    use web_sys::wasm_bindgen::JsCast;
    use winit::platform::web::WindowAttributesExtWebSys;

    let document = web_sys::window().unwrap().document().unwrap();
    let canvas = document
      .create_element("canvas")
      .unwrap()
      .dyn_into::<web_sys::HtmlCanvasElement>()
      .unwrap();
    canvas.set_class_name(RIBIR_CANVAS);
    let style = canvas.style();
    let _ = style.set_property("width", "100%");
    let _ = style.set_property("height", "100%");
    let elems = document.get_elements_by_class_name(RIBIR_CONTAINER);

    if let Some(elem) = elems.item(0) {
      elem.set_class_name(&elem.class_name().replace(RIBIR_CONTAINER, ""));
      elem.append_child(&canvas).unwrap();
    } else if let Some(body) = document.body() {
      body.append_child(&canvas).unwrap();
    } else {
      document.append_child(&canvas).unwrap();
    }

    attrs.0 = attrs.0.with_canvas(Some(canvas));

    Arc::new(
      App::active_event_loop()
        .create_window(attrs.0)
        .unwrap(),
    )
  }

  /// Synchronously create a winit window. This must be called from within the
  /// event loop callback where `active_event_loop` is available.
  #[cfg(not(target_arch = "wasm32"))]
  pub(crate) fn create_winit_window(attrs: WindowAttributes) -> Arc<winit::window::Window> {
    Arc::new(
      App::active_event_loop()
        .create_window(attrs.0)
        .unwrap(),
    )
  }

  /// Asynchronously initialize the backend for an existing winit window.
  /// This doesn't require `active_event_loop` to be available.
  pub(crate) async fn from_winit_window(winit_wnd: Arc<winit::window::Window>) -> Self {
    let ptr = winit_wnd.as_ref() as *const winit::window::Window;
    // Safety: a reference to winit_wnd is valid as long as the WinitShellWnd is
    // alive.
    let backend = Backend::new(unsafe { &*ptr }).await;
    WinitShellWnd { backend, winit_wnd }
  }

  #[cfg(target_arch = "wasm32")]
  pub(crate) async fn new(mut attrs: WindowAttributes) -> Self {
    use web_sys::wasm_bindgen::JsCast;
    use winit::platform::web::WindowAttributesExtWebSys;

    let document = web_sys::window().unwrap().document().unwrap();
    let canvas = document
      .create_element("canvas")
      .unwrap()
      .dyn_into::<web_sys::HtmlCanvasElement>()
      .unwrap();
    canvas.set_class_name(RIBIR_CANVAS);
    let style = canvas.style();
    let _ = style.set_property("width", "100%");
    let _ = style.set_property("height", "100%");
    let elems = document.get_elements_by_class_name(RIBIR_CONTAINER);

    if let Some(elem) = elems.item(0) {
      elem.set_class_name(&elem.class_name().replace(RIBIR_CONTAINER, ""));
      elem.append_child(&canvas).unwrap();
    } else if let Some(body) = document.body() {
      body.append_child(&canvas).unwrap();
    } else {
      document.append_child(&canvas).unwrap();
    }

    attrs.0 = attrs.0.with_canvas(Some(canvas));

    Self::inner_new(attrs).await
  }

  #[cfg(not(target_arch = "wasm32"))]
  pub(crate) async fn new(attrs: WindowAttributes) -> Self { Self::inner_new(attrs).await }

  async fn inner_new(attrs: WindowAttributes) -> Self {
    let winit_wnd = Arc::new(
      App::active_event_loop()
        .create_window(attrs.0)
        .unwrap(),
    );
    let ptr = winit_wnd.as_ref() as *const winit::window::Window;
    // Safety: a reference to winit_wnd is valid as long as the WinitShellWnd is
    // alive.
    let backend = Backend::new(unsafe { &*ptr }).await;
    WinitShellWnd { backend, winit_wnd }
  }
}

fn img_to_winit_icon(icon: &PixelImage) -> winit::window::Icon {
  assert!(icon.color_format() == ColorFormat::Rgba8);
  winit::window::Icon::from_rgba(icon.pixel_bytes().to_vec(), icon.width(), icon.height()).unwrap()
}
