#[cfg(feature = "bundler")]
use at_rule_parser::AtRule;
use at_rule_parser::{CustomAtRuleConfig, CustomAtRuleParser};
use lightningcss::bundler::BundleErrorKind;
#[cfg(feature = "bundler")]
use lightningcss::bundler::{Bundler, SourceProvider};
use lightningcss::css_modules::{CssModuleExports, CssModuleReferences, PatternParseError};
use lightningcss::dependencies::{Dependency, DependencyOptions};
use lightningcss::error::{Error, ErrorLocation, MinifyErrorKind, ParserError, PrinterErrorKind};
use lightningcss::stylesheet::{
  MinifyOptions, ParserFlags, ParserOptions, PrinterOptions, PseudoClasses, StyleAttribute, StyleSheet,
};
use lightningcss::targets::{Browsers, Features, Targets};
use napi::bindgen_prelude::{FromNapiValue, ToNapiValue};
use napi::{CallContext, Env, JsObject, JsUnknown};
use parcel_sourcemap::SourceMap;
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
use std::sync::{Arc, RwLock};

mod at_rule_parser;
#[cfg(feature = "bundler")]
#[cfg(not(target_arch = "wasm32"))]
mod threadsafe_function;
#[cfg(feature = "visitor")]
mod transformer;
mod utils;

#[cfg(feature = "visitor")]
use transformer::JsVisitor;

#[cfg(not(feature = "visitor"))]
struct JsVisitor;

#[cfg(feature = "visitor")]
use lightningcss::visitor::Visit;

use utils::get_named_property;

#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct TransformResult<'i> {
  #[serde(with = "serde_bytes")]
  code: Vec<u8>,
  #[serde(with = "serde_bytes")]
  map: Option<Vec<u8>>,
  exports: Option<CssModuleExports>,
  references: Option<CssModuleReferences>,
  dependencies: Option<Vec<Dependency>>,
  warnings: Vec<Warning<'i>>,
}

impl<'i> TransformResult<'i> {
  fn into_js(self, env: Env) -> napi::Result<JsUnknown> {
    // Manually construct buffers so we avoid a copy and work around
    // https://github.com/napi-rs/napi-rs/issues/1124.
    let mut obj = env.create_object()?;
    let buf = env.create_buffer_with_data(self.code)?;
    obj.set_named_property("code", buf.into_raw())?;
    obj.set_named_property(
      "map",
      if let Some(map) = self.map {
        let buf = env.create_buffer_with_data(map)?;
        buf.into_raw().into_unknown()
      } else {
        env.get_null()?.into_unknown()
      },
    )?;
    obj.set_named_property("exports", env.to_js_value(&self.exports)?)?;
    obj.set_named_property("references", env.to_js_value(&self.references)?)?;
    obj.set_named_property("dependencies", env.to_js_value(&self.dependencies)?)?;
    obj.set_named_property("warnings", env.to_js_value(&self.warnings)?)?;
    Ok(obj.into_unknown())
  }
}

#[cfg(feature = "visitor")]
fn get_visitor(env: Env, opts: &JsObject) -> Option<JsVisitor> {
  if let Ok(visitor) = get_named_property::<JsObject>(opts, "visitor") {
    Some(JsVisitor::new(env, visitor))
  } else {
    None
  }
}

#[cfg(not(feature = "visitor"))]
fn get_visitor(_env: Env, _opts: &JsObject) -> Option<JsVisitor> {
  None
}

pub fn transform(ctx: CallContext) -> napi::Result<JsUnknown> {
  let opts = ctx.get::<JsObject>(0)?;
  let mut visitor = get_visitor(*ctx.env, &opts);

  let config: Config = ctx.env.from_js_value(opts)?;
  let code = unsafe { std::str::from_utf8_unchecked(&config.code) };
  let res = compile(code, &config, &mut visitor);

  match res {
    Ok(res) => res.into_js(*ctx.env),
    Err(err) => Err(err.into_js_error(*ctx.env, Some(code))?),
  }
}

pub fn transform_style_attribute(ctx: CallContext) -> napi::Result<JsUnknown> {
  let opts = ctx.get::<JsObject>(0)?;
  let mut visitor = get_visitor(*ctx.env, &opts);

  let config: AttrConfig = ctx.env.from_js_value(opts)?;
  let code = unsafe { std::str::from_utf8_unchecked(&config.code) };
  let res = compile_attr(code, &config, &mut visitor);

  match res {
    Ok(res) => res.into_js(ctx),
    Err(err) => Err(err.into_js_error(*ctx.env, Some(code))?),
  }
}

#[cfg(feature = "bundler")]
#[cfg(not(target_arch = "wasm32"))]
mod bundle {
  use super::*;
  use crossbeam_channel::{self, Receiver, Sender};
  use lightningcss::bundler::FileProvider;
  use napi::{Env, JsFunction, JsString, NapiRaw};
  use std::path::{Path, PathBuf};
  use std::str::FromStr;
  use std::sync::Mutex;
  use threadsafe_function::{ThreadSafeCallContext, ThreadsafeFunction, ThreadsafeFunctionCallMode};

