use {super::*, CompileErrorKind::*};

#[derive(Default)]
pub(crate) struct Analyzer<'run, 'src> {
  aliases: Table<'src, Alias<'src, Namepath<'src>>>,
  assignments: Vec<&'run Binding<'src, Expression<'src>>>,
  functions: Vec<&'run FunctionDefinition<'src>>,
  modules: Table<'src, Justfile<'src>>,
  recipes: Vec<&'run Recipe<'src, UnresolvedDependency<'src>>>,
  sets: Table<'src, Set<'src>>,
  unexports: HashSet<String>,
  warnings: Vec<Warning>,
}

impl<'run, 'src> Analyzer<'run, 'src> {
  pub(crate) fn analyze(
    asts: &'run HashMap<PathBuf, Ast<'src>>,
    config: &Config,
    doc: Option<String>,
    groups: &[StringLiteral<'src>],
    loaded: &[PathBuf],
    name: Option<Name<'src>>,
    overrides: &mut HashMap<Number, String>,
    paths: &HashMap<PathBuf, PathBuf>,
    private: bool,
    root: &Path,
  ) -> RunResult<'src, Justfile<'src>> {
    Self::default().justfile(
      asts, config, doc, groups, loaded, name, overrides, paths, private, root,
    )
  }

  fn justfile(
    mut self,
    asts: &'run HashMap<PathBuf, Ast<'src>>,
    config: &Config,
    doc: Option<String>,
    groups: &[StringLiteral<'src>],
    loaded: &[PathBuf],
    name: Option<Name<'src>>,
    overrides: &mut HashMap<Number, String>,
    paths: &HashMap<PathBuf, PathBuf>,
    private: bool,
    root: &Path,
  ) -> RunResult<'src, Justfile<'src>> {
    let mut definitions = HashMap::new();
    let mut imports = HashSet::new();
    let mut unstable_features = BTreeSet::new();

    let mut stack = Vec::new();
    let ast = asts.get(root).unwrap();
    stack.push(ast);

    while let Some(ast) = stack.pop() {
      unstable_features.extend(&ast.unstable_features);

      for item in &ast.items {
        match item {
          Item::Alias(alias) => {
            Self::define(&mut definitions, alias.name, "alias", false)?;
            self.aliases.insert(alias.clone());
          }
          Item::Assignment(assignment) => {
            self.assignments.push(assignment);
          }
          Item::Comment(_) => (),
          Item::Function(function) => {
            self.functions.push(function);
          }
          Item::Import { absolute, .. } => {
            if let Some(absolute) = absolute {
              if imports.insert(absolute) {
                stack.push(asts.get(absolute).unwrap());
              }
            }
          }
          Item::Module {
            absolute,
            doc,
            groups,
            name,
            private,
            ..
          } => {
            if let Some(absolute) = absolute {
              Self::define(&mut definitions, *name, "module", false)?;
              self.modules.insert(Self::analyze(
                asts,
                config,
                doc.clone(),
                groups.as_slice(),
                loaded,
                Some(*name),
                overrides,
                paths,
                *private,
                absolute,
              )?);
            }
          }
          Item::Newline => {}
          Item::Recipe(recipe) => {
            if recipe.enabled() {
              Self::analyze_recipe(recipe)?;
              self.recipes.push(recipe);
            }
          }
          Item::Set(set) => {
            self.analyze_set(set)?;
            self.sets.insert(set.clone());
          }
          Item::Unexport { name } => {
            if !self.unexports.insert(name.lexeme().to_string()) {
              return Err(
                name
                  .error(DuplicateUnexport {
                    variable: name.lexeme(),
                  })
                  .into(),
              );
            }
          }
        }
      }

      self.warnings.extend(ast.warnings.iter().cloned());
    }

    let mut allow_duplicate_variables = false;

    for (_name, set) in &self.sets {
      if let Setting::AllowDuplicateVariables(value) = set.value {
        allow_duplicate_variables = value;
      }
    }

    let mut assignments: Table<'src, Assignment<'src>> = Table::default();
    for assignment in self.assignments {
      let variable = assignment.name.lexeme();

      if !allow_duplicate_variables && assignments.contains_key(variable) {
        return Err(assignment.name.error(DuplicateVariable { variable }).into());
      }

      if assignments
        .get(variable)
        .is_none_or(|original| assignment.file_depth <= original.file_depth)
      {
        assignments.insert(assignment.clone());
      }

      if self.unexports.contains(variable) {
        return Err(assignment.name.error(ExportUnexported { variable }).into());
      }
    }

    let mut functions = Table::<FunctionDefinition>::default();
    for function in self.functions {
      let name = function.name.lexeme();
      if let Some(first) = functions.get(name) {
        return Err(
          function
            .name
            .error(Redefinition {
              first_type: "function",
              second_type: "function",
              name,
              first: first.name.line,
            })
            .into(),
        );
      }
      functions.insert(function.clone());
    }

    AssignmentResolver::resolve_assignments(&assignments, &functions)?;

    for set in self.sets.values() {
      for expression in set.value.expressions() {
        for reference in expression.references() {
          match reference {
            Reference::Call { name, arguments } => {
              Analyzer::resolve_call(&functions, name, arguments)?;
            }
            Reference::Variable(variable) => {
              let name = variable.lexeme();
              if !assignments.contains_key(name) && !constants().contains_key(name) {
                return Err(variable.error(UndefinedVariable { variable: name }).into());
              }
            }
          }
        }
      }
    }

    let mut unknown_overrides = Vec::new();

    for ((path, name), value) in &config.overrides {
      if *path == ast.module_path {
        if let Some(assignment) = assignments.get(name) {
          overrides.insert(assignment.number, value.clone());
        } else {
          unknown_overrides.push(path.join(name));
        }
      } else if path.starts_with(&ast.module_path)
        && !self
          .modules
          .contains_key(&path.components[ast.module_path.components.len()])
      {
        unknown_overrides.push(format!("{path}::{name}"));
      }
    }

    if !unknown_overrides.is_empty() {
      return Err(Error::UnknownOverrides {
        overrides: unknown_overrides,
      });
    }

    let settings =
      Evaluator::evaluate_settings(&assignments, overrides, &Scope::root(), self.sets)?;

    let mut deduplicated_recipes = Table::<'src, UnresolvedRecipe<'src>>::default();
    for recipe in self.recipes {
      Self::define(
        &mut definitions,
        recipe.name,
        "recipe",
        settings.allow_duplicate_recipes,
      )?;

      if deduplicated_recipes
        .get(recipe.name.lexeme())
        .is_none_or(|original| recipe.file_depth <= original.file_depth)
      {
        deduplicated_recipes.insert(recipe.clone());
      }

      if !recipe.is_script() {
        for line in &recipe.body {
          let sigils = line.sigils(&settings);

          if sigils.contains(&Sigil::Guard) && sigils.contains(&Sigil::Infallible) {
            let Fragment::Text { token } = line.fragments.first().unwrap() else {
              unreachable!();
            };
            return Err(
              token
                .error(CompileErrorKind::GuardAndInfallibleSigil)
                .into(),
            );
          }
        }
      }
    }

    let recipes = RecipeResolver::resolve_recipes(
      &assignments,
      &functions,
      &ast.module_path,
      &self.modules,
      &settings,
      deduplicated_recipes,
    )?;

    let mut aliases = Table::new();
    while let Some(alias) = self.aliases.pop() {
      aliases.insert(Self::resolve_alias(&self.modules, &recipes, alias)?);
    }

    let source = root.to_owned();
    let root = paths.get(root).unwrap();

    let mut default = None;
    for recipe in recipes.values() {
      if recipe.attributes.contains(AttributeDiscriminant::Default) {
        if default.is_some() {
          return Err(
            recipe
              .name
              .error(CompileErrorKind::DuplicateDefault {
                recipe: recipe.name.lexeme(),
              })
              .into(),
          );
        }

        default = Some(Arc::clone(recipe));
      }
    }

    let default = default.or_else(|| {
      recipes
        .values()
        .filter(|recipe| recipe.name.path == root)
        .fold(None, |accumulator, next| match accumulator {
          None => Some(Arc::clone(next)),
          Some(previous) => Some(if previous.line_number() < next.line_number() {
            previous
          } else {
            Arc::clone(next)
          }),
        })
    });

    Ok(Justfile {
      aliases,
      assignments,
      default,
      doc: doc.filter(|doc| !doc.is_empty()),
      functions,
      groups: groups.into(),
      loaded: loaded.into(),
      module_path: ast.module_path.clone(),
      modules: self.modules,
      name,
      private,
      recipes,
      settings,
      source,
      unexports: self.unexports,
      unstable_features,
      warnings: self.warnings,
      working_directory: ast.working_directory.clone(),
    })
  }

  fn define(
    definitions: &mut HashMap<&'src str, (&'static str, Name<'src>)>,
    name: Name<'src>,
    second_type: &'static str,
    duplicates_allowed: bool,
  ) -> CompileResult<'src> {
    if let Some((first_type, original)) = definitions.get(name.lexeme()) {
      if !(*first_type == second_type && duplicates_allowed) {
        let ((first_type, second_type), (original, redefinition)) = if name.line < original.line {
          ((second_type, *first_type), (name, *original))
        } else {
          ((*first_type, second_type), (*original, name))
        };

        return Err(redefinition.token.error(Redefinition {
          first_type,
          second_type,
          name: name.lexeme(),
          first: original.line,
        }));
      }
    }

    definitions.insert(name.lexeme(), (second_type, name));

    Ok(())
  }

  fn analyze_recipe(recipe: &UnresolvedRecipe<'src>) -> CompileResult<'src> {
    let mut parameters = BTreeSet::new();
    let mut passed_default = false;

    for parameter in &recipe.parameters {
      if parameters.contains(parameter.name.lexeme()) {
        return Err(parameter.name.error(DuplicateParameter {
          recipe: recipe.name.lexeme(),
          parameter: parameter.name.lexeme(),
        }));
      }

      parameters.insert(parameter.name.lexeme());

      if parameter.default.is_some() {
        passed_default = true;
      } else if passed_default && parameter.is_required() && !parameter.is_option() {
        return Err(
          parameter
            .name
            .token
            .error(RequiredParameterFollowsDefaultParameter {
              parameter: parameter.name.lexeme(),
            }),
        );
      }
    }

    let mut continued = false;
    for line in &recipe.body {
      if !recipe.is_script() && !continued {
        if let Some(Fragment::Text { token }) = line.fragments.first() {
          let text = token.lexeme();

          if text.starts_with(' ') || text.starts_with('\t') {
            return Err(token.error(ExtraLeadingWhitespace));
          }
        }
      }

      continued = line.is_continuation();
    }

    if !recipe.is_script() {
      if let Some(attribute) = recipe.attributes.get(AttributeDiscriminant::Extension) {
        return Err(recipe.name.error(InvalidAttribute {
          item_kind: "recipe",
          item_name: recipe.name.lexeme(),
          attribute: Box::new(attribute.clone()),
        }));
      }
    }

    Ok(())
  }

  fn analyze_set(&self, set: &Set<'src>) -> CompileResult<'src> {
    if let Some(original) = self.sets.get(set.name.lexeme()) {
      return Err(set.name.error(DuplicateSet {
        setting: original.name.lexeme(),
        first: original.name.line,
      }));
    }

    if let Some(second) = Keyword::from_lexeme(set.name.lexeme()) {
      let first = match second {
        Keyword::NoCd => Keyword::WorkingDirectory,
        Keyword::WorkingDirectory => Keyword::NoCd,
        _ => {
          return Ok(());
        }
      };

      if let Some(conflict) = self.sets.get(first.lexeme()) {
        return Err(set.name.error(NoCdAndWorkingDirectorySetting {
          first,
          first_line: conflict.name.line,
          second,
        }));
      }
    }

    Ok(())
  }

  fn resolve_alias<'a>(
    modules: &'a Table<'src, Justfile<'src>>,
    recipes: &'a Table<'src, Arc<Recipe<'src>>>,
    alias: Alias<'src, Namepath<'src>>,
  ) -> CompileResult<'src, Alias<'src>> {
    match Self::resolve_recipe(&alias.target, modules, recipes) {
      Some(target) => Ok(alias.resolve(target)),
      None => Err(alias.name.error(UnknownAliasTarget {
        alias: alias.name.lexeme(),
        target: alias.target,
      })),
    }
  }

  pub(crate) fn resolve_call(
    functions: &'run Table<'src, FunctionDefinition<'src>>,
    name: Name<'src>,
    arguments: usize,
  ) -> CompileResult<'src> {
    let function = name.lexeme();

    let expected = if let Some(function) = functions.get(function) {
      function.parameters.len()..=function.parameters.len()
    } else if let Some(function) = function::get(function) {
      function.expected_arguments()
    } else {
      return Err(name.error(CompileErrorKind::UndefinedFunction { function }));
    };

    if !expected.contains(&arguments) {
      return Err(name.error(CompileErrorKind::FunctionArgumentCountMismatch {
        arguments,
        expected,
        function,
      }));
    }

    Ok(())
  }

  pub(crate) fn resolve_recipe<'a>(
    path: &Namepath<'src>,
    mut modules: &'a Table<'src, Justfile<'src>>,
    mut recipes: &'a Table<'src, Arc<Recipe<'src>>>,
  ) -> Option<Arc<Recipe<'src>>> {
    let (name, path) = path.split_last();

