#![allow(clippy::not_unsafe_ptr_arg_deref)]

use std::collections::HashSet;
use std::ffi::{CStr, CString};
use std::mem::ManuallyDrop;
use std::os::raw::c_char;
use std::sync::{Arc, RwLock};

use lightningcss::css_modules::PatternParseError;
use lightningcss::error::{Error, MinifyErrorKind, ParserError, PrinterError};
use lightningcss::stylesheet::{MinifyOptions, ParserFlags, ParserOptions, PrinterOptions, StyleSheet};
use lightningcss::targets::Browsers;
use parcel_sourcemap::SourceMap;

pub struct StyleSheetWrapper<'i> {
  stylesheet: StyleSheet<'i>,
  source: &'i str,
  warnings: Vec<CssError<'i>>,
}

pub struct CssError<'i> {
  kind: ErrorKind<'i>,
  message: Option<CString>,
}

impl<'i> CssError<'i> {
  fn message(&mut self) -> *const c_char {
    if let Some(message) = &self.message {
      return message.as_ptr();
    }

    let string: String = match &self.kind {
      ErrorKind::ParserError(err) => err.to_string().into(),
      ErrorKind::MinifyError(err) => err.to_string().into(),
      ErrorKind::PrinterError(err) => err.to_string().into(),
      ErrorKind::PatternParseError(err) => err.to_string().into(),
      ErrorKind::BrowserslistError(err) => err.to_string().into(),
      ErrorKind::SourceMapError(err) => err.to_string().into(),
    };

    self.message = Some(CString::new(string).unwrap());
    self.message.as_ref().unwrap().as_ptr()
  }
}

pub enum ErrorKind<'i> {
  ParserError(Error<ParserError<'i>>),
  MinifyError(Error<MinifyErrorKind>),
  PrinterError(PrinterError),
  PatternParseError(PatternParseError),
  BrowserslistError(browserslist::Error),
  SourceMapError(parcel_sourcemap::SourceMapError),
}

macro_rules! impl_from {
  ($name: ident, $t: ty) => {
    impl<'i> From<$t> for CssError<'i> {
      fn from(err: $t) -> Self {
        CssError {
          kind: ErrorKind::$name(err),
          message: None,
        }
      }
    }
  };
}