  pub fn bundle(ctx: CallContext) -> napi::Result<JsUnknown> {
    let opts = ctx.get::<JsObject>(0)?;
    let mut visitor = get_visitor(*ctx.env, &opts);

    let config: BundleConfig = ctx.env.from_js_value(opts)?;
    let fs = FileProvider::new();

    // This is pretty silly, but works around a rust limitation that you cannot
    // explicitly annotate lifetime bounds on closures.
    fn annotate<'i, 'o, F>(f: F) -> F
    where
      F: FnOnce(&mut StyleSheet<'i, 'o, AtRule<'i>>) -> napi::Result<()>,
    {
      f
    }

    let res = compile_bundle(
      &fs,
      &config,
      visitor.as_mut().map(|visitor| annotate(|stylesheet| stylesheet.visit(visitor))),
    );

    match res {
      Ok(res) => res.into_js(*ctx.env),
      Err(err) => Err(err.into_js_error(*ctx.env, None)?),
    }
  }

  // A SourceProvider which calls JavaScript functions to resolve and read files.
  struct JsSourceProvider {
    resolve: Option<ThreadsafeFunction<ResolveMessage>>,
    read: Option<ThreadsafeFunction<ReadMessage>>,
    inputs: Mutex<Vec<*mut String>>,
  }

  unsafe impl Sync for JsSourceProvider {}
  unsafe impl Send for JsSourceProvider {}

  // Allocate a single channel per thread to communicate with the JS thread.
  thread_local! {
    static CHANNEL: (Sender<napi::Result<String>>, Receiver<napi::Result<String>>) = crossbeam_channel::unbounded();
  }

  impl SourceProvider for JsSourceProvider {
    type Error = napi::Error;

    fn read<'a>(&'a self, file: &Path) -> Result<&'a str, Self::Error> {
      let source = if let Some(read) = &self.read {
        CHANNEL.with(|channel| {
          let message = ReadMessage {
            file: file.to_str().unwrap().to_owned(),
            tx: channel.0.clone(),
          };

          read.call(message, ThreadsafeFunctionCallMode::Blocking);
          channel.1.recv().unwrap()
        })
      } else {
        Ok(std::fs::read_to_string(file)?)
      };

      match source {
        Ok(source) => {
          // cache the result
          let ptr = Box::into_raw(Box::new(source));
          self.inputs.lock().unwrap().push(ptr);
          // SAFETY: this is safe because the pointer is not dropped
          // until the JsSourceProvider is, and we never remove from the
          // list of pointers stored in the vector.
          Ok(unsafe { &*ptr })
        }
        Err(e) => Err(e),
      }
    }