    for name in path {
      let module = modules.get(name.lexeme())?;
      modules = &module.modules;
      recipes = &module.recipes;
    }

    recipes.get(name.lexeme()).cloned()
  }
}

#[cfg(test)]
mod tests {
  use super::*;

  analysis_error! {
    name: duplicate_alias,
    input: "alias foo := bar\nalias foo := baz",
    offset: 23,
    line: 1,
    column: 6,
    width: 3,
    kind: Redefinition {
      first_type: "alias",
      second_type: "alias",
      name: "foo",
      first: 0,
    },
  }

  analysis_error! {
    name: unknown_alias_target,
    input: "alias foo := bar\n",
    offset: 6,
    line: 0,
    column: 6,
    width: 3,
    kind: UnknownAliasTarget {
      alias: "foo",
      target: Namepath::from(Name::from_identifier(
        Token{
          column: 13,
          kind: TokenKind::Identifier,
          length: 3,
          line: 0,
          offset: 13,
          path: Path::new("justfile"),
          src: "alias foo := bar\n",
        }
      ))
    },
  }

  analysis_error! {
    name: alias_shadows_recipe_before,
    input: "bar: \n  echo bar\nalias foo := bar\nfoo:\n  echo foo",
    offset: 34,
    line: 3,
    column: 0,
    width: 3,
    kind: Redefinition {
      first_type: "alias",
      second_type: "recipe",
      name: "foo",
      first: 2,
    },
  }

