// Copyright 2018-2026 the Deno authors. MIT license.

use std::cell::RefCell;
use std::collections::HashMap;
use std::collections::HashSet;
use std::rc::Rc;

use serde::Deserialize;
use serde::Serialize;

use super::RequestedModuleType;
use crate::ModuleCodeString;
use crate::ModuleSource;
use crate::fast_string::FastString;
use crate::modules::ModuleId;
use crate::modules::ModuleInfo;
use crate::modules::ModuleLoadId;
use crate::modules::ModuleName;
use crate::modules::ModuleReference;
use crate::modules::ModuleRequest;
use crate::modules::ModuleType;
use crate::runtime::SnapshotDataId;
use crate::runtime::SnapshotLoadDataStore;
use crate::runtime::SnapshotStoreDataStore;

/// A symbolic module entity.
#[derive(Debug, PartialEq, Serialize, Deserialize)]
pub(crate) enum SymbolicModule {
  /// This module is an alias to another module.
  /// This is useful such that multiple names could point to
  /// the same underlying module (particularly due to redirects).
  Alias(ModuleName),
  /// This module associates with a V8 module by id.
  Mod(ModuleId),
}

/// Map of [`ModuleName`] and [`RequestedModuleType`] to a data field.
struct ModuleNameTypeMap<T> {
  submaps: Vec<HashMap<ModuleName, T>>,
  map_index: HashMap<RequestedModuleType, usize>,
  len: usize,
}

impl<T> Default for ModuleNameTypeMap<T> {
  fn default() -> Self {
    Self {
      submaps: Default::default(),
      map_index: Default::default(),
      len: 0,
    }
  }
}

impl<T> ModuleNameTypeMap<T> {
  pub fn len(&self) -> usize {
    self.len
  }

  fn map_index(&self, ty: &RequestedModuleType) -> Option<usize> {
    self.map_index.get(ty).copied()
  }

  pub fn get<Q>(&self, ty: &RequestedModuleType, name: &Q) -> Option<&T>
  where
    ModuleName: std::borrow::Borrow<Q>,
    Q: std::cmp::Eq + std::hash::Hash + std::fmt::Debug + ?Sized,
  {
    let index = self.map_index(ty)?;
    let map = self.submaps.get(index)?;
    map.get(name)
  }

  pub fn insert(
    &mut self,
    module_type: &RequestedModuleType,
    name: FastString,
    module: T,
  ) {
    let index = match self.map_index(module_type) {
      Some(index) => index,
      None => {
        let index = self.submaps.len();
        self.map_index.insert(module_type.clone(), index);
        self.submaps.push(Default::default());
        index
      }
    };

    if self
      .submaps
      .get_mut(index)
      .unwrap()
      .insert(name, module)
      .is_none()
    {
      self.len += 1;
    }
  }

  /// Rather than providing an iterator, we provide a drain method. This is mainly because Rust
  /// doesn't have generators.
  pub fn drain(
    mut self,
    mut f: impl FnMut(usize, &RequestedModuleType, ModuleName, T),
  ) {
    let mut i = 0;
    for (ty, value) in self.map_index.into_iter() {
      for (key, value) in self.submaps.get_mut(value).unwrap().drain() {
        f(i, &ty, key, value);
        i += 1;
      }
    }
  }
}

/// An array of tuples that provide module exports.
///
/// "default" name will make the export "default" - ie. one that can be imported
/// with `import foo from "./virtual.js"`;.
/// All other name provide "named exports" - ie. ones that can be imported like
/// so: `import { name1, name2 } from "./virtual.js`.
pub(crate) type SyntheticModuleExports =
  Vec<(v8::Global<v8::String>, v8::Global<v8::Value>)>;

// TODO(bartlomieju): add an assertion that checks for that assumption?
// If it's true we can simplify the type to be an `Option` instead of a `HashMap`.
/// This hash map is not expected to hold more than one element at a time.
/// It is a temporary store, so we can forward data to
/// `synthetic_module_evaluation_steps` callback.
pub(crate) type SyntheticModuleExportsStore =
  HashMap<v8::Global<v8::Module>, SyntheticModuleExports>;