    fn resolve(&self, specifier: &str, originating_file: &Path) -> Result<PathBuf, Self::Error> {
      if let Some(resolve) = &self.resolve {
        return CHANNEL.with(|channel| {
          let message = ResolveMessage {
            specifier: specifier.to_owned(),
            originating_file: originating_file.to_str().unwrap().to_owned(),
            tx: channel.0.clone(),
          };

          resolve.call(message, ThreadsafeFunctionCallMode::Blocking);
          let result = channel.1.recv().unwrap();
          match result {
            Ok(result) => Ok(PathBuf::from_str(&result).unwrap()),
            Err(e) => Err(e),
          }
        });
      }

      Ok(originating_file.with_file_name(specifier))
    }
  }

  struct ResolveMessage {
    specifier: String,
    originating_file: String,
    tx: Sender<napi::Result<String>>,
  }

  struct ReadMessage {
    file: String,
    tx: Sender<napi::Result<String>>,
  }

  struct VisitMessage {
    stylesheet: &'static mut StyleSheet<'static, 'static, AtRule<'static>>,
    tx: Sender<napi::Result<String>>,
  }

  fn await_promise(env: Env, result: JsUnknown, tx: Sender<napi::Result<String>>) -> napi::Result<()> {
    // If the result is a promise, wait for it to resolve, and send the result to the channel.
    // Otherwise, send the result immediately.
    if result.is_promise()? {
      let result: JsObject = result.try_into()?;
      let then: JsFunction = get_named_property(&result, "then")?;
      let tx2 = tx.clone();
      let cb = env.create_function_from_closure("callback", move |ctx| {
        let res = ctx.get::<JsString>(0)?.into_utf8()?;
        let s = res.into_owned()?;
        tx.send(Ok(s)).unwrap();
        ctx.env.get_undefined()
      })?;
      let eb = env.create_function_from_closure("error_callback", move |ctx| {
        let res = ctx.get::<JsUnknown>(0)?;
        tx2.send(Err(napi::Error::from(res))).unwrap();
        ctx.env.get_undefined()
      })?;
      then.call(Some(&result), &[cb, eb])?;
    } else {
      let result: JsString = result.try_into()?;
      let utf8 = result.into_utf8()?;
      let s = utf8.into_owned()?;
      tx.send(Ok(s)).unwrap();
    }

    Ok(())
  }

  fn resolve_on_js_thread(ctx: ThreadSafeCallContext<ResolveMessage>) -> napi::Result<()> {
    let specifier = ctx.env.create_string(&ctx.value.specifier)?;
    let originating_file = ctx.env.create_string(&ctx.value.originating_file)?;
    let result = ctx.callback.unwrap().call(None, &[specifier, originating_file])?;
    await_promise(ctx.env, result, ctx.value.tx)
  }

  fn handle_error(tx: Sender<napi::Result<String>>, res: napi::Result<()>) -> napi::Result<()> {
    match res {
      Ok(_) => Ok(()),
      Err(e) => {
        tx.send(Err(e)).expect("send error");
        Ok(())
      }
    }
  }

  fn resolve_on_js_thread_wrapper(ctx: ThreadSafeCallContext<ResolveMessage>) -> napi::Result<()> {
    let tx = ctx.value.tx.clone();
    handle_error(tx, resolve_on_js_thread(ctx))
  }

  fn read_on_js_thread(ctx: ThreadSafeCallContext<ReadMessage>) -> napi::Result<()> {
    let file = ctx.env.create_string(&ctx.value.file)?;
    let result = ctx.callback.unwrap().call(None, &[file])?;
    await_promise(ctx.env, result, ctx.value.tx)
  }

  fn read_on_js_thread_wrapper(ctx: ThreadSafeCallContext<ReadMessage>) -> napi::Result<()> {
    let tx = ctx.value.tx.clone();
    handle_error(tx, read_on_js_thread(ctx))
  }

  pub fn bundle_async(ctx: CallContext) -> napi::Result<JsObject> {
    let opts = ctx.get::<JsObject>(0)?;
    let visitor = get_visitor(*ctx.env, &opts);

    let config: BundleConfig = ctx.env.from_js_value(&opts)?;

    if let Ok(resolver) = get_named_property::<JsObject>(&opts, "resolver") {
      let read = if resolver.has_named_property("read")? {
        let read = get_named_property::<JsFunction>(&resolver, "read")?;
        Some(ThreadsafeFunction::create(
          ctx.env.raw(),
          unsafe { read.raw() },
          0,
          read_on_js_thread_wrapper,
        )?)
      } else {
        None
      };

      let resolve = if resolver.has_named_property("resolve")? {
        let resolve = get_named_property::<JsFunction>(&resolver, "resolve")?;
        Some(ThreadsafeFunction::create(
          ctx.env.raw(),
          unsafe { resolve.raw() },
          0,
          resolve_on_js_thread_wrapper,
        )?)
      } else {
        None
      };

      let provider = JsSourceProvider {
        resolve,
        read,
        inputs: Mutex::new(Vec::new()),
      };

      run_bundle_task(provider, config, visitor, *ctx.env)
    } else {
      let provider = FileProvider::new();
      run_bundle_task(provider, config, visitor, *ctx.env)
    }
  }

  // Runs bundling on a background thread managed by rayon. This is similar to AsyncTask from napi-rs, however,
  // because we call back into the JS thread, which might call other tasks in the node threadpool (e.g. fs.readFile),
  // we may end up deadlocking if the number of rayon threads exceeds node's threadpool size. Therefore, we must
  // run bundling from a thread not managed by Node.
  fn run_bundle_task<P: 'static + SourceProvider>(
    provider: P,
    config: BundleConfig,
    visitor: Option<JsVisitor>,
    env: Env,
  ) -> napi::Result<JsObject>
  where
    P::Error: IntoJsError,
  {
    let (deferred, promise) = env.create_deferred()?;

    let tsfn = if let Some(mut visitor) = visitor {
      Some(ThreadsafeFunction::create(
        env.raw(),
        std::ptr::null_mut(),
        0,
        move |ctx: ThreadSafeCallContext<VisitMessage>| {
          if let Err(err) = ctx.value.stylesheet.visit(&mut visitor) {
            ctx.value.tx.send(Err(err)).expect("send error");
            return Ok(());
          }
          ctx.value.tx.send(Ok(Default::default())).expect("send error");
          Ok(())
        },
      )?)
    } else {
      None
    };

    // Run bundling task in rayon threadpool.
    rayon::spawn(move || {
      let res = compile_bundle(
        unsafe { std::mem::transmute::<&'_ P, &'static P>(&provider) },
        &config,
        tsfn.map(move |tsfn| {
          move |stylesheet: &mut StyleSheet<AtRule>| {
            CHANNEL.with(|channel| {
              let message = VisitMessage {
                // SAFETY: we immediately lock the thread until we get a response,
                // so stylesheet cannot be dropped in that time.
                stylesheet: unsafe {
                  std::mem::transmute::<
                    &'_ mut StyleSheet<'_, '_, AtRule>,
                    &'static mut StyleSheet<'static, 'static, AtRule>,
                  >(stylesheet)
                },
                tx: channel.0.clone(),
              };

              tsfn.call(message, ThreadsafeFunctionCallMode::Blocking);
              channel.1.recv().expect("recv error").map(|_| ())
            })
          }
        }),
      );

      deferred.resolve(move |env| match res {
        Ok(v) => v.into_js(env),
        Err(err) => Err(err.into_js_error(env, None)?),
      });
    });

    Ok(promise)
  }
}

#[cfg(feature = "bundler")]
#[cfg(target_arch = "wasm32")]
mod bundle {
  use super::*;
  use napi::{Env, JsFunction, JsString, NapiRaw, NapiValue, Ref};
  use std::cell::UnsafeCell;
  use std::path::{Path, PathBuf};
  use std::str::FromStr;

