use std::{io::Cursor, ops::Add, sync::Arc};

use bytemuck::{Pod, Zeroable};
use glam::Vec2;
use nonoverlapping_interval_tree::NonOverlappingIntervalTree;
use truth::{context::RootEmitter, io::BinReader, AnmFile, Game};
use wgpu::{
  naga::FastHashMap, util::DeviceExt, BindGroup, BindGroupDescriptor, BindGroupEntry, BindGroupLayoutDescriptor, BindGroupLayoutEntry, Device, Extent3d, Queue, ShaderStages, Texture, TextureDescriptor, TextureDimension, TextureFormat, TextureUsages
};
use winit::window::Window;

use super::{
  image::{produce_image_from_entry, Image}, vm::opcodes::{Instruction, Op}
};

pub struct LoadedEntry {
  _texture: Texture,
  pub bind_group: BindGroup,

  _rt_texture_size: Vec2,
  pub sprites: Vec<LoadedSprite>,
}

impl LoadedEntry {
  fn load(device: &Device, queue: &Queue, window: &Window, entry: &truth::anm::Entry) -> Arc<LoadedEntry> {
    let image = entry.img_data().is_some().then(|| produce_image_from_entry(&entry).expect("failed to parse..."));

    let (width, height, usages) = if *entry.path == "@R" {
      let size = window.inner_size();
      (
        size.width,
        size.height,
        TextureUsages::TEXTURE_BINDING
          | TextureUsages::RENDER_ATTACHMENT
          | TextureUsages::COPY_SRC
          | TextureUsages::COPY_DST,
      )
    } else {
      let (width, height) = image
        .as_ref()
        .map(|image| (image.width, image.height))
        .unwrap_or((entry.specs.rt_width as usize, entry.specs.rt_height as usize));
      (width as u32, height as u32, TextureUsages::TEXTURE_BINDING)
    };

    let (texture, bind_group) = Self::init_graphics(device, queue, entry, width, height, image, usages);

    let entry_size = Vec2::new(width as f32, height as f32);

    let sprites = entry.sprites.iter().map(|(_, sprite)| LoadedSprite::load(sprite, entry_size)).collect::<Vec<_>>();

    Arc::new(LoadedEntry {
      _texture: texture,
      bind_group,
      _rt_texture_size: entry_size,
      sprites,
    })
  }

  fn init_graphics(
    device: &Device,
    queue: &Queue,
    entry: &truth::anm::Entry,
    width: u32,
    height: u32,
    image: Option<Image>,
    usages: TextureUsages,
  ) -> (Texture, BindGroup) {
    let descriptor = &TextureDescriptor {
      label: Some(entry.path.as_str()),
      size: Extent3d {
        width,
        height,
        depth_or_array_layers: 1,
      },
      mip_level_count: 1,
      sample_count: 1,
      dimension: TextureDimension::D2,
      format: TextureFormat::Bgra8Unorm,
      usage: usages,
      view_formats: &[],
    };

    let texture = if let Some(image) = image {
      device.create_texture_with_data(
        &queue,
        descriptor,
        wgpu::util::TextureDataOrder::LayerMajor,
        &image.data,
      )
    } else {
      device.create_texture(descriptor)
    };

    let bind_group = device.create_bind_group(&BindGroupDescriptor {
      label: Some("anm bind group"),
      entries: &[BindGroupEntry {
        binding: 0,
        resource: wgpu::BindingResource::TextureView(&texture.create_view(&Default::default())),
      }],
      layout: &device.create_bind_group_layout(&BindGroupLayoutDescriptor {
        entries: &[BindGroupLayoutEntry {
          binding: 0,
          count: None,
          ty: wgpu::BindingType::Texture {
            sample_type: wgpu::TextureSampleType::Float { filterable: true },
            view_dimension: wgpu::TextureViewDimension::D2,
            multisampled: false,
          },
          visibility: ShaderStages::FRAGMENT,
        }],
        label: None,
      }),
    });

    (texture, bind_group)
  }
}

#[derive(Default, Debug, Clone, Copy)]
pub struct LoadedSprite {
  pub uvs: SpriteUvs,
  pub size: Vec2,
}

#[derive(Default, Debug, Clone, Copy, Pod, Zeroable)]
#[repr(C, packed)]
pub struct SpriteUvs {
  pub top_left_uv: Vec2,
  pub bottom_right_uv: Vec2,
}

impl LoadedSprite {
  fn load(sprite: &truth::anm::Sprite, entry_size: Vec2) -> LoadedSprite {
    let offset = Vec2::new(sprite.offset[0] as f32, sprite.offset[1] as f32);
    let size = Vec2::new(sprite.size[0] as f32, sprite.size[1] as f32);
    let top_left_uv = offset / entry_size;
    let bottom_right_uv = (offset + size) / entry_size;
    LoadedSprite {
      uvs: SpriteUvs {
        top_left_uv,
        bottom_right_uv,
      },
      size,
    }
  }
}

impl Add<Vec2> for SpriteUvs {
  type Output = Self;

  fn add(self, rhs: Vec2) -> Self::Output {
    Self {
      top_left_uv: self.top_left_uv + rhs,
      bottom_right_uv: self.bottom_right_uv + rhs,
    }
  }
}

pub struct LoadedScript {
  pub instructions: Vec<Instruction>,
  pub interrupts: FastHashMap<i32, usize>,
}

impl LoadedScript {
  fn load(script: &truth::anm::Script) -> Arc<LoadedScript> {
    let mut offset_instructions: FastHashMap<_, _> = Default::default();
    let mut current_offset = 0;
    for (index, inst) in script.instrs.iter().enumerate() {
      offset_instructions.insert(current_offset, index);
      current_offset += 8 + inst.args_blob.len() as u32
    }

    let instructions: Vec<_> =
      script.instrs.iter().map(|inst| Instruction::from_raw(inst.clone(), &offset_instructions)).collect();

    let interrupts = instructions
      .iter()
      .enumerate()
      .filter_map(|(index, inst)| {
        if let Op::InterruptLabel(label) = inst.op {
          Some((label, index))
        } else {
          None
        }
      })
      .collect();

    Arc::new(LoadedScript {
      instructions,
      interrupts,
    })
  }
}

pub struct LoadedFile {
  pub(super) scripts: Vec<Arc<LoadedScript>>,
  pub(super) sprite_entries: NonOverlappingIntervalTree<u32, (u32, Arc<LoadedEntry>)>,
}

impl LoadedFile {
  pub fn load(device: &Device, queue: &Queue, window: &Window, file_name: &str, file_data: &[u8]) -> Self {
    let file = AnmFile::read_from_stream(
      &mut BinReader::from_reader(&RootEmitter::new_stderr(), file_name, Cursor::new(file_data)),
      Game::Th11,
      true,
    )
    .unwrap();

    let scripts =
      file.entries.iter().flat_map(|entry| &entry.scripts).map(|(_, script)| LoadedScript::load(script)).collect();

    let entries = file.entries.iter().map(|entry| LoadedEntry::load(device, queue, window, entry)).collect::<Vec<_>>();

    let mut sprite_entries = NonOverlappingIntervalTree::new();

    let mut id = 0;
    for entry in &entries {
      let start = id;
      id += entry.sprites.len() as u32;
      sprite_entries.insert(start..id, (start, entry.clone()));
    }

    Self {
      scripts,
      sprite_entries,
    }
  }
}
