use std::{
  cell::RefCell, collections::VecDeque, num::NonZero, path::Path, rc::{Rc, Weak}, sync::Arc
};

use bytemuck::{bytes_of, Pod, Zeroable};
use glam::{Mat4, Vec3, Vec4};
use num_traits::FloatConst;
use wgpu::{
  include_wgsl, naga::FastHashMap, util::{BufferInitDescriptor, DeviceExt}, BindGroup, BindGroupDescriptor, BindGroupEntry, BindGroupLayoutDescriptor, BindGroupLayoutEntry, BindingResource, BlendState, Buffer, BufferUsages, ColorTargetState, ColorWrites, DepthStencilState, Device, FragmentState, PipelineLayout, PipelineLayoutDescriptor, PrimitiveState, Queue, RenderPipeline, RenderPipelineDescriptor, ShaderModule, ShaderStages, Texture, TextureFormat, VertexState
};
use winit::window::Window;

use crate::engine::UpdateContext;

use super::{
  loaded_file::{LoadedFile, SpriteUvs}, AnmVm
};

mod location;
mod rendering;

pub use location::VmLocation;
pub type Vm = Rc<RefCell<AnmVm>>;
pub type WeakVm = Weak<RefCell<AnmVm>>;

#[derive(Clone, Copy, Pod, Zeroable)]
#[repr(C, packed)]
struct Uniform {
  proj_matrix: Mat4,
}

pub struct Manager {
  anm_files: FastHashMap<String, Arc<LoadedFile>>,
  world_backbuffer_anm: WeakVm,

  ui_vms: VecDeque<WeakVm>,
  world_vms: VecDeque<WeakVm>,

  depth_texture: Texture,
  ui_uniform: Uniform,
  _world_uniform: Uniform,
  uniform_buffer: Buffer,
  render_bind_group: BindGroup,
  render_pipeline_layout: PipelineLayout,
  render_shader: ShaderModule,
  clear_pipeline: RenderPipeline,
  blit_pipeline: RenderPipeline,
}