  pub fn bundle(ctx: CallContext) -> napi::Result<JsUnknown> {
    let opts = ctx.get::<JsObject>(0)?;
    let mut visitor = get_visitor(*ctx.env, &opts);

    let resolver = get_named_property::<JsObject>(&opts, "resolver")?;
    let read = get_named_property::<JsFunction>(&resolver, "read")?;
    let resolve = if resolver.has_named_property("resolve")? {
      let resolve = get_named_property::<JsFunction>(&resolver, "resolve")?;
      Some(ctx.env.create_reference(resolve)?)
    } else {
      None
    };
    let config: BundleConfig = ctx.env.from_js_value(opts)?;

    let provider = JsSourceProvider {
      env: ctx.env.clone(),
      resolve,
      read: ctx.env.create_reference(read)?,
      inputs: UnsafeCell::new(Vec::new()),
    };

    // This is pretty silly, but works around a rust limitation that you cannot
    // explicitly annotate lifetime bounds on closures.
    fn annotate<'i, 'o, F>(f: F) -> F
    where
      F: FnOnce(&mut StyleSheet<'i, 'o, AtRule<'i>>) -> napi::Result<()>,
    {
      f
    }

    let res = compile_bundle(
      &provider,
      &config,
      visitor.as_mut().map(|visitor| annotate(|stylesheet| stylesheet.visit(visitor))),
    );

    match res {
      Ok(res) => res.into_js(*ctx.env),
      Err(err) => Err(err.into_js_error(*ctx.env, None)?),
    }
  }

  struct JsSourceProvider {
    env: Env,
    resolve: Option<Ref<()>>,
    read: Ref<()>,
    inputs: UnsafeCell<Vec<*mut String>>,
  }

  impl Drop for JsSourceProvider {
    fn drop(&mut self) {
      if let Some(resolve) = &mut self.resolve {
        drop(resolve.unref(self.env));
      }
      drop(self.read.unref(self.env));
    }
  }

  unsafe impl Sync for JsSourceProvider {}
  unsafe impl Send for JsSourceProvider {}

  // This relies on Binaryen's Asyncify transform to allow Rust to call async JS functions from sync code.
  // See the comments in async.mjs for more details about how this works.
  extern "C" {
    fn await_promise_sync(
      promise: napi::sys::napi_value,
      result: *mut napi::sys::napi_value,
      error: *mut napi::sys::napi_value,
    );
  }

  fn get_result(env: Env, mut value: JsUnknown) -> napi::Result<JsString> {
    if value.is_promise()? {
      let mut result = std::ptr::null_mut();
      let mut error = std::ptr::null_mut();
      unsafe { await_promise_sync(value.raw(), &mut result, &mut error) };
      if !error.is_null() {
        let error = unsafe { JsUnknown::from_raw(env.raw(), error)? };
        return Err(napi::Error::from(error));
      }
      if result.is_null() {
        return Err(napi::Error::new(napi::Status::GenericFailure, "No result".to_string()));
      }

      value = unsafe { JsUnknown::from_raw(env.raw(), result)? };
    }

    value.try_into()
  }

  impl SourceProvider for JsSourceProvider {
    type Error = napi::Error;

    fn read<'a>(&'a self, file: &Path) -> Result<&'a str, Self::Error> {
      let read: JsFunction = self.env.get_reference_value_unchecked(&self.read)?;
      let file = self.env.create_string(file.to_str().unwrap())?;
      let source: JsUnknown = read.call(None, &[file])?;
      let source = get_result(self.env, source)?.into_utf8()?.into_owned()?;

      // cache the result
      let ptr = Box::into_raw(Box::new(source));
      let inputs = unsafe { &mut *self.inputs.get() };
      inputs.push(ptr);
      // SAFETY: this is safe because the pointer is not dropped
      // until the JsSourceProvider is, and we never remove from the
      // list of pointers stored in the vector.
      Ok(unsafe { &*ptr })
    }

    fn resolve(&self, specifier: &str, originating_file: &Path) -> Result<PathBuf, Self::Error> {
      if let Some(resolve) = &self.resolve {
        let resolve: JsFunction = self.env.get_reference_value_unchecked(resolve)?;
        let specifier = self.env.create_string(specifier)?;
        let originating_file = self.env.create_string(originating_file.to_str().unwrap())?;
        let result: JsUnknown = resolve.call(None, &[specifier, originating_file])?;
        let result = get_result(self.env, result)?.into_utf8()?;
        Ok(PathBuf::from_str(result.as_str()?).unwrap())
      } else {
        Ok(originating_file.with_file_name(specifier))
      }
    }
  }
}

#[cfg(feature = "bundler")]
pub use bundle::*;

// ---------------------------------------------

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct Config {
  pub filename: Option<String>,
  pub project_root: Option<String>,
  #[serde(with = "serde_bytes")]
  pub code: Vec<u8>,
  pub targets: Option<Browsers>,
  #[serde(default)]
  pub include: u32,
  #[serde(default)]
  pub exclude: u32,
  pub minify: Option<bool>,
  pub source_map: Option<bool>,
  pub input_source_map: Option<String>,
  pub drafts: Option<Drafts>,
  pub non_standard: Option<NonStandard>,
  pub css_modules: Option<CssModulesOption>,
  pub analyze_dependencies: Option<AnalyzeDependenciesOption>,
  pub pseudo_classes: Option<OwnedPseudoClasses>,
  pub unused_symbols: Option<HashSet<String>>,
  pub error_recovery: Option<bool>,
  pub custom_at_rules: Option<HashMap<String, CustomAtRuleConfig>>,
}