#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
pub(crate) enum ModuleSourceKind {
  Wasm,
}

impl ModuleSourceKind {
  pub fn from_module_type(module_type: &ModuleType) -> Option<Self> {
    match module_type {
      ModuleType::Wasm => Some(Self::Wasm),
      _ => None,
    }
  }
}

#[derive(Debug, Clone, Eq, PartialEq, Hash)]
enum ModuleSourceType {
  Loaded(ModuleType),
  Requested(RequestedModuleType),
}

#[derive(Debug, Eq, PartialEq, Hash)]
pub(crate) struct ModuleSourceKey {
  pub name: ModuleName,
  typ: ModuleSourceType,
}

impl ModuleSourceKey {
  pub fn from_reference(module_reference: &ModuleReference) -> Self {
    Self {
      name: module_reference.specifier.to_string().into(),
      typ: ModuleSourceType::Requested(
        module_reference.requested_module_type.clone(),
      ),
    }
  }

  pub fn from_loaded_source(loaded_source: &mut ModuleSource) -> Self {
    let name = if let Some(module_url_found) =
      loaded_source.cheap_copy_module_url_found()
    {
      module_url_found
    } else {
      loaded_source.cheap_copy_module_url_specified()
    };
    Self {
      name,
      typ: ModuleSourceType::Loaded(loaded_source.module_type.clone()),
    }
  }
}

#[derive(Default)]
pub(crate) struct ModuleMapData {
  /// Inverted index from module to index in `info`.
  pub(crate) handles_inverted: HashMap<v8::Global<v8::Module>, usize>,
  /// The handles we have loaded so far, corresponding with the [`ModuleInfo`] in `info`.
  pub(crate) handles: Vec<v8::Global<v8::Module>>,
  pub(crate) main_module_callbacks: Vec<v8::Global<v8::Function>>,
  /// The modules we have loaded so far.
  pub(crate) info: Vec<ModuleInfo>,
  /// [`ModuleName`] to [`SymbolicModule`] for modules.
  by_name: ModuleNameTypeMap<SymbolicModule>,
  /// The next ID used for a load.
  pub(crate) next_load_id: ModuleLoadId,
  /// If a main module has been loaded, points to it by index.
  pub(crate) main_module_id: Option<ModuleId>,
  /// This store is used to temporarily store data that is used
  /// to evaluate a "synthetic module".
  pub(crate) synthetic_module_exports_store: SyntheticModuleExportsStore,
  pub(crate) lazy_esm_sources:
    Rc<RefCell<HashMap<ModuleName, ModuleCodeString>>>,
  pub(crate) residual_lazy_esm_sources:
    &'static [(&'static str, &'static str)],
  /// Specifiers of lazy-loaded ESM modules known to exist (survives
  /// snapshotting). Used to check if a module should be loaded from
  /// `lazy_esm_sources` without going through the external module loader.
  pub(crate) known_lazy_esm: RefCell<HashSet<String>>,
  pub(crate) lazy_script_sources:
    Rc<RefCell<HashMap<ModuleName, ModuleCodeString>>>,
  pub(crate) residual_lazy_script_sources:
    &'static [(&'static str, &'static str)],
  /// Results of `load_ext_script` evaluations. Populated on first
  /// evaluation so later callers (`Deno.core.loadExtScript()` from JS,
  /// the `synthetic_esm` dispatch from Rust) share a single evaluated
  /// exports value — the source is removed from `lazy_script_sources`
  /// on first eval, so subsequent reads come from here. Runtime-only —
  /// not snapshotted.
  pub(crate) loaded_script_results:
    Rc<RefCell<HashMap<ModuleName, v8::Global<v8::Value>>>>,
  /// `synthetic_esm` registry: module specifier (e.g. `node:worker_threads`)
  /// to backing-script specifier (e.g. `ext:deno_node/worker_threads.ts`).
  /// Populated at extension init from each extension's
  /// `synthetic_esm_files` list. Runtime-only — not snapshotted.
  pub(crate) synthetic_esm_modules:
    Rc<RefCell<HashMap<ModuleName, ModuleName>>>,
  /// Set of scripts currently being loaded (for circular dep detection).
  pub(crate) lazy_script_loading: Rc<RefCell<HashSet<ModuleName>>>,
  /// Snapshot-time `__bootstrap` view (frozen clone of `core.ops` etc.)
  /// registered from JS via `op_set_captured_bootstrap`. `load_ext_script`
  /// temporarily installs this on `globalThis.__bootstrap` for the duration
  /// of each script evaluation if `__bootstrap` isn't already on the global
  /// — every lazy_loaded_js polyfill's IIFE preamble destructures it, and
  /// the `synthetic_esm` dispatch goes straight through Rust without the
  /// JS `core.loadExtScript` wrapper. Runtime-only — not snapshotted.
  pub(crate) captured_bootstrap: RefCell<Option<v8::Global<v8::Value>>>,
  pub(crate) sources: HashMap<ModuleSourceKey, v8::Global<v8::Object>>,
  /// Specifiers of `lazy_loaded_esm` / `lazy_loaded_js` files whose source
  /// was actually compiled by V8 during snapshot creation. Their bytes live
  /// in the snapshot blob; the binary does not need a separate copy.
  pub(crate) consumed_lazy_specifiers: RefCell<HashSet<String>>,
}