impl Manager {
  pub fn new(context: &UpdateContext) -> Manager {
    let depth_texture = Self::create_depth_texture(context.device, context.window.inner_size());

    let texture_bind_group_layout = context.device.create_bind_group_layout(&BindGroupLayoutDescriptor {
      entries: &[BindGroupLayoutEntry {
        binding: 0,
        ty: wgpu::BindingType::Texture {
          sample_type: wgpu::TextureSampleType::Float { filterable: true },
          view_dimension: wgpu::TextureViewDimension::D2,
          multisampled: false,
        },
        count: None,
        visibility: ShaderStages::FRAGMENT,
      }],
      label: None,
    });

    let bind_group_layout = context.device.create_bind_group_layout(&BindGroupLayoutDescriptor {
      label: None,
      entries: &[
        BindGroupLayoutEntry {
          binding: 0,
          ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
          count: None,
          visibility: ShaderStages::FRAGMENT,
        },
        BindGroupLayoutEntry {
          binding: 1,
          count: None,
          visibility: ShaderStages::VERTEX,
          ty: wgpu::BindingType::Buffer {
            ty: wgpu::BufferBindingType::Uniform,
            has_dynamic_offset: false,
            min_binding_size: Some(NonZero::new(std::mem::size_of::<Uniform>() as u64).unwrap()),
          },
        },
      ],
    });

    let sampler = context.device.create_sampler(&wgpu::SamplerDescriptor {
      label: Some("sampler"),
      address_mode_u: wgpu::AddressMode::ClampToEdge,
      address_mode_v: wgpu::AddressMode::ClampToEdge,
      address_mode_w: wgpu::AddressMode::ClampToEdge,
      mag_filter: wgpu::FilterMode::Linear,
      min_filter: wgpu::FilterMode::Nearest,
      mipmap_filter: wgpu::FilterMode::Nearest,
      ..Default::default()
    });

    let ui_uniform = Uniform {
      proj_matrix: Mat4::orthographic_lh(0.0, 640.0, 480.0, 0.0, 100.0, -100.0),
    };

    let uniform_buffer = context.device.create_buffer_init(&BufferInitDescriptor {
      contents: bytes_of(&ui_uniform),
      label: Some("uniform"),
      usage: BufferUsages::UNIFORM | BufferUsages::COPY_DST,
    });

    let render_bind_group = context.device.create_bind_group(&BindGroupDescriptor {
      label: Some("render bind group"),
      layout: &bind_group_layout,
      entries: &[
        BindGroupEntry {
          binding: 0,
          resource: BindingResource::Sampler(&sampler),
        },
        BindGroupEntry {
          binding: 1,
          resource: BindingResource::Buffer(uniform_buffer.as_entire_buffer_binding()),
        },
      ],
    });

    let pipeline_layout = context.device.create_pipeline_layout(&PipelineLayoutDescriptor {
      label: None,
      bind_group_layouts: &[&texture_bind_group_layout, &bind_group_layout],
      push_constant_ranges: &[],
    });

    let render_shader = context.device.create_shader_module(include_wgsl!("./render.wgsl"));
    let blit_shader = context.device.create_shader_module(include_wgsl!("./blit_screen.wgsl"));
    let clear_shader = context.device.create_shader_module(include_wgsl!("./clear.wgsl"));

    let config = context.config.lock().unwrap();

    let clear_pipeline = context.device.create_render_pipeline(&RenderPipelineDescriptor {
      label: Some("clear"),
      layout: Some(&context.device.create_pipeline_layout(&PipelineLayoutDescriptor {
        label: None,
        bind_group_layouts: &[],
        push_constant_ranges: &[],
      })),
      vertex: VertexState {
        buffers: &[],
        module: &clear_shader,
        compilation_options: Default::default(),
        entry_point: "vs_main",
      },
      primitive: PrimitiveState {
        topology: wgpu::PrimitiveTopology::TriangleStrip,
        ..Default::default()
      },
      depth_stencil: Some(DepthStencilState {
        format: TextureFormat::Depth32Float,
        depth_write_enabled: true,
        depth_compare: wgpu::CompareFunction::Less,
        stencil: wgpu::StencilState::default(),
        bias: wgpu::DepthBiasState::default(),
      }),
      multisample: Default::default(),
      fragment: Some(FragmentState {
        targets: &[Some(ColorTargetState {
          format: config.format,
          blend: Some(BlendState::REPLACE),
          write_mask: ColorWrites::ALL,
        })],
        compilation_options: Default::default(),
        entry_point: "fs_main",
        module: &clear_shader,
      }),
      multiview: None,
      cache: None,
    });

    let blit_pipeline = context.device.create_render_pipeline(&RenderPipelineDescriptor {
      label: Some("blit"),
      layout: Some(&pipeline_layout),
      vertex: VertexState {
        buffers: &[],
        module: &blit_shader,
        compilation_options: Default::default(),
        entry_point: "vs_main",
      },
      primitive: PrimitiveState {
        topology: wgpu::PrimitiveTopology::TriangleStrip,
        ..Default::default()
      },
      depth_stencil: Some(DepthStencilState {
        format: TextureFormat::Depth32Float,
        depth_write_enabled: true,
        depth_compare: wgpu::CompareFunction::Less,
        stencil: wgpu::StencilState::default(),
        bias: wgpu::DepthBiasState::default(),
      }),
      multisample: Default::default(),
      fragment: Some(FragmentState {
        targets: &[Some(ColorTargetState {
          format: config.format,
          blend: Some(BlendState::ALPHA_BLENDING),
          write_mask: ColorWrites::ALL,
        })],
        compilation_options: Default::default(),
        entry_point: "fs_main",
        module: &blit_shader,
      }),
      multiview: None,
      cache: None,
    });
    let world_uniform = Uniform {
      proj_matrix: Mat4::perspective_lh(
        45.0 * (f32::PI() / 180.0),
        config.width as f32 / config.height as f32,
        0.1,
        1000.0,
      ),
    };

    Manager {
      anm_files: FastHashMap::default(),
      world_backbuffer_anm: Weak::new(),

      ui_vms: VecDeque::new(),
      world_vms: VecDeque::new(),

      depth_texture,
      ui_uniform,
      _world_uniform: world_uniform,
      uniform_buffer,
      render_bind_group,
      render_pipeline_layout: pipeline_layout,
      render_shader,
      clear_pipeline,
      blit_pipeline,
    }
  }

  pub fn load_anm(
    &mut self,
    device: &Device,
    queue: &Queue,
    window: &Window,
    file_name: impl AsRef<Path>,
  ) -> Arc<LoadedFile> {
    let file_name_str = file_name.as_ref().to_str().unwrap().to_owned();
    if let Some(loaded_anm) = self.anm_files.get(&file_name_str) {
      return loaded_anm.clone();
    }

    let file_data = std::fs::read(Path::new("./assets").join(file_name)).expect("failed to load anm file");
    let loaded_anm = Arc::new(LoadedFile::load(device, queue, window, &file_name_str, &file_data));

    self.anm_files.insert(file_name_str.to_owned(), loaded_anm.clone());
    loaded_anm
  }