#[derive(Debug, Deserialize)]
#[serde(untagged)]
enum AnalyzeDependenciesOption {
  Bool(bool),
  Config(AnalyzeDependenciesConfig),
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct AnalyzeDependenciesConfig {
  preserve_imports: bool,
}

#[derive(Debug, Deserialize)]
#[serde(untagged)]
enum CssModulesOption {
  Bool(bool),
  Config(CssModulesConfig),
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct CssModulesConfig {
  pattern: Option<String>,
  dashed_idents: Option<bool>,
  animation: Option<bool>,
  container: Option<bool>,
  grid: Option<bool>,
  custom_idents: Option<bool>,
  pure: Option<bool>,
}

#[cfg(feature = "bundler")]
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct BundleConfig {
  pub filename: String,
  pub project_root: Option<String>,
  pub targets: Option<Browsers>,
  #[serde(default)]
  pub include: u32,
  #[serde(default)]
  pub exclude: u32,
  pub minify: Option<bool>,
  pub source_map: Option<bool>,
  pub drafts: Option<Drafts>,
  pub non_standard: Option<NonStandard>,
  pub css_modules: Option<CssModulesOption>,
  pub analyze_dependencies: Option<AnalyzeDependenciesOption>,
  pub pseudo_classes: Option<OwnedPseudoClasses>,
  pub unused_symbols: Option<HashSet<String>>,
  pub error_recovery: Option<bool>,
  pub custom_at_rules: Option<HashMap<String, CustomAtRuleConfig>>,
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct OwnedPseudoClasses {
  pub hover: Option<String>,
  pub active: Option<String>,
  pub focus: Option<String>,
  pub focus_visible: Option<String>,
  pub focus_within: Option<String>,
}

impl<'a> Into<PseudoClasses<'a>> for &'a OwnedPseudoClasses {
  fn into(self) -> PseudoClasses<'a> {
    PseudoClasses {
      hover: self.hover.as_deref(),
      active: self.active.as_deref(),
      focus: self.focus.as_deref(),
      focus_visible: self.focus_visible.as_deref(),
      focus_within: self.focus_within.as_deref(),
    }
  }
}

#[derive(Serialize, Debug, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
struct Drafts {
  #[serde(default)]
  custom_media: bool,
}

#[derive(Serialize, Debug, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
struct NonStandard {
  #[serde(default)]
  deep_selector_combinator: bool,
}

fn compile<'i>(
  code: &'i str,
  config: &Config,
  #[allow(unused_variables)] visitor: &mut Option<JsVisitor>,
) -> Result<TransformResult<'i>, CompileError<'i, napi::Error>> {
  let drafts = config.drafts.as_ref();
  let non_standard = config.non_standard.as_ref();
  let warnings = Some(Arc::new(RwLock::new(Vec::new())));

  let filename = config.filename.clone().unwrap_or_default();
  let project_root = config.project_root.as_ref().map(|p| p.as_ref());
  let mut source_map = if config.source_map.unwrap_or_default() {
    let mut sm = SourceMap::new(project_root.unwrap_or("/"));
    sm.add_source(&filename);
    sm.set_source_content(0, code)?;
    Some(sm)
  } else {
    None
  };

  let res = {
    let mut flags = ParserFlags::empty();
    flags.set(ParserFlags::CUSTOM_MEDIA, matches!(drafts, Some(d) if d.custom_media));
    flags.set(
      ParserFlags::DEEP_SELECTOR_COMBINATOR,
      matches!(non_standard, Some(v) if v.deep_selector_combinator),
    );

    let mut stylesheet = StyleSheet::parse_with(
      &code,
      ParserOptions {
        filename: filename.clone(),
        flags,
        css_modules: if let Some(css_modules) = &config.css_modules {
          match css_modules {
            CssModulesOption::Bool(true) => Some(lightningcss::css_modules::Config::default()),
            CssModulesOption::Bool(false) => None,
            CssModulesOption::Config(c) => Some(lightningcss::css_modules::Config {
              pattern: if let Some(pattern) = c.pattern.as_ref() {
                match lightningcss::css_modules::Pattern::parse(pattern) {
                  Ok(p) => p,
                  Err(e) => return Err(CompileError::PatternError(e)),
                }
              } else {
                Default::default()
              },
              dashed_idents: c.dashed_idents.unwrap_or_default(),
              animation: c.animation.unwrap_or(true),
              container: c.container.unwrap_or(true),
              grid: c.grid.unwrap_or(true),
              custom_idents: c.custom_idents.unwrap_or(true),
              pure: c.pure.unwrap_or_default(),
            }),
          }
        } else {
          None
        },
        source_index: 0,
        error_recovery: config.error_recovery.unwrap_or_default(),
        warnings: warnings.clone(),
      },
      &mut CustomAtRuleParser {
        configs: config.custom_at_rules.clone().unwrap_or_default(),
      },
    )?;

    #[cfg(feature = "visitor")]
    if let Some(visitor) = visitor.as_mut() {
      stylesheet.visit(visitor).map_err(CompileError::JsError)?;
    }

    let targets = Targets {
      browsers: config.targets,
      include: Features::from_bits_truncate(config.include),
      exclude: Features::from_bits_truncate(config.exclude),
    };

    stylesheet.minify(MinifyOptions {
      targets,
      unused_symbols: config.unused_symbols.clone().unwrap_or_default(),
    })?;

    stylesheet.to_css(PrinterOptions {
      minify: config.minify.unwrap_or_default(),
      source_map: source_map.as_mut(),
      project_root,
      targets,
      analyze_dependencies: if let Some(d) = &config.analyze_dependencies {
        match d {
          AnalyzeDependenciesOption::Bool(b) if *b => Some(DependencyOptions { remove_imports: true }),
          AnalyzeDependenciesOption::Config(c) => Some(DependencyOptions {
            remove_imports: !c.preserve_imports,
          }),
          _ => None,
        }
      } else {
        None
      },
      pseudo_classes: config.pseudo_classes.as_ref().map(|p| p.into()),
    })?
  };

  let map = if let Some(mut source_map) = source_map {
    if let Some(input_source_map) = &config.input_source_map {
      if let Ok(mut sm) = SourceMap::from_json("/", input_source_map) {
        let _ = source_map.extends(&mut sm);
      }
    }

    source_map.to_json(None).ok()
  } else {
    None
  };

  Ok(TransformResult {
    code: res.code.into_bytes(),
    map: map.map(|m| m.into_bytes()),
    exports: res.exports,
    references: res.references,
    dependencies: res.dependencies,
    warnings: warnings.map_or(Vec::new(), |w| {
      Arc::try_unwrap(w)
        .unwrap()
        .into_inner()
        .unwrap()
        .into_iter()
        .map(|w| w.into())
        .collect()
    }),
  })
}