  analysis_error! {
    name: alias_shadows_recipe_after,
    input: "foo:\n  echo foo\nalias foo := bar\nbar:\n  echo bar",
    offset: 22,
    line: 2,
    column: 6,
    width: 3,
    kind: Redefinition {
      first_type: "recipe",
      second_type: "alias",
      name: "foo",
      first: 0,
    },
  }

  analysis_error! {
    name:   required_after_default,
    input:  "hello arg='foo' bar:",
    offset: 16,
    line:   0,
    column: 16,
    width:  3,
    kind:   RequiredParameterFollowsDefaultParameter { parameter: "bar" },
  }

  analysis_error! {
    name:   duplicate_parameter,
    input:  "a b b:",
    offset: 4,
    line:   0,
    column: 4,
    width:  1,
    kind:   DuplicateParameter { recipe: "a", parameter: "b" },
  }

  analysis_error! {
    name:   duplicate_variadic_parameter,
    input:  "a b +b:",
    offset: 5,
    line:   0,
    column: 5,
    width:  1,
    kind:   DuplicateParameter { recipe: "a", parameter: "b" },
  }

  analysis_error! {
    name:   duplicate_recipe,
    input:  "a:\nb:\na:",
    offset:  6,
    line:   2,
    column: 0,
    width:  1,
    kind:   Redefinition { first_type: "recipe", second_type: "recipe", name: "a", first: 0 },
  }