impl_from!(ParserError, Error<ParserError<'i>>);
impl_from!(MinifyError, Error<MinifyErrorKind>);
impl_from!(PrinterError, PrinterError);
impl_from!(PatternParseError, PatternParseError);
impl_from!(BrowserslistError, browserslist::Error);
impl_from!(SourceMapError, parcel_sourcemap::SourceMapError);

#[repr(C)]
pub struct ParseOptions {
  filename: *const c_char,
  nesting: bool,
  custom_media: bool,
  scroll_navigation_controls: bool,
  css_modules: bool,
  css_modules_pattern: *const c_char,
  css_modules_dashed_idents: bool,
  error_recovery: bool,
}

#[repr(C)]
#[derive(Default, PartialEq)]
pub struct Targets {
  android: u32,
  chrome: u32,
  edge: u32,
  firefox: u32,
  ie: u32,
  ios_saf: u32,
  opera: u32,
  safari: u32,
  samsung: u32,
}

impl Into<Browsers> for Targets {
  fn into(self) -> Browsers {
    macro_rules! browser {
      ($val: expr) => {
        if $val > 0 {
          Some($val)
        } else {
          None
        }
      };
    }

    Browsers {
      android: browser!(self.android),
      chrome: browser!(self.chrome),
      edge: browser!(self.edge),
      firefox: browser!(self.firefox),
      ie: browser!(self.ie),
      ios_saf: browser!(self.ios_saf),
      opera: browser!(self.opera),
      safari: browser!(self.safari),
      samsung: browser!(self.samsung),
    }
  }
}

macro_rules! unwrap {
  ($result: expr, $error: ident, $ret: expr) => {
    match $result {
      Ok(v) => v,
      Err(err) => unsafe {
        *$error = Box::into_raw(Box::new(err.into()));
        return $ret;
      },
    }
  };
}

#[no_mangle]
pub extern "C" fn lightningcss_browserslist_to_targets(
  query: *const c_char,
  targets: *mut Targets,
  error: *mut *mut CssError,
) -> bool {
  let string = unsafe { std::str::from_utf8_unchecked(CStr::from_ptr(query).to_bytes()) };
  match Browsers::from_browserslist([string]) {
    Ok(Some(browsers)) => {
      let targets = unsafe { &mut *targets };
      targets.android = browsers.android.unwrap_or_default();
      targets.chrome = browsers.chrome.unwrap_or_default();
      targets.edge = browsers.edge.unwrap_or_default();
      targets.firefox = browsers.firefox.unwrap_or_default();
      targets.ie = browsers.ie.unwrap_or_default();
      targets.ios_saf = browsers.ios_saf.unwrap_or_default();
      targets.opera = browsers.opera.unwrap_or_default();
      targets.safari = browsers.safari.unwrap_or_default();
      targets.samsung = browsers.samsung.unwrap_or_default();
      true
    }
    Ok(None) => true,
    Err(err) => unsafe {
      *error = Box::into_raw(Box::new(err.into()));
      false
    },
  }
}

#[repr(C)]
pub struct TransformOptions {
  targets: Targets,
  unused_symbols: *mut *mut c_char,
  unused_symbols_len: usize,
}

impl Into<MinifyOptions> for TransformOptions {
  fn into(self) -> MinifyOptions {
    let mut unused_symbols = HashSet::new();
    let slice = unsafe { std::slice::from_raw_parts(self.unused_symbols, self.unused_symbols_len) };
    for symbol in slice {
      let string = unsafe { std::str::from_utf8_unchecked(CStr::from_ptr(*symbol).to_bytes()).to_owned() };
      unused_symbols.insert(string);
    }

    MinifyOptions {
      targets: if self.targets != Targets::default() {
        Some(self.targets.into()).into()
      } else {
        Default::default()
      },
      unused_symbols,
    }
  }
}

#[repr(C)]
pub struct ToCssOptions {
  minify: bool,
  source_map: bool,
  input_source_map: *const c_char,
  input_source_map_len: usize,
  project_root: *const c_char,
  targets: Targets,
  analyze_dependencies: bool,
  pseudo_classes: PseudoClasses,
}

#[derive(PartialEq)]
#[repr(C)]
pub struct PseudoClasses {
  hover: *const c_char,
  active: *const c_char,
  focus: *const c_char,
  focus_visible: *const c_char,
  focus_within: *const c_char,
}

impl Default for PseudoClasses {
  fn default() -> Self {
    PseudoClasses {
      hover: std::ptr::null(),
      active: std::ptr::null(),
      focus: std::ptr::null(),
      focus_visible: std::ptr::null(),
      focus_within: std::ptr::null(),
    }
  }
}

impl<'a> Into<lightningcss::printer::PseudoClasses<'a>> for PseudoClasses {
  fn into(self) -> lightningcss::printer::PseudoClasses<'a> {
    macro_rules! pc {
      ($ptr: expr) => {
        if $ptr.is_null() {
          None
        } else {
          Some(unsafe { std::str::from_utf8_unchecked(CStr::from_ptr($ptr).to_bytes()) })
        }
      };
    }

    lightningcss::printer::PseudoClasses {
      hover: pc!(self.hover),
      active: pc!(self.active),
      focus: pc!(self.focus),
      focus_visible: pc!(self.focus_visible),
      focus_within: pc!(self.focus_within),
    }
  }
}

#[no_mangle]
pub extern "C" fn lightningcss_stylesheet_parse(
  source: *const c_char,
  len: usize,
  options: ParseOptions,
  error: *mut *mut CssError,
) -> *mut StyleSheetWrapper {
  let slice = unsafe { std::slice::from_raw_parts(source as *const u8, len) };
  let code = unsafe { std::str::from_utf8_unchecked(slice) };
  let warnings = Arc::new(RwLock::new(Vec::new()));
  let mut flags = ParserFlags::empty();
  flags.set(ParserFlags::CUSTOM_MEDIA, options.custom_media);
  flags.set(
    ParserFlags::SCROLL_NAVIGATION_CONTROLS,
    options.scroll_navigation_controls,
  );
  let opts = ParserOptions {
    filename: if options.filename.is_null() {
      String::new()
    } else {
      unsafe { std::str::from_utf8_unchecked(CStr::from_ptr(options.filename).to_bytes()).to_owned() }
    },
    flags,
    css_modules: if options.css_modules {
      let pattern = if !options.css_modules_pattern.is_null() {
        let pattern =
          unsafe { std::str::from_utf8_unchecked(CStr::from_ptr(options.css_modules_pattern).to_bytes()) };
        unwrap!(
          lightningcss::css_modules::Pattern::parse(pattern),
          error,
          std::ptr::null_mut()
        )
      } else {
        lightningcss::css_modules::Pattern::default()
      };
      Some(lightningcss::css_modules::Config {
        pattern,
        dashed_idents: options.css_modules_dashed_idents,
        ..Default::default()
      })
    } else {
      None
    },
    error_recovery: options.error_recovery,
    source_index: 0,
    warnings: Some(warnings.clone()),
  };

  let stylesheet = unwrap!(StyleSheet::parse(code, opts), error, std::ptr::null_mut());
  Box::into_raw(Box::new(StyleSheetWrapper {
    stylesheet,
    source: code,
    warnings: warnings.clone().read().unwrap().iter().map(|w| w.clone().into()).collect(),
  }))
}