#[cfg(feature = "bundler")]
fn compile_bundle<
  'i,
  'o,
  P: SourceProvider,
  F: FnOnce(&mut StyleSheet<'i, 'o, AtRule<'i>>) -> napi::Result<()>,
>(
  fs: &'i P,
  config: &'o BundleConfig,
  visit: Option<F>,
) -> Result<TransformResult<'i>, CompileError<'i, P::Error>> {
  use std::path::Path;

  let project_root = config.project_root.as_ref().map(|p| p.as_ref());
  let mut source_map = if config.source_map.unwrap_or_default() {
    Some(SourceMap::new(project_root.unwrap_or("/")))
  } else {
    None
  };
  let warnings = Some(Arc::new(RwLock::new(Vec::new())));

  let res = {
    let drafts = config.drafts.as_ref();
    let non_standard = config.non_standard.as_ref();
    let mut flags = ParserFlags::empty();
    flags.set(ParserFlags::CUSTOM_MEDIA, matches!(drafts, Some(d) if d.custom_media));
    flags.set(
      ParserFlags::DEEP_SELECTOR_COMBINATOR,
      matches!(non_standard, Some(v) if v.deep_selector_combinator),
    );

    let parser_options = ParserOptions {
      flags,
      css_modules: if let Some(css_modules) = &config.css_modules {
        match css_modules {
          CssModulesOption::Bool(true) => Some(lightningcss::css_modules::Config::default()),
          CssModulesOption::Bool(false) => None,
          CssModulesOption::Config(c) => Some(lightningcss::css_modules::Config {
            pattern: if let Some(pattern) = c.pattern.as_ref() {
              match lightningcss::css_modules::Pattern::parse(pattern) {
                Ok(p) => p,
                Err(e) => return Err(CompileError::PatternError(e)),
              }
            } else {
              Default::default()
            },
            dashed_idents: c.dashed_idents.unwrap_or_default(),
            animation: c.animation.unwrap_or(true),
            container: c.container.unwrap_or(true),
            grid: c.grid.unwrap_or(true),
            custom_idents: c.custom_idents.unwrap_or(true),
            pure: c.pure.unwrap_or_default(),
          }),
        }
      } else {
        None
      },
      error_recovery: config.error_recovery.unwrap_or_default(),
      warnings: warnings.clone(),
      filename: String::new(),
      source_index: 0,
    };

    let mut at_rule_parser = CustomAtRuleParser {
      configs: config.custom_at_rules.clone().unwrap_or_default(),
    };

    let mut bundler =
      Bundler::new_with_at_rule_parser(fs, source_map.as_mut(), parser_options, &mut at_rule_parser);
    let mut stylesheet = bundler.bundle(Path::new(&config.filename))?;

    if let Some(visit) = visit {
      visit(&mut stylesheet).map_err(CompileError::JsError)?;
    }

    let targets = Targets {
      browsers: config.targets,
      include: Features::from_bits_truncate(config.include),
      exclude: Features::from_bits_truncate(config.exclude),
    };

    stylesheet.minify(MinifyOptions {
      targets,
      unused_symbols: config.unused_symbols.clone().unwrap_or_default(),
    })?;

    stylesheet.to_css(PrinterOptions {
      minify: config.minify.unwrap_or_default(),
      source_map: source_map.as_mut(),
      project_root,
      targets,
      analyze_dependencies: if let Some(d) = &config.analyze_dependencies {
        match d {
          AnalyzeDependenciesOption::Bool(b) if *b => Some(DependencyOptions { remove_imports: true }),
          AnalyzeDependenciesOption::Config(c) => Some(DependencyOptions {
            remove_imports: !c.preserve_imports,
          }),
          _ => None,
        }
      } else {
        None
      },
      pseudo_classes: config.pseudo_classes.as_ref().map(|p| p.into()),
    })?
  };

  let map = if let Some(source_map) = &mut source_map {
    source_map.to_json(None).ok()
  } else {
    None
  };

  Ok(TransformResult {
    code: res.code.into_bytes(),
    map: map.map(|m| m.into_bytes()),
    exports: res.exports,
    references: res.references,
    dependencies: res.dependencies,
    warnings: warnings.map_or(Vec::new(), |w| {
      Arc::try_unwrap(w)
        .unwrap()
        .into_inner()
        .unwrap()
        .into_iter()
        .map(|w| w.into())
        .collect()
    }),
  })
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct AttrConfig {
  pub filename: Option<String>,
  #[serde(with = "serde_bytes")]
  pub code: Vec<u8>,
  pub targets: Option<Browsers>,
  #[serde(default)]
  pub include: u32,
  #[serde(default)]
  pub exclude: u32,
  #[serde(default)]
  pub minify: bool,
  #[serde(default)]
  pub analyze_dependencies: bool,
  #[serde(default)]
  pub error_recovery: bool,
}

#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct AttrResult<'i> {
  #[serde(with = "serde_bytes")]
  code: Vec<u8>,
  dependencies: Option<Vec<Dependency>>,
  warnings: Vec<Warning<'i>>,
}

impl<'i> AttrResult<'i> {
  fn into_js(self, ctx: CallContext) -> napi::Result<JsUnknown> {
    // Manually construct buffers so we avoid a copy and work around
    // https://github.com/napi-rs/napi-rs/issues/1124.
    let mut obj = ctx.env.create_object()?;
    let buf = ctx.env.create_buffer_with_data(self.code)?;
    obj.set_named_property("code", buf.into_raw())?;
    obj.set_named_property("dependencies", ctx.env.to_js_value(&self.dependencies)?)?;
    obj.set_named_property("warnings", ctx.env.to_js_value(&self.warnings)?)?;
    Ok(obj.into_unknown())
  }
}

fn compile_attr<'i>(
  code: &'i str,
  config: &AttrConfig,
  #[allow(unused_variables)] visitor: &mut Option<JsVisitor>,
) -> Result<AttrResult<'i>, CompileError<'i, napi::Error>> {
  let warnings = if config.error_recovery {
    Some(Arc::new(RwLock::new(Vec::new())))
  } else {
    None
  };
  let res = {
    let filename = config.filename.clone().unwrap_or_default();
    let mut attr = StyleAttribute::parse(
      &code,
      ParserOptions {
        filename,
        error_recovery: config.error_recovery,
        warnings: warnings.clone(),
        ..ParserOptions::default()
      },
    )?;

    #[cfg(feature = "visitor")]
    if let Some(visitor) = visitor.as_mut() {
      attr.visit(visitor).unwrap();
    }

    let targets = Targets {
      browsers: config.targets,
      include: Features::from_bits_truncate(config.include),
      exclude: Features::from_bits_truncate(config.exclude),
    };

    attr.minify(MinifyOptions {
      targets,
      ..MinifyOptions::default()
    });
    attr.to_css(PrinterOptions {
      minify: config.minify,
      source_map: None,
      project_root: None,
      targets,
      analyze_dependencies: if config.analyze_dependencies {
        Some(DependencyOptions::default())
      } else {
        None
      },
      pseudo_classes: None,
    })?
  };
  Ok(AttrResult {
    code: res.code.into_bytes(),
    dependencies: res.dependencies,
    warnings: warnings.map_or(Vec::new(), |w| {
      Arc::try_unwrap(w)
        .unwrap()
        .into_inner()
        .unwrap()
        .into_iter()
        .map(|w| w.into())
        .collect()
    }),
  })
}