/// Snapshot-compatible representation of this data.
#[derive(Default, Serialize, Deserialize)]
pub(crate) struct ModuleMapSnapshotData {
  next_load_id: i32,
  main_module_id: Option<i32>,
  modules: Vec<ModuleInfo>,
  module_handles: Vec<SnapshotDataId>,
  main_module_callbacks: Vec<SnapshotDataId>,
  by_name: Vec<(FastString, RequestedModuleType, SymbolicModule)>,
  /// Specifiers of lazy-loaded ESM modules that are known to exist but
  /// are not compiled/instantiated in the snapshot. They will be loaded
  /// from the binary on first access at runtime.
  #[serde(default)]
  lazy_esm_specifiers: Vec<String>,
  /// `load_ext_script` cache snapshot. Captures the exports object
  /// returned by each polyfill IIFE evaluated at snapshot time so the
  /// runtime can share the same value (with the `synthetic_esm` dispatch
  /// in particular) without re-evaluating — re-eval would clobber
  /// registered `internals.__*` hooks and duplicate class identities.
  #[serde(default)]
  loaded_script_results: Vec<(FastString, SnapshotDataId)>,
  /// Snapshot of the captured `__bootstrap` view registered via
  /// `op_set_captured_bootstrap` at the end of `01_core.js`. Restored
  /// at runtime so the Rust `load_ext_script` can reinstall it on
  /// `globalThis.__bootstrap` during each script evaluation.
  #[serde(default)]
  captured_bootstrap: Option<SnapshotDataId>,
}

impl ModuleMapData {
  pub fn create_module_info(
    &mut self,
    name: FastString,
    module_type: ModuleType,
    handle: v8::Global<v8::Module>,
    main: bool,
    requests: Vec<ModuleRequest>,
  ) -> ModuleId {
    let data = self;
    let id = data.handles.len();
    let (name1, name2) = name.into_cheap_copy();
    data.handles_inverted.insert(handle.clone(), id);
    data.handles.push(handle);
    if main {
      data.main_module_id = Some(id);
    }
    // TODO(bartlomieju): verify if we can store `ModuleType` here instead
    let requested_module_type = RequestedModuleType::from(module_type.clone());
    data
      .by_name
      .insert(&requested_module_type, name1, SymbolicModule::Mod(id));
    data.info.push(ModuleInfo {
      id,
      main,
      name: name2,
      requests,
      module_type,
    });

    id
  }

