use crate::common::*;

use CompilationErrorKind::*;

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

impl<'src: 'run, 'run> AssignmentResolver<'src, 'run> {
  pub(crate) fn resolve_assignments(
    assignments: &Table<'src, Assignment<'src>>,
  ) -> CompilationResult<'src, ()> {
    let mut resolver = AssignmentResolver {
      stack: empty(),
      seen: empty(),
      evaluated: empty(),
      assignments,
    };

    for name in assignments.keys() {
      resolver.resolve_assignment(name)?;
    }

    Ok(())
  }

  fn resolve_assignment(&mut self, name: &'src str) -> CompilationResult<'src, ()> {
    if self.evaluated.contains(name) {
      return Ok(());
    }

    self.seen.insert(name);
    self.stack.push(name);

    if let Some(assignment) = self.assignments.get(name) {
      self.resolve_expression(&assignment.value)?;
      self.evaluated.insert(name);
    } else {
      let message = format!("attempted to resolve unknown assignment `{}`", name);
      let token = Token {
        src:    "",
        offset: 0,
        line:   0,
        column: 0,
        length: 0,
        kind:   TokenKind::Unspecified,
      };
      return Err(CompilationError {
        kind: Internal { message },
        token,
      });
    }
    Ok(())
  }

  fn resolve_expression(&mut self, expression: &Expression<'src>) -> CompilationResult<'src, ()> {
    match expression {
      Expression::Variable { name } => {
        let variable = name.lexeme();
        if self.evaluated.contains(variable) {
          Ok(())
        } else if self.seen.contains(variable) {
          let token = self.assignments[variable].name.token();
          self.stack.push(variable);
          Err(token.error(CircularVariableDependency {
            variable,
            circle: self.stack.clone(),
          }))
        } else if self.assignments.contains_key(variable) {
          self.resolve_assignment(variable)
        } else {
          Err(name.token().error(UndefinedVariable { variable }))
        }
      },
      Expression::Call { thunk } => match thunk {
        Thunk::Nullary { .. } => Ok(()),
        Thunk::Unary { arg, .. } => self.resolve_expression(arg),
        Thunk::Binary { args: [a, b], .. } => {
          self.resolve_expression(a)?;
          self.resolve_expression(b)
        },
      },
      Expression::Concatination { lhs, rhs } => {
        self.resolve_expression(lhs)?;
        self.resolve_expression(rhs)
      },
      Expression::Conditional {
        lhs,
        rhs,
        then,
        otherwise,
        ..
      } => {
        self.resolve_expression(lhs)?;
        self.resolve_expression(rhs)?;
        self.resolve_expression(then)?;
        self.resolve_expression(otherwise)
      },
      Expression::StringLiteral { .. } | Expression::Backtick { .. } => Ok(()),
      Expression::Group { contents } => self.resolve_expression(contents),
    }
  }
}

#[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:   unknown_function_parameter,
    input:  "x := env_var(yy)",
    offset:  13,
    line:   0,
    column: 13,
    width:  2,
    kind:   UndefinedVariable{variable: "yy"},
  }

  analysis_error! {
    name:   unknown_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:   unknown_function_parameter_binary_second,
    input:  "x := env_var_or_default('foo', yy)",
    offset:  31,
    line:   0,
    column: 31,
    width:  2,
    kind:   UndefinedVariable{variable: "yy"},
  }
}