  analysis_error! {
    name:   duplicate_variable,
    input:  "a := \"0\"\na := \"0\"",
    offset: 9,
    line:   1,
    column: 0,
    width:  1,
    kind:   DuplicateVariable { variable: "a" },
  }

  analysis_error! {
    name:   extra_whitespace,
    input:  "a:\n blah\n  blarg",
    offset:  10,
    line:   2,
    column: 1,
    width:  6,
    kind:   ExtraLeadingWhitespace,
  }

  analysis_error! {
    name:   undefined_function,
    input:  "a := foo()",
    offset: 5,
    line:   0,
    column: 5,
    width:  3,
    kind:   UndefinedFunction { function: "foo" },
  }

  analysis_error! {
    name:   undefined_function_in_interpolation,
    input:  "a:\n echo {{bar()}}",
    offset: 11,
    line:   1,
    column: 8,
    width:  3,
    kind:   UndefinedFunction { function: "bar" },
  }

  analysis_error! {
    name:   undefined_function_in_default,
    input:  "a f=baz():",
    offset: 4,
    line:   0,
    column: 4,
    width:  3,
    kind:   UndefinedFunction { function: "baz" },
  }

  analysis_error! {
    name: function_argument_count_nullary,
    input: "x := arch('foo')",
    offset: 5,
    line: 0,
    column: 5,
    width: 4,
    kind: FunctionArgumentCountMismatch {
      function: "arch",
      arguments: 1,
      expected: 0..=0,
    },
  }