  /// Get module id, following all aliases in case of module specifier
  /// that had been redirected.
  pub fn get_id(
    &self,
    name: &str,
    requested_module_type: impl AsRef<RequestedModuleType>,
  ) -> Option<ModuleId> {
    let map = &self.by_name;
    let first_symbolic_module =
      map.get(requested_module_type.as_ref(), name)?;
    let mut mod_name = match first_symbolic_module {
      SymbolicModule::Mod(mod_id) => return Some(*mod_id),
      SymbolicModule::Alias(target) => target,
    };
    loop {
      let symbolic_module =
        map.get(requested_module_type.as_ref(), mod_name)?;
      match symbolic_module {
        SymbolicModule::Alias(target) => {
          debug_assert!(mod_name != target);
          mod_name = target;
        }
        SymbolicModule::Mod(mod_id) => return Some(*mod_id),
      }
    }
  }

  pub(crate) fn alias(
    &mut self,
    name: FastString,
    requested_module_type: &RequestedModuleType,
    target: FastString,
  ) {
    debug_assert_ne!(name, target);
    self.by_name.insert(
      requested_module_type,
      name,
      SymbolicModule::Alias(target),
    );
  }

  /// Register an additional `(name, requested_module_type) -> module_id`
  /// mapping for a module that was already registered under a different
  /// requested module type. Used to make modules visible to imports whose
  /// `with { type: ... }` attribute doesn't match the loaded module's
  /// actual type — for example when a `module.registerHooks()` load hook
  /// returns `format: "module"` for an import with a custom type
  /// attribute like `with { type: "x-css" }`.
  pub(crate) fn register_under_type(
    &mut self,
    name: FastString,
    requested_module_type: &RequestedModuleType,
    module_id: ModuleId,
  ) {
    self.by_name.insert(
      requested_module_type,
      name,
      SymbolicModule::Mod(module_id),
    );
  }

  #[cfg(test)]
  pub(crate) fn is_alias(
    &self,
    name: &str,
    requested_module_type: impl AsRef<RequestedModuleType>,
  ) -> bool {
    let map = &self.by_name;
    let entry = map.get(requested_module_type.as_ref(), name);
    matches!(entry, Some(SymbolicModule::Alias(_)))
  }

  pub(crate) fn get_handle(
    &self,
    id: ModuleId,
  ) -> Option<v8::Global<v8::Module>> {
    self.handles.get(id).cloned()
  }

  pub(crate) fn get_name_by_module(
    &self,
    global: &v8::Global<v8::Module>,
  ) -> Option<String> {
    match self.handles_inverted.get(global) {
      Some(id) => self.get_name_by_id(*id),
      _ => None,
    }
  }

  pub(crate) fn get_type_by_module(
    &self,
    global: &v8::Global<v8::Module>,
  ) -> Option<ModuleType> {
    match self.handles_inverted.get(global) {
      Some(id) => {
        let info = self.info.get(*id).unwrap();
        Some(info.module_type.clone())
      }
      _ => None,
    }
  }

  pub(crate) fn get_info_by_module(
    &self,
    global: &v8::Global<v8::Module>,
  ) -> Option<&ModuleInfo> {
    match self.handles_inverted.get(global) {
      Some(id) => Some(self.info.get(*id).unwrap()),
      _ => None,
    }
  }

  pub(crate) fn is_main_module(&self, global: &v8::Global<v8::Module>) -> bool {
    self
      .main_module_id
      .map(|id| self.handles_inverted.get(global) == Some(&id))
      .unwrap_or_default()
  }

  pub(crate) fn get_name_by_id(&self, id: ModuleId) -> Option<String> {
    // TODO(mmastrac): Don't clone
    self.info.get(id).map(|info| info.name.as_str().to_owned())
  }

