use std::sync::{Arc, Mutex};

use wgpu::{
  naga::FastHashMap, Backends, CommandEncoder, Device, DeviceDescriptor, Features, Instance, InstanceDescriptor, InstanceFlags, Queue, RequestAdapterOptions, Surface, SurfaceConfiguration, Texture, TextureFormat, TextureUsages
};
use winit::{
  dpi::PhysicalSize, event::{ElementState, KeyEvent, WindowEvent}, keyboard::{KeyCode, PhysicalKey}, window::Window
};

use crate::game::Game;

pub struct Engine<'a> {
  surface: Surface<'a>,
  pub device: Arc<Device>,
  pub queue: Arc<Queue>,
  pub config: Arc<Mutex<SurfaceConfiguration>>,
  pub size: PhysicalSize<u32>,
  pub window: Arc<Window>,

  keys: Keys,
  state: Game,
  focused: bool,
}

#[derive(Debug, Default)]
pub enum KeyState {
  #[default]
  Released,
  Pressed,
  Held,
}

impl KeyState {
  fn is_down(&self) -> bool {
    match self {
      Self::Released => false,
      Self::Pressed | Self::Held => true,
    }
  }
}

#[derive(Default)]
pub struct Keys {
  values: FastHashMap<KeyCode, KeyState>,
}

impl Keys {
  fn update_key(&mut self, key: KeyCode, state: ElementState) {
    let key_state = self.values.entry(key).or_default();
    if key_state.is_down() != state.is_pressed() {
      *key_state = if state.is_pressed() {
        KeyState::Pressed
      } else {
        KeyState::Released
      }
    }
  }

  fn tick_keys(&mut self) {
    for (_, state) in &mut self.values {
      if state.is_down() {
        *state = KeyState::Held;
      }
    }
  }

  pub fn is_key_down(&self, key: KeyCode) -> bool {
    if let Some(key_state) = self.values.get(&key) {
      key_state.is_down()
    } else {
      false
    }
  }

  pub fn was_key_released(&self, key: KeyCode) -> bool {
    !self.is_key_down(key)
  }

  pub fn was_key_pressed(&self, key: KeyCode) -> bool {
    matches!(self.values.get(&key), Some(KeyState::Pressed))
  }
}

impl<'a> Engine<'a> {
  pub fn new(window: Arc<Window>) -> Self {
    let instance = Instance::new(InstanceDescriptor {
      backends: Backends::all(),
      flags: InstanceFlags::advanced_debugging(),
      ..Default::default()
    });

    let surface = instance.create_surface(window.clone()).unwrap();

    let (adapter, device, queue) = async_std::task::block_on(async {
      let adapter = instance
        .request_adapter(&RequestAdapterOptions {
          compatible_surface: Some(&surface),
          ..Default::default()
        })
        .await
        .unwrap();

      let (device, queue) = adapter
        .request_device(
          &DeviceDescriptor {
            label: Some("device"),
            required_features: Features::empty(),
            ..Default::default()
          },
          None,
        )
        .await
        .unwrap();

      (adapter, device, queue)
    });

    let (device, queue) = (Arc::new(device), Arc::new(queue));

    let size = window.inner_size();
    let caps = surface.get_capabilities(&adapter);
    let format =
      caps.formats.iter().find(|f| matches!(f, TextureFormat::Bgra8Unorm)).cloned().unwrap_or(caps.formats[0]);
    let config = Arc::new(Mutex::new(SurfaceConfiguration {
      usage: TextureUsages::RENDER_ATTACHMENT,
      format,
      width: size.width,
      height: size.height,
      present_mode: caps.present_modes[0],
      desired_maximum_frame_latency: 2,
      alpha_mode: caps.alpha_modes[0],
      view_formats: vec![],
    }));

    surface.configure(&device, &config.lock().unwrap());

    let keys = Keys::default();
    let state = Game::new(&UpdateContext {
      device: &device,
      queue: &queue,
      window: &window,
      keys: &keys,
      config: &config.clone(),
    });

    Self {
      surface,
      device,
      queue,
      config,
      size,
      window,

      keys: Keys::default(),
      focused: false,

      state,
    }
  }

  pub fn handle_event(&mut self, window_event: &WindowEvent) {
    match window_event {
      WindowEvent::KeyboardInput {
        event:
          KeyEvent {
            physical_key: PhysicalKey::Code(key),
            state,
            repeat,
            ..
          },
        ..
      } => {
        if *repeat {
          return;
        }
        self.keys.update_key(*key, *state);
      }
      WindowEvent::Focused(focused) => {
        self.focused = *focused;
      }
      _ => {}
    }
  }

  pub fn resize(&mut self, new_size: PhysicalSize<u32>) {
    if new_size.width > 0 && new_size.height > 0 {
      self.size = new_size;
      {
        let mut config = self.config.lock().unwrap();
        config.width = new_size.width;
        config.height = new_size.height;
        self.surface.configure(&self.device, &config);
      }

      self.state.resize(
        &UpdateContext {
          device: &self.device,
          queue: &self.queue,
          keys: &self.keys,
          window: &self.window,
          config: &self.config,
        },
        new_size,
      );
    }
  }

  pub fn update(&mut self) {
    self.state.update(&UpdateContext {
      device: &self.device,
      queue: &self.queue,
      keys: &self.keys,
      window: &self.window,
      config: &self.config,
    });
    self.keys.tick_keys();
  }

  pub fn render(&self) -> Result<(), wgpu::SurfaceError> {
    let output = self.surface.get_current_texture()?;

    let mut encoder = self.device.create_command_encoder(&Default::default());

    self.state.render(self, &output.texture, &mut encoder);

    self.queue.submit(std::iter::once(encoder.finish()));
    output.present();

    Ok(())
  }
}

pub struct UpdateContext<'a> {
  pub device: &'a Arc<Device>,
  pub queue: &'a Arc<Queue>,
  pub keys: &'a Keys,
  pub window: &'a Arc<Window>,
  pub config: &'a Arc<Mutex<SurfaceConfiguration>>,
}

pub trait EngineState {
  fn new(context: &UpdateContext) -> Self
  where
    Self: Sized;
  fn update(&mut self, context: &UpdateContext);
  fn resize(&mut self, context: &UpdateContext, new_size: PhysicalSize<u32>);
  fn render(&self, engine: &Engine, surface: &Texture, encoder: &mut CommandEncoder);
}