  analysis_error! {
    name: function_argument_count_unary,
    input: "x := env_var()",
    offset: 5,
    line: 0,
    column: 5,
    width: 7,
    kind: FunctionArgumentCountMismatch {
      function: "env_var",
      arguments: 0,
      expected: 1..=1,
    },
  }

  analysis_error! {
    name: function_argument_count_too_high_unary_opt,
    input: "x := env('foo', 'foo', 'foo')",
    offset: 5,
    line: 0,
    column: 5,
    width: 3,
    kind: FunctionArgumentCountMismatch {
      function: "env",
      arguments: 3,
      expected: 1..=2,
    },
  }

  analysis_error! {
    name: function_argument_count_too_low_unary_opt,
    input: "x := env()",
    offset: 5,
    line: 0,
    column: 5,
    width: 3,
    kind: FunctionArgumentCountMismatch {
      function: "env",
      arguments: 0,
      expected: 1..=2,
    },
  }

  analysis_error! {
    name: function_argument_count_binary,
    input: "x := env_var_or_default('foo')",
    offset: 5,
    line: 0,
    column: 5,
    width: 18,
    kind: FunctionArgumentCountMismatch {
      function: "env_var_or_default",
      arguments: 1,
      expected: 2..=2,
    },
  }

  analysis_error! {
    name: function_argument_count_binary_plus,
    input: "x := join('foo')",
    offset: 5,
    line: 0,
    column: 5,
    width: 4,
    kind: FunctionArgumentCountMismatch {
      function: "join",
      arguments: 1,
      expected: 2..=usize::MAX,
    },
  }

  analysis_error! {
    name: function_argument_count_ternary,
    input: "x := replace('foo')",
    offset: 5,
    line: 0,
    column: 5,
    width: 7,
    kind: FunctionArgumentCountMismatch {
      function: "replace",
      arguments: 1,
      expected: 3..=3,
    },
  }
}