#[no_mangle]
pub extern "C" fn lightningcss_stylesheet_transform(
  stylesheet: *mut StyleSheetWrapper,
  options: TransformOptions,
  error: *mut *mut CssError,
) -> bool {
  let wrapper = unsafe { stylesheet.as_mut() }.unwrap();
  unwrap!(wrapper.stylesheet.minify(options.into()), error, false);
  true
}

#[no_mangle]
pub extern "C" fn lightningcss_stylesheet_to_css(
  stylesheet: *mut StyleSheetWrapper,
  options: ToCssOptions,
  error: *mut *mut CssError,
) -> ToCssResult {
  let wrapper = unsafe { stylesheet.as_mut() }.unwrap();
  let mut source_map = if options.source_map {
    let mut sm = SourceMap::new("/");
    sm.add_source(&wrapper.stylesheet.sources[0]);
    unwrap!(sm.set_source_content(0, wrapper.source), error, ToCssResult::default());
    Some(sm)
  } else {
    None
  };

  let opts = PrinterOptions {
    minify: options.minify,
    project_root: if options.project_root.is_null() {
      None
    } else {
      Some(unsafe { std::str::from_utf8_unchecked(CStr::from_ptr(options.project_root).to_bytes()) })
    },
    source_map: source_map.as_mut(),
    targets: if options.targets != Targets::default() {
      Some(options.targets.into()).into()
    } else {
      Default::default()
    },
    analyze_dependencies: if options.analyze_dependencies {
      Some(Default::default())
    } else {
      None
    },
    pseudo_classes: if options.pseudo_classes != PseudoClasses::default() {
      Some(options.pseudo_classes.into())
    } else {
      None
    },
  };

  let res = unwrap!(wrapper.stylesheet.to_css(opts), error, ToCssResult::default());

  let map = if let Some(mut source_map) = source_map {
    if !options.input_source_map.is_null() {
      let slice =
        unsafe { std::slice::from_raw_parts(options.input_source_map as *const u8, options.input_source_map_len) };
      let input_source_map = unsafe { std::str::from_utf8_unchecked(slice) };
      let mut sm = unwrap!(
        SourceMap::from_json("/", input_source_map),
        error,
        ToCssResult::default()
      );
      unwrap!(source_map.extends(&mut sm), error, ToCssResult::default());
    }

    unwrap!(source_map.to_json(None), error, ToCssResult::default()).into()
  } else {
    RawString::default()
  };

  let (exports, exports_len) = if let Some(exports) = res.exports {
    let exports: Vec<CssModuleExport> = exports
      .into_iter()
      .map(|(k, v)| {
        let composes_len = v.composes.len();
        let composes = if !v.composes.is_empty() {
          let composes: Vec<CssModuleReference> = v.composes.into_iter().map(|composes| composes.into()).collect();
          ManuallyDrop::new(composes).as_mut_ptr()
        } else {
          std::ptr::null_mut()
        };

        CssModuleExport {
          exported: k.into(),
          local: v.name.into(),
          is_referenced: v.is_referenced,
          composes,
          composes_len,
        }
      })
      .collect();
    let mut exports = ManuallyDrop::new(exports);
    (exports.as_mut_ptr(), exports.len())
  } else {
    (std::ptr::null_mut(), 0)
  };

  let (references, references_len) = if let Some(references) = res.references {
    let references: Vec<CssModulePlaceholder> = references
      .into_iter()
      .map(|(k, v)| CssModulePlaceholder {
        placeholder: k.into(),
        reference: v.into(),
      })
      .collect();
    let mut references = ManuallyDrop::new(references);
    (references.as_mut_ptr(), references.len())
  } else {
    (std::ptr::null_mut(), 0)
  };

  ToCssResult {
    code: res.code.into(),
    map,
    exports,
    exports_len,
    references,
    references_len,
  }
}

#[no_mangle]
pub extern "C" fn lightningcss_stylesheet_free(stylesheet: *mut StyleSheetWrapper) {
  if !stylesheet.is_null() {
    drop(unsafe { Box::from_raw(stylesheet) })
  }
}