enum CompileError<'i, E: std::error::Error> {
  ParseError(Error<ParserError<'i>>),
  MinifyError(Error<MinifyErrorKind>),
  PrinterError(Error<PrinterErrorKind>),
  SourceMapError(parcel_sourcemap::SourceMapError),
  BundleError(Error<BundleErrorKind<'i, E>>),
  PatternError(PatternParseError),
  #[cfg(feature = "visitor")]
  JsError(napi::Error),
}

impl<'i, E: std::error::Error> std::fmt::Display for CompileError<'i, E> {
  fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
    match self {
      CompileError::ParseError(err) => err.kind.fmt(f),
      CompileError::MinifyError(err) => err.kind.fmt(f),
      CompileError::PrinterError(err) => err.kind.fmt(f),
      CompileError::BundleError(err) => err.kind.fmt(f),
      CompileError::PatternError(err) => err.fmt(f),
      CompileError::SourceMapError(err) => write!(f, "{}", err.to_string()), // TODO: switch to `fmt::Display` once parcel_sourcemap supports this
      #[cfg(feature = "visitor")]
      CompileError::JsError(err) => std::fmt::Debug::fmt(&err, f),
    }
  }
}

impl<'i, E: IntoJsError + std::error::Error> CompileError<'i, E> {
  fn into_js_error(self, env: Env, code: Option<&str>) -> napi::Result<napi::Error> {
    let reason = self.to_string();
    let data = match &self {
      CompileError::ParseError(Error { kind, .. }) => env.to_js_value(kind)?,
      CompileError::PrinterError(Error { kind, .. }) => env.to_js_value(kind)?,
      CompileError::MinifyError(Error { kind, .. }) => env.to_js_value(kind)?,
      CompileError::BundleError(Error { kind, .. }) => env.to_js_value(kind)?,
      _ => env.get_null()?.into_unknown(),
    };

    let (js_error, loc) = match self {
      CompileError::BundleError(Error {
        loc,
        kind: BundleErrorKind::ResolverError(e),
      }) => {
        // Add location info to existing JS error if available.
        (e.into_js_error(env)?, loc)
      }
      CompileError::ParseError(Error { loc, .. })
      | CompileError::PrinterError(Error { loc, .. })
      | CompileError::MinifyError(Error { loc, .. })
      | CompileError::BundleError(Error { loc, .. }) => {
        // Generate an error with location information.
        let syntax_error = env.get_global()?.get_named_property::<napi::JsFunction>("SyntaxError")?;
        let reason = env.create_string_from_std(reason)?;
        let obj = syntax_error.new_instance(&[reason])?;
        (obj.into_unknown(), loc)
      }
      _ => return Ok(self.into()),
    };

    if js_error.get_type()? == napi::ValueType::Object {
      let mut obj: JsObject = unsafe { js_error.cast() };
      if let Some(loc) = loc {
        let line = env.create_int32((loc.line + 1) as i32)?;
        let col = env.create_int32(loc.column as i32)?;
        let filename = env.create_string_from_std(loc.filename)?;
        obj.set_named_property("fileName", filename)?;
        if let Some(code) = code {
          let source = env.create_string(code)?;
          obj.set_named_property("source", source)?;
        }
        let mut loc = env.create_object()?;
        loc.set_named_property("line", line)?;
        loc.set_named_property("column", col)?;
        obj.set_named_property("loc", loc)?;
      }
      obj.set_named_property("data", data)?;
      Ok(obj.into_unknown().into())
    } else {
      Ok(js_error.into())
    }
  }
}