  #[allow(unused)]
  pub fn set_world_backbuffer_anm(&mut self, file: Arc<LoadedFile>, script: usize) -> Vm {
    let vm = Rc::new(RefCell::new(AnmVm::new(file, None, false, false, script, 0)));
    self.update_single(&vm);

    self.world_backbuffer_anm = Rc::downgrade(&vm);

    vm
  }

  pub fn new_vm(
    &mut self,
    file: Arc<LoadedFile>,
    origin: Option<Vec3>,
    ticked_by_parent: bool,
    debug: bool,
    script: usize,
    location: VmLocation,
  ) -> Vm {
    let layer = location.layer();

    let vm = Rc::new(RefCell::new(AnmVm::new(
      file,
      origin,
      ticked_by_parent,
      debug,
      script,
      layer,
    )));
    self.update_single(&vm);

    let mut context = ManagerUpdate::new();
    vm.borrow_mut().tick(&mut context);

    let (vm_list, front) = match location {
      VmLocation::Ui { front, .. } => (&mut self.ui_vms, front),
      VmLocation::World { front, .. } => (&mut self.world_vms, front),
    };

    if front {
      vm_list.push_front(Rc::downgrade(&vm))
    } else {
      vm_list.push_back(Rc::downgrade(&vm));
    }

    vm
  }

  fn update_single(&mut self, vm: &Vm) {
    let mut context = ManagerUpdate::new();
    vm.borrow_mut().tick(&mut context);
    context.apply_lists(&mut self.ui_vms, &mut self.world_vms);
  }

  fn update_list(list: &mut VecDeque<WeakVm>, context: &mut ManagerUpdate) {
    list.retain(|value| {
      if let Some(rc) = value.upgrade() {
        !rc.borrow().deleted
      } else {
        false
      }
    });

    for vm in list {
      if let Some(vm) = vm.upgrade() {
        if !vm.borrow().ticked_by_parent {
          vm.borrow_mut().tick(context);
        }
      }
    }
  }

  pub fn update(&mut self) {
    let mut context = ManagerUpdate::new();

    Self::update_list(&mut self.world_vms, &mut context);
    Self::update_list(&mut self.ui_vms, &mut context);

    context.apply_lists(&mut self.ui_vms, &mut self.world_vms);
  }
}

#[derive(Default)]
pub(super) struct ManagerUpdate {
  new_ui_vms_front: Vec<WeakVm>,
  new_ui_vms_back: Vec<WeakVm>,
  new_world_vms_front: Vec<WeakVm>,
  new_world_vms_back: Vec<WeakVm>,
}

impl ManagerUpdate {
  fn new() -> Self {
    Self::default()
  }

  pub fn new_vm(
    &mut self,
    file: Arc<LoadedFile>,
    origin: Option<Vec3>,
    ticked_by_parent: bool,
    debug: bool,
    script: usize,
    location: VmLocation,
  ) -> Vm {
    let layer = location.layer();

    let vm = Rc::new(RefCell::new(AnmVm::new(
      file,
      origin,
      ticked_by_parent,
      debug,
      script,
      layer,
    )));
    vm.borrow_mut().tick(self);

    let vm_list = match location {
      VmLocation::Ui { front, .. } => {
        if front {
          &mut self.new_ui_vms_front
        } else {
          &mut self.new_ui_vms_back
        }
      }
      VmLocation::World { front, .. } => {
        if front {
          &mut self.new_world_vms_front
        } else {
          &mut self.new_world_vms_back
        }
      }
    };

    vm_list.push(Rc::downgrade(&vm));

    vm
  }

  fn apply_to_list(list: &mut VecDeque<WeakVm>, front: Vec<WeakVm>, back: Vec<WeakVm>) {
    for vm in front.into_iter().rev() {
      list.push_front(vm.clone());
    }
    list.extend(back.into_iter());
  }

  fn apply_lists(self, ui_list: &mut VecDeque<WeakVm>, world_list: &mut VecDeque<WeakVm>) {
    Self::apply_to_list(ui_list, self.new_ui_vms_front, self.new_ui_vms_back);
    Self::apply_to_list(world_list, self.new_world_vms_front, self.new_world_vms_back);
  }
}
