use std::collections::HashSet;

use serde::{Deserialize, Serialize};
use swc_atoms::JsWord;
use swc_common::{Mark, Span, SyntaxContext, DUMMY_SP};
use swc_ecmascript::ast;

pub fn match_member_expr(
  expr: &ast::MemberExpr,
  idents: Vec<&str>,
  decls: &HashSet<(JsWord, SyntaxContext)>,
) -> bool {
  use ast::{Expr::*, ExprOrSuper::*, Ident, Lit, Str};

  let mut member = expr;
  let mut idents = idents;
  while idents.len() > 1 {
    let expected = idents.pop().unwrap();
    let prop = match &*member.prop {
      Lit(Lit::Str(Str { value: ref sym, .. })) => sym,
      Ident(Ident { ref sym, .. }) => {
        if member.computed {
          return false;
        }

        sym
      }
      _ => return false,
    };

    if prop != expected {
      return false;
    }

    match &member.obj {
      Expr(expr) => match &**expr {
        Member(m) => member = m,
        Ident(Ident { ref sym, span, .. }) => {
          return idents.len() == 1
            && sym == idents.pop().unwrap()
            && !decls.contains(&(sym.clone(), span.ctxt()));
        }
        _ => return false,
      },
      _ => return false,
    }
  }

  false
}

pub fn create_require(specifier: swc_atoms::JsWord) -> ast::CallExpr {
  let mut normalized_specifier = specifier;
  if normalized_specifier.starts_with("node:") {
    normalized_specifier = normalized_specifier.replace("node:", "").into();
  }

  ast::CallExpr {
    callee: ast::ExprOrSuper::Expr(Box::new(ast::Expr::Ident(ast::Ident::new(
      "require".into(),
      DUMMY_SP,
    )))),
    args: vec![ast::ExprOrSpread {
      expr: Box::new(ast::Expr::Lit(ast::Lit::Str(ast::Str {
        span: DUMMY_SP,
        value: normalized_specifier,
        has_escape: false,
        kind: ast::StrKind::Synthesized,
      }))),
      spread: None,
    }],
    span: DUMMY_SP,
    type_args: None,
  }
}

fn is_marked(span: Span, mark: Mark) -> bool {
  let mut ctxt = span.ctxt().clone();

  loop {
    let m = ctxt.remove_mark();
    if m == Mark::root() {
      return false;
    }

    if m == mark {
      return true;
    }
  }
}

pub fn match_require(
  node: &ast::Expr,
  decls: &HashSet<(JsWord, SyntaxContext)>,
  ignore_mark: Mark,
) -> Option<JsWord> {
  use ast::*;

  match node {
    Expr::Call(call) => match &call.callee {
      ExprOrSuper::Expr(expr) => match &**expr {
        Expr::Ident(ident) => {
          if ident.sym == js_word!("require")
            && !decls.contains(&(ident.sym.clone(), ident.span.ctxt))
            && !is_marked(ident.span, ignore_mark)
          {
            if let Some(arg) = call.args.get(0) {
              if let Expr::Lit(lit) = &*arg.expr {
                if let Lit::Str(str_) = lit {
                  return Some(str_.value.clone());
                }
              }
            }
          }

          None
        }
        _ => None,
      },
      _ => None,
    },
    _ => None,
  }
}

pub fn match_import(node: &ast::Expr, ignore_mark: Mark) -> Option<JsWord> {
  use ast::*;

  match node {
    Expr::Call(call) => match &call.callee {
      ExprOrSuper::Expr(expr) => match &**expr {
        Expr::Ident(ident) => {
          if ident.sym == js_word!("import") && !is_marked(ident.span, ignore_mark) {
            if let Some(arg) = call.args.get(0) {
              if let Expr::Lit(lit) = &*arg.expr {
                if let Lit::Str(str_) = lit {
                  return Some(str_.value.clone());
                }
              }
            }
          }

          None
        }
        _ => None,
      },
      _ => None,
    },
    _ => None,
  }
}

#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)]
pub struct SourceLocation {
  start_line: usize,
  start_col: usize,
  end_line: usize,
  end_col: usize,
}

impl SourceLocation {
  pub fn from(source_map: &swc_common::SourceMap, span: swc_common::Span) -> Self {
    let start = source_map.lookup_char_pos(span.lo);
    let end = source_map.lookup_char_pos(span.hi);
    // - SWC's columns are exclusive, ours are inclusive (column - 1)
    // - SWC has 0-based columns, ours are 1-based (column + 1)
    // = +-0
    SourceLocation {
      start_line: start.line,
      start_col: start.col_display + 1,
      end_line: end.line,
      end_col: end.col_display,
    }
  }
}

#[derive(Serialize, Deserialize, Debug)]
pub struct CodeHighlight {
  pub message: Option<String>,
  pub loc: SourceLocation,
}

#[derive(Serialize, Deserialize, Debug)]
pub struct Diagnostic {
  pub message: String,
  pub code_highlights: Option<Vec<CodeHighlight>>,
  pub hints: Option<Vec<String>>,
  pub show_environment: bool,
  pub severity: DiagnosticSeverity,
  pub documentation_url: Option<String>,
}

#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
pub enum DiagnosticSeverity {
  /// Fails the build with an error.
  Error,
  /// Logs a warning, but the build does not fail.
  Warning,
  /// An error if this is source code in the project, or a warning if in node_modules.
  SourceError,
}

#[derive(Serialize, Debug, Deserialize, Eq, PartialEq, Clone, Copy)]
pub enum SourceType {
  Script,
  Module,
}
