use crate::types::generator::{GenerateContext, GenerateOutput, Generator};

use anyhow::Result;
use rolldown_common::{CssAssetMeta, InstantiatedChunk, InstantiationKind};
use rolldown_error::BuildResult;
use rolldown_sourcemap::{SourceJoiner, SourceMapSource};
use string_wizard::SourceMapOptions;

pub struct CssGenerator;

impl Generator for CssGenerator {
  async fn instantiate_chunk(ctx: &mut GenerateContext<'_>) -> Result<BuildResult<GenerateOutput>> {
    let mut ordered_css_modules = ctx
      .chunk
      .modules
      .iter()
      .filter_map(|&id| ctx.link_output.module_table[id].as_normal())
      .filter(|m| m.css_view.is_some())
      .collect::<Vec<_>>();

    if ordered_css_modules.is_empty() {
      return Ok(Ok(GenerateOutput {
        chunks: vec![],
        warnings: std::mem::take(&mut ctx.warnings),
      }));
    }

    ordered_css_modules.sort_by_key(|m| m.exec_order);

    let mut source_joiner = SourceJoiner::default();

    let enable_sourcemap = ctx.options.sourcemap.is_some();
    source_joiner.enable_sourcemap = enable_sourcemap;

    for module in &ordered_css_modules {
      let css_view = module.css_view.as_ref().unwrap();
      let mut magic_string = string_wizard::MagicString::new(css_view.source.as_str());
      for mutation in &css_view.mutations {
        mutation.apply(&mut magic_string);
      }

      if enable_sourcemap {
        let content = magic_string.to_string();
        let mut sourcemap = magic_string.source_map(SourceMapOptions::default());
        sourcemap.set_sources(vec![&module.stable_id]);
        let source = SourceMapSource::new(content, sourcemap).with_pre_compute_sourcemap_data(true);
        source_joiner.append_source(source);
      } else {
        source_joiner.append_source(magic_string.to_string());
      }
    }
    // source_joiner.join() will emit a '\n' for each source except the last one
    // append an empty source here to ensure there is a '\n' after each real css source
    source_joiner.append_source("");

    let (content, map) = source_joiner.join();

    // Here file path is generated by chunk file name template, it maybe including path segments.
    // So here need to read it's parent directory as file_dir.
    let file_path = ctx.options.cwd.as_path().join(&ctx.options.out_dir).join(
      ctx
        .chunk
        .css_preliminary_filename
        .as_deref()
        .expect("chunk file name should be generated before rendering")
        .as_str(),
    );
    let file_dir = file_path.parent().expect("chunk file name should have a parent");

    let css_asset_meta = CssAssetMeta {
      filename: ctx
        .chunk
        .css_preliminary_filename
        .as_deref()
        .expect("should have preliminary_filename")
        .clone(),
      debug_id: 0,
      file_dir: file_dir.to_path_buf(),
      preliminary_filename: ctx
        .chunk
        .css_preliminary_filename
        .clone()
        .expect("should have preliminary filename"),
    };

    Ok(Ok(GenerateOutput {
      chunks: vec![InstantiatedChunk {
        originate_from: ctx.chunk_idx,
        content: content.into(),
        map,
        kind: InstantiationKind::from(css_asset_meta),
        augment_chunk_hash: None,
        preliminary_filename: ctx
          .chunk
          .css_preliminary_filename
          .clone()
          .expect("should have preliminary filename"),
      }],
      warnings: std::mem::take(&mut ctx.warnings),
    }))
  }
}