trait IntoJsError {
  fn into_js_error(self, env: Env) -> napi::Result<JsUnknown>;
}

impl IntoJsError for std::io::Error {
  fn into_js_error(self, env: Env) -> napi::Result<JsUnknown> {
    let reason = self.to_string();
    let syntax_error = env.get_global()?.get_named_property::<napi::JsFunction>("SyntaxError")?;
    let reason = env.create_string_from_std(reason)?;
    let obj = syntax_error.new_instance(&[reason])?;
    Ok(obj.into_unknown())
  }
}

impl IntoJsError for napi::Error {
  fn into_js_error(self, env: Env) -> napi::Result<JsUnknown> {
    unsafe { JsUnknown::from_napi_value(env.raw(), ToNapiValue::to_napi_value(env.raw(), self)?) }
  }
}

impl<'i, E: std::error::Error> From<Error<ParserError<'i>>> for CompileError<'i, E> {
  fn from(e: Error<ParserError<'i>>) -> CompileError<'i, E> {
    CompileError::ParseError(e)
  }
}

impl<'i, E: std::error::Error> From<Error<MinifyErrorKind>> for CompileError<'i, E> {
  fn from(err: Error<MinifyErrorKind>) -> CompileError<'i, E> {
    CompileError::MinifyError(err)
  }
}

impl<'i, E: std::error::Error> From<Error<PrinterErrorKind>> for CompileError<'i, E> {
  fn from(err: Error<PrinterErrorKind>) -> CompileError<'i, E> {
    CompileError::PrinterError(err)
  }
}

impl<'i, E: std::error::Error> From<parcel_sourcemap::SourceMapError> for CompileError<'i, E> {
  fn from(e: parcel_sourcemap::SourceMapError) -> CompileError<'i, E> {
    CompileError::SourceMapError(e)
  }
}

impl<'i, E: std::error::Error> From<Error<BundleErrorKind<'i, E>>> for CompileError<'i, E> {
  fn from(e: Error<BundleErrorKind<'i, E>>) -> CompileError<'i, E> {
    CompileError::BundleError(e)
  }
}

impl<'i, E: std::error::Error> From<CompileError<'i, E>> for napi::Error {
  fn from(e: CompileError<'i, E>) -> napi::Error {
    match e {
      CompileError::SourceMapError(e) => napi::Error::from_reason(e.to_string()),
      CompileError::PatternError(e) => napi::Error::from_reason(e.to_string()),
      #[cfg(feature = "visitor")]
      CompileError::JsError(e) => e,
      _ => napi::Error::new(napi::Status::GenericFailure, e.to_string()),
    }
  }
}

#[derive(Serialize)]
struct Warning<'i> {
  message: String,
  #[serde(flatten)]
  data: ParserError<'i>,
  loc: Option<ErrorLocation>,
}

impl<'i> From<Error<ParserError<'i>>> for Warning<'i> {
  fn from(mut e: Error<ParserError<'i>>) -> Self {
    // Convert to 1-based line numbers.
    if let Some(loc) = &mut e.loc {
      loc.line += 1;
    }
    Warning {
      message: e.kind.to_string(),
      data: e.kind,
      loc: e.loc,
    }
  }
}
