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

pub(crate) struct AssignmentResolver<'src: 'run, 'run> {
  assignments: &'run Table<'src, Assignment<'src>>,
  evaluated: BTreeSet<&'src str>,
  functions: &'run Table<'src, FunctionDefinition<'src>>,
  stack: Vec<&'src str>,
}

impl<'src: 'run, 'run> AssignmentResolver<'src, 'run> {
  pub(crate) fn resolve_assignments(
    assignments: &'run Table<'src, Assignment<'src>>,
    functions: &'run Table<'src, FunctionDefinition<'src>>,
  ) -> CompileResult<'src> {
    let mut resolver = Self {
      assignments,
      evaluated: BTreeSet::new(),
      functions,
      stack: Vec::new(),
    };

    for assignment in assignments.values() {
      resolver.resolve_assignment(assignment)?;
    }

    for function in functions.values() {
      for reference in function.body.references() {
        resolver.resolve_reference(Some(&function.parameters), reference)?;
      }
    }

    Ok(())
  }

  fn resolve_assignment(&mut self, assignment: &Assignment<'src>) -> CompileResult<'src> {
    let name = assignment.name.lexeme();

    if self.evaluated.contains(name) {
      return Ok(());
    }

    self.stack.push(name);

    for reference in assignment.value.references() {
      self.resolve_reference(None, reference)?;
    }

    self.evaluated.insert(name);

    self.stack.pop();

    Ok(())
  }

  fn resolve_reference(
    &mut self,
    parameters: Option<&[(Name<'src>, Number)]>,
    reference: Reference<'src>,
  ) -> CompileResult<'src> {
    match reference {
      Reference::Call { name, arguments } => {
        Analyzer::resolve_call(self.functions, name, arguments)
      }
      Reference::Variable(name) => self.resolve_variable(parameters, name),
    }
  }

  fn resolve_variable(
    &mut self,
    parameters: Option<&[(Name<'src>, Number)]>,
    variable: Name<'src>,
  ) -> CompileResult<'src> {
    let name = variable.lexeme();

    if let Some(parameters) = parameters {
      if parameters
        .iter()
        .any(|(parameter, _number)| parameter.lexeme() == name)
      {
        return Ok(());
      }
    }

    if self.evaluated.contains(name) || constants().contains_key(name) {
      return Ok(());
    }

    if self.stack.contains(&name) {
      self.stack.push(name);
      return Err(
        self.assignments[name]
          .name
          .error(CircularVariableDependency {
            variable: name,
            circle: self.stack.clone(),
          }),
      );
    } else if let Some(assignment) = self.assignments.get(name) {
      self.resolve_assignment(assignment)?;
    } else {
      return Err(variable.error(UndefinedVariable { variable: name }));
    }

    Ok(())
  }
}

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

  analysis_error! {
    name:   circular_variable_dependency,
    input:  "a := b\nb := a",
    offset: 0,
    line:   0,
    column: 0,
    width:  1,
    kind:   CircularVariableDependency { variable: "a", circle: vec!["a", "b", "a"] },
  }

  analysis_error! {
    name:   self_variable_dependency,
    input:  "a := a",
    offset: 0,
    line:   0,
    column: 0,
    width:  1,
    kind:   CircularVariableDependency { variable: "a", circle: vec!["a", "a"] },
  }

  analysis_error! {
    name:   unknown_expression_variable,
    input:  "x := yy",
    offset: 5,
    line:   0,
    column: 5,
    width:  2,
    kind:   UndefinedVariable { variable: "yy" },
  }

  analysis_error! {
    name:   undefined_function_parameter,
    input:  "x := env_var(yy)",
    offset: 13,
    line:   0,
    column: 13,
    width:  2,
    kind:   UndefinedVariable { variable: "yy" },
  }

  analysis_error! {
    name:   undefined_function_parameter_binary_first,
    input:  "x := env_var_or_default(yy, 'foo')",
    offset: 24,
    line:   0,
    column: 24,
    width:  2,
    kind:   UndefinedVariable { variable: "yy" },
  }

  analysis_error! {
    name:   undefined_function_parameter_binary_second,
    input:  "x := env_var_or_default('foo', yy)",
    offset: 31,
    line:   0,
    column: 31,
    width:  2,
    kind:   UndefinedVariable { variable: "yy" },
  }
}