  pub fn serialize_for_snapshotting(
    self,
    data_store: &mut SnapshotStoreDataStore,
  ) -> ModuleMapSnapshotData {
    debug_assert_eq!(self.by_name.len(), self.handles.len());
    debug_assert_eq!(self.info.len(), self.handles.len());

    let mut ser = ModuleMapSnapshotData {
      next_load_id: self.next_load_id,
      main_module_id: self.main_module_id.map(|x| x as _),
      modules: self.info,
      ..Default::default()
    };

    ser.main_module_callbacks = self
      .main_module_callbacks
      .into_iter()
      .map(|x| data_store.register(x))
      .collect();
    ser.module_handles = self
      .handles
      .into_iter()
      .map(|v| data_store.register(v))
      .collect();

    self.by_name.drain(|_, module_type, name, module| {
      ser.by_name.push((name, module_type.clone(), module));
    });

    ser.lazy_esm_specifiers = self
      .known_lazy_esm
      .borrow()
      .iter()
      .map(|s| s.to_string())
      .collect();

    // Move out of the Rc<RefCell<...>> so we can consume the values.
    let cached_results: HashMap<ModuleName, v8::Global<v8::Value>> =
      std::mem::take(&mut *self.loaded_script_results.borrow_mut());
    ser.loaded_script_results = cached_results
      .into_iter()
      .map(|(name, value)| (name, data_store.register(value)))
      .collect();

    ser.captured_bootstrap = self
      .captured_bootstrap
      .into_inner()
      .map(|value| data_store.register(value));

    ser
  }

  pub fn update_with_snapshotted_data(
    &mut self,
    scope: &mut v8::PinScope,
    data_store: &mut SnapshotLoadDataStore,
    data: ModuleMapSnapshotData,
  ) {
    self.next_load_id = data.next_load_id;
    self.main_module_id = data.main_module_id.map(|x| x as _);
    self.info = data.modules;
    self.handles.reserve(data.module_handles.len());
    self.handles_inverted.reserve(data.module_handles.len());
    self.main_module_callbacks = data
      .main_module_callbacks
      .into_iter()
      .map(|x| data_store.get(scope, x))
      .collect();

    for module_handle in data.module_handles {
      let id = self.handles.len();
      let module = data_store.get::<v8::Module>(scope, module_handle);
      self.handles_inverted.insert(module.clone(), id as _);
      self.handles.push(module);
    }

    for (name, module_type, module) in data.by_name {
      self.by_name.insert(&module_type, name, module)
    }

    *self.known_lazy_esm.borrow_mut() =
      data.lazy_esm_specifiers.into_iter().collect();

    let mut cached_results = self.loaded_script_results.borrow_mut();
    for (name, id) in data.loaded_script_results {
      let value = data_store.get::<v8::Value>(scope, id);
      cached_results.insert(name, value);
    }
    drop(cached_results);

    if let Some(id) = data.captured_bootstrap {
      let value = data_store.get::<v8::Value>(scope, id);
      *self.captured_bootstrap.borrow_mut() = Some(value);
    }
  }

  // TODO(mmastrac): this is better than giving the entire crate access to the internals.
  #[cfg(test)]
  pub fn assert_module_map(&self, modules: &Vec<ModuleInfo>) {
    use crate::runtime::NO_OF_BUILTIN_MODULES;
    let data = self;
    assert_eq!(data.handles.len(), modules.len() + NO_OF_BUILTIN_MODULES);
    assert_eq!(data.info.len(), modules.len() + NO_OF_BUILTIN_MODULES);
    assert_eq!(data.next_load_id as usize, modules.len());
    assert_eq!(data.by_name.len(), modules.len() + NO_OF_BUILTIN_MODULES);

    for info in modules {
      assert!(data.handles.get(info.id).is_some());
      assert_eq!(data.info.get(info.id).unwrap(), info);
      let requested_module_type =
        RequestedModuleType::from(info.module_type.clone());
      assert_eq!(
        data.by_name.get(&requested_module_type, &info.name),
        Some(&SymbolicModule::Mod(info.id))
      );
    }
  }
}

#[cfg(test)]
mod tests {
  use url::Url;

  use super::*;
  use crate::ascii_str;

  #[test]
  fn module_name_map_test() {
    let mut data: ModuleNameTypeMap<usize> = ModuleNameTypeMap::default();
    data.insert(
      &RequestedModuleType::Json,
      ascii_str!("http://example.com/").into(),
      1,
    );
    assert_eq!(
      Some(&1),
      data.get(
        &RequestedModuleType::Json,
        Url::parse("http://example.com/").unwrap().as_str()
      )
    );
  }
}
