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

#[derive(Default)]
pub(crate) struct Analyzer<'run, 'src> {
  aliases: Table<'src, Alias<'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: BTreeSet<String>,
  warnings: Vec<Warning>,
}

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

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

    let mut stack = Vec::new();
    let ast = asts.get(&(module_path.clone(), root.to_owned())).unwrap();
    stack.push(ast);

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

      list_features.extend(&ast.list_features);

      for item in &ast.items {
        if !item.is_enabled() {
          continue;
        }

        match item {
          Item::Alias(alias) => {
            Self::define(&mut definitions, alias.name, ItemKind::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
              && imports.insert(absolute)
            {
              stack.push(asts.get(&(module_path.clone(), absolute.clone())).unwrap());
            }
          }
          Item::Module {
            absolute,
            attributes,
            doc,
            name,
            optional,
            ..
          } => {
            if let Some(absolute) = absolute {
              Self::define(&mut definitions, *name, ItemKind::Module, false)?;
              self.modules.insert(Self::analyze(
                asts,
                config,
                doc.clone(),
                &attributes.groups(),
                loaded,
                &module_path.join(name.lexeme()),
                Some(*name),
                overrides,
                paths,
                attributes.private(),
                absolute,
              )?);
              if let Some(Attribute::Doc(Some(expression))) = attributes.get(AttributeKind::Doc) {
                module_docs.push((name.lexeme(), expression.clone()));
              }
            } else if *optional {
              absent_modules.insert(name.lexeme().to_string());
            }
          }
          Item::Newline => {}
          Item::Recipe(recipe) => {
            Self::analyze_recipe(recipe)?;
            self.recipes.push(recipe);
          }
          Item::Setting(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(),
              }));
            }
          }
        }
      }

      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 }));
      }

      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 }));
      }
    }

    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: ItemKind::Function,
          second_type: ItemKind::Function,
          name,
          first: first.name.line,
        }));
      }

      let mut parameters = BTreeSet::new();
      for (parameter, _) in &function.parameters {
        if !parameters.insert(parameter.lexeme()) {
          return Err(parameter.error(DuplicateFunctionParameter {
            function: name,
            parameter: parameter.lexeme(),
          }));
        }
      }

      functions.insert(function.clone());
    }

    for ((path, name), value) in &config.overrides {
      if *path == ast.module_path
        && let Some(assignment) = assignments.get(name)
      {
        overrides.insert(assignment.number, value.clone());
      }
    }

    let (bindings, evaluation_order) =
      VariableResolver::resolve_assignments(&mut assignments, &mut functions)?;

    let variable_resolver = VariableResolver::new(&assignments, bindings, &functions, overrides);

    let mut variable_references = HashSet::new();

    for set in self.sets.values_mut() {
      for expression in set.value.expressions_mut() {
        variable_resolver.resolve_expression(
          expression,
          &ExpressionContext::new(),
          &mut variable_references,
        )?;
      }
    }

    let scope = Scope::root();

    let mut evaluator = {
      let lists = self
        .sets
        .values()
        .any(|set| matches!(set.value, Setting::Lists(true)));

      let mut collect_references = |expression| {
        variable_resolver.collect_references(
          expression,
          &ExpressionContext::new(),
          &mut variable_references,
          &mut HashSet::new(),
        );
      };

      for attribute in self.recipes.iter().flat_map(|recipe| &recipe.attributes) {
        match attribute {
          Attribute::Arg {
            help_property,
            pattern_property,
            ..
          } => {
            if let Some((_, expression)) = help_property {
              collect_references(expression);
            }
            if let Some((_, expression)) = pattern_property {
              collect_references(expression);
            }
          }
          Attribute::Doc(Some(expression)) => {
            collect_references(expression);
          }
          _ => {}
        }
      }

      for (_name, expression) in &module_docs {
        collect_references(expression);
      }

      Evaluator::evaluate_const_assignments(
        &assignments,
        &evaluation_order,
        overrides,
        &scope,
        &variable_references,
        lists,
      )?
    };

    let settings = evaluator.evaluate_sets(self.sets)?;

    if !settings.lists {
      for (feature, token) in list_features {
        if feature.function() && functions.contains_key(token.lexeme()) {
          continue;
        }

        return Err(token.error(CompileErrorKind::ListFeature(feature)));
      }
    }

    for (name, mut expression) in module_docs {
      variable_resolver.resolve_expression(
        &mut expression,
        &ExpressionContext::new(),
        &mut variable_references,
      )?;
      let value = evaluator.evaluate_value_const(&expression)?;
      self.modules.get_mut(name).unwrap().doc = if value.is_empty() {
        None
      } else {
        Some(value.join())
      };
    }

    let mut deduplicated_recipes = Table::<'src, UnresolvedRecipe<'src>>::default();
    for recipe in self.recipes {
      Self::define(
        &mut definitions,
        recipe.name,
        ItemKind::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(&settings) {
        let mut continued = false;
        for line in &recipe.body {
          let comment = !continued && settings.ignore_comments && line.is_comment();

          if !continued {
            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(GuardAndInfallibleSigil));
            }

            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 = !comment && line.is_continuation();
        }

        for attribute in [AttributeKind::Cache, AttributeKind::Extension] {
          if let Some(attribute) = recipe.attributes.get(attribute) {
            return Err(recipe.name.error(InvalidShellRecipeAttribute {
              attribute: Box::new(attribute.clone()),
              recipe: recipe.name.lexeme(),
            }));
          }
        }
      }
    }

    let (recipes, disabled_recipes) = RecipeResolver::resolve_recipes(
      &absent_modules,
      &mut evaluator,
      &ast.module_path,
      &self.modules,
      &settings,
      deduplicated_recipes,
      &variable_resolver,
    )?;

    let mut recipe_aliases = Table::new();
    let mut module_aliases = Table::new();
    let mut disabled_aliases = Table::new();
    for alias in self.aliases.into_values() {
      if let Some(resolution) =
        Resolution::resolve_module(&alias.target, &absent_modules, &self.modules)
      {
        match resolution {
          Resolution::Resolved(target) => {
            module_aliases.insert(Alias {
              attributes: alias.attributes,
              name: alias.name,
              target,
            });
          }
          Resolution::Disabled(modules) => {
            disabled_aliases.insert(Disabled {
              modules,
              name: alias.name,
            });
          }
        }
      } else if let Some(resolution) = Resolution::resolve_recipe(
        &alias.target,
        &absent_modules,
        &disabled_recipes,
        &self.modules,
        &recipes,
      ) {
        match resolution {
          Resolution::Resolved(target) => {
            recipe_aliases.insert(alias.resolve(target));
          }
          Resolution::Disabled(modules) => {
            disabled_aliases.insert(Disabled {
              modules,
              name: alias.name,
            });
          }
        }
      } else {
        return Err(alias.name.error(UnknownAliasTarget {
          alias: alias.name.lexeme(),
          target: alias.target,
        }));
      }
    }

    let mut assignment_references = HashMap::new();
    for assignment in assignments.values() {
      let mut references = HashSet::from([assignment.number]);
      if !overrides.contains_key(&assignment.number) {
        variable_resolver.collect_references(
          &assignment.value,
          &ExpressionContext::new(),
          &mut references,
          &mut HashSet::new(),
        );
      }
      assignment_references.insert(assignment.number, references);
    }

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

    let mut default = None::<Arc<Recipe>>;
    for recipe in recipes.values() {
      if recipe.attributes.contains(AttributeKind::Default) {
        if let Some(previous) = &default {
          let recipe = if previous.line_number() > recipe.line_number() {
            previous
          } else {
            recipe
          };
          return Err(recipe.name.error(CompileErrorKind::DuplicateDefault {
            recipe: recipe.name.lexeme(),
          }));
        }

        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 {
      absent_modules,
      assignment_references,
      assignments,
      default,
      disabled_aliases,
      disabled_recipes,
      doc,
      evaluation_order,
      functions,
      groups: groups.into(),
      loaded: loaded.into(),
      module_aliases,
      module_path: ast.module_path.clone(),
      modules: self.modules,
      name,
      private,
      recipe_aliases,
      recipes,
      settings,
      source,
      unexports: self.unexports,
      unstable_features,
      warnings: self.warnings,
      working_directory: ast.working_directory.clone(),
    })
  }

  fn define(
    definitions: &mut HashMap<&'src str, (ItemKind, Name<'src>)>,
    name: Name<'src>,
    second_type: ItemKind,
    duplicates_allowed: bool,
  ) -> CompileResult<'src> {
    if let Some((first_type, original)) = definitions.get(name.lexeme())
      && !(*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() && !parameter.is_option() {
        passed_default = true;
      } else if passed_default && parameter.is_required() && !parameter.is_option() {
        return Err(
          parameter
            .name
            .token
            .error(RequiredParameterFollowsDefaultParameter {
              parameter: parameter.name.lexeme(),
            }),
        );
      }
    }

    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()) {
      for &first in set.value.conflicts() {
        if let Some(conflict) = self.sets.get(first.lexeme())
          && conflict.value.conflicts().contains(&second)
        {
          return Err(set.name.error(IncompatibleSettings {
            first,
            first_line: conflict.name.line,
            second,
          }));
        }
      }
    }

    Ok(())
  }
}

#[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: ItemKind::Alias,
      second_type: ItemKind::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: ItemKind::Alias,
      second_type: ItemKind::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: ItemKind::Recipe,
      second_type: ItemKind::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: 0,
      first_type: ItemKind::Recipe,
      name: "a",
      second_type: ItemKind::Recipe,
    },
  }

  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,
    },
  }
}