#[repr(C)]
pub struct ToCssResult {
  code: RawString,
  map: RawString,
  exports: *mut CssModuleExport,
  exports_len: usize,
  references: *mut CssModulePlaceholder,
  references_len: usize,
}

impl Default for ToCssResult {
  fn default() -> Self {
    ToCssResult {
      code: RawString::default(),
      map: RawString::default(),
      exports: std::ptr::null_mut(),
      exports_len: 0,
      references: std::ptr::null_mut(),
      references_len: 0,
    }
  }
}

impl Drop for ToCssResult {
  fn drop(&mut self) {
    if !self.exports.is_null() {
      let exports = unsafe { Vec::from_raw_parts(self.exports, self.exports_len, self.exports_len) };
      drop(exports);
      self.exports = std::ptr::null_mut();
    }

    if !self.references.is_null() {
      let references = unsafe { Vec::from_raw_parts(self.references, self.references_len, self.references_len) };
      drop(references);
      self.references = std::ptr::null_mut();
    }
  }
}

#[no_mangle]
pub extern "C" fn lightningcss_to_css_result_free(result: ToCssResult) {
  drop(result)
}

#[repr(C)]
pub struct CssModuleExport {
  exported: RawString,
  local: RawString,
  is_referenced: bool,
  composes: *mut CssModuleReference,
  composes_len: usize,
}

impl Drop for CssModuleExport {
  fn drop(&mut self) {
    if !self.composes.is_null() {
      let composes = unsafe { Vec::from_raw_parts(self.composes, self.composes_len, self.composes_len) };
      drop(composes);
      self.composes = std::ptr::null_mut();
    }
  }
}

#[repr(C)]
pub enum CssModuleReference {
  /// A local reference.
  Local {
    /// The local (compiled) name for the reference.
    name: RawString,
  },
  /// A global reference.
  Global {
    /// The referenced global name.
    name: RawString,
  },
  /// A reference to an export in a different file.
  Dependency {
    /// The name to reference within the dependency.
    name: RawString,
    /// The dependency specifier for the referenced file.
    specifier: RawString,
  },
}

impl From<lightningcss::css_modules::CssModuleReference> for CssModuleReference {
  fn from(reference: lightningcss::css_modules::CssModuleReference) -> Self {
    use lightningcss::css_modules::CssModuleReference::*;
    match reference {
      Local { name } => CssModuleReference::Local { name: name.into() },
      Global { name } => CssModuleReference::Global { name: name.into() },
      Dependency { name, specifier } => CssModuleReference::Dependency {
        name: name.into(),
        specifier: specifier.into(),
      },
    }
  }
}

#[repr(C)]
pub struct CssModulePlaceholder {
  placeholder: RawString,
  reference: CssModuleReference,
}

#[repr(C)]
pub struct RawString {
  text: *mut c_char,
  len: usize,
}

impl Default for RawString {
  fn default() -> Self {
    RawString {
      text: std::ptr::null_mut(),
      len: 0,
    }
  }
}

impl From<String> for RawString {
  fn from(string: String) -> RawString {
    RawString {
      len: string.len(),
      text: Box::into_raw(string.into_boxed_str()) as *mut c_char,
    }
  }
}

impl Drop for RawString {
  fn drop(&mut self) {
    if self.text.is_null() {
      return;
    }
    drop(unsafe { Box::from_raw(self.text) });
    self.text = std::ptr::null_mut();
  }
}

#[no_mangle]
pub extern "C" fn lightningcss_error_message(error: *mut CssError) -> *const c_char {
  match unsafe { error.as_mut() } {
    Some(err) => err.message(),
    None => std::ptr::null(),
  }
}

#[no_mangle]
pub extern "C" fn lightningcss_error_free(error: *mut CssError) {
  if !error.is_null() {
    drop(unsafe { Box::from_raw(error) })
  }
}

#[no_mangle]
pub extern "C" fn lightningcss_stylesheet_get_warning_count<'i>(stylesheet: *mut StyleSheetWrapper<'i>) -> usize {
  match unsafe { stylesheet.as_mut() } {
    Some(s) => s.warnings.len(),
    None => 0,
  }
}

#[no_mangle]
pub extern "C" fn lightningcss_stylesheet_get_warning<'i>(
  stylesheet: *mut StyleSheetWrapper<'i>,
  index: usize,
) -> *const c_char {
  let stylesheet = match unsafe { stylesheet.as_mut() } {
    Some(s) => s,
    None => return std::ptr::null(),
  };

  match stylesheet.warnings.get_mut(index) {
    Some(w) => w.message(),
    None => std::ptr::null(),
  }
}
