// Portions adapted from google/go-jsonnet v0.21.0 (Apache-2.0).
// https://github.com/google/go-jsonnet/tree/67968688d9952f50662e979fab1bef889d0d3e57
// Source: internal/formatter/fix_indentation.go
// Copyright 2019 Google Inc. All rights reserved.
// Modifications: Ported indentation rules to bounded local syntax traversal.
// License and retained attribution: LICENSE and NOTICE at the module root.

// Expression indentation follows the pinned Go formatter's base/lineUp model.
// Copyright Google Inc.; Apache-2.0; see NOTICE.

///|
priv struct FormatIndentValue {
  anchor : Int
  levels : Int
} derive(Eq)

///|
priv struct FormatIndentContext {
  base : FormatIndentValue
  line_up : FormatIndentValue
} derive(Eq)

///|
priv struct FormatIndentPlan {
  base : Array[FormatIndentValue]
  line_up : Array[FormatIndentValue]
}

///|
fn FormatIndentValue::resolve(
  self : FormatIndentValue,
  columns : Array[Int],
  indent : Int,
  maximum : Int,
) -> Int {
  // Slice closing fodder is intentionally left untouched by fixed Go.
  if self.anchor == -2 {
    return self.levels
  }
  let anchor = if self.anchor >= 0 { columns[self.anchor] } else { 0 }
  if anchor > maximum || self.levels > (maximum - anchor) / indent {
    maximum
  } else {
    anchor + self.levels * indent
  }
}

///|
fn format_grouping_paren(
  tokens : Array[Token],
  layout : FormatLayout,
  open : Int,
) -> Bool {
  if tokens[open].text != "(" || layout.parameter[open] {
    return false
  }
  if open == 0 {
    return true
  }
  let previous = tokens[open - 1]
  !(previous.value is Some(_) ||
  is_name(previous.text) ||
  previous.text == "]" ||
  previous.text == "}" ||
  previous.text == "self" ||
  previous.text == "super" ||
  previous.text == "$" ||
  (previous.text == ")" && !layout.parameter[layout.close[open - 1]]))
}

///|
fn format_indent_plan(
  tokens : Array[Token],
  root : Expr,
  layout : FormatLayout,
  writer : FormatOutput,
) -> FormatIndentPlan raise JsonnetError {
  let zero : FormatIndentValue = { anchor: -1, levels: 0, }
  let plan : FormatIndentPlan = {
    base: Array::make(tokens.length(), zero),
    line_up: Array::make(tokens.length(), zero),
  }
  let positions : Map[(Int, Int), Int] = Map([])
  let ends : Map[(Int, Int), Int] = Map([])
  for i, token in tokens {
    writer.tick()
    positions[(token.loc.line, token.loc.column)] = i
    ends[(token.end.line, token.end.column)] = i + 1
  }
  let start = fn(e : Expr) {
    let mut first = positions.get((e.loc.line, e.loc.column)).unwrap_or(0)
    let mut end = match e.end {
      Some(end) => ends.get((end.line, end.column)).unwrap_or(first + 1)
      None => first + 1
    }
    while first > 0 && format_grouping_paren(tokens, layout, first - 1) {
      let close = layout.close[first - 1]
      if close != end && close != end - 1 {
        break
      }
      first -= 1
      end = close + 1
    }
    first
  }
  let finish = fn(e : Expr) {
    let mut current = e
    while current.end is None {
      match current.kind {
        Func(_, body) | Assert(_, _, body) => current = body
        _ => break
      }
    }
    match current.end {
      Some(end) =>
        ends.get((end.line, end.column)).unwrap_or(start(current) + 1)
      None => start(current) + 1
    }
  }
  let on_new_line = fn(i : Int) {
    if layout.newline[i] {
      true
    } else {
      let mut result = false
      for trivia in tokens[i].leading {
        match trivia.kind {
          Whitespace =>
            if trivia.raw.contains("\n") {
              result = true
              break
            }
          LineComment => {
            result = true
            break
          }
          BlockComment => {
            result = trivia.raw.contains("\n")
            break
          }
          TokenText => ()
        }
      }
      result
    }
  }
  let child_at = fn(
    i : Int,
    old : FormatIndentContext,
    strong : Bool,
  ) -> FormatIndentContext {
    if on_new_line(i) {
      let next = { ..old.base, levels: old.base.levels + 1, }
      { base: next, line_up: next, }
    } else {
      let line_up : FormatIndentValue = { anchor: i, levels: 0, }
      { base: if strong { line_up } else { old.base }, line_up, }
    }
  }
  let stack : Array[(Expr, FormatIndentContext, Bool)] = [
    (root, { base: zero, line_up: zero, }, true),
  ]
  while stack.pop() is Some((e, context, replace)) {
    writer.tick()
    let first = positions.get((e.loc.line, e.loc.column)).unwrap_or(0)
    let end = finish(e)
    if replace {
      for i in start(e)..
            push_within(value, child_at(start(value), next, false), next)
        }
      }
    }
    match e.kind {
      Local(bindings, body) => {
        let name = if bindings[0].range is Some(range) {
          positions
          .get((range.begin.line, range.begin.column))
          .unwrap_or(first + 1)
        } else {
          first + 1
        }
        let names = bindings.map(binding => {
          match binding.range {
            Some(range) =>
              positions
              .get((range.begin.line, range.begin.column))
              .unwrap_or(name)
            None => start(binding.value)
          }
        })
        if names.any(i => tokens[i].has_newline()) {
          for i in names[1:] {
            writer.tick()
            layout.newline[i] = true
          }
        }
        let next = child_at(name, context, false)
        for i in name.. {
        let mut parameter_context = context
        for open in first.. {
        push(condition, new_child(condition))
        push(yes, new_child(yes))
        if no.end is Some(_) {
          push(no, new_child(no))
        }
        // Branch markers reset to the parent base, including an inline else-if.
        for i in finish(condition).. {
        let mut strong = false
        for i in finish(left).. {
        let next = child_at(first + 1, context, false)
        for i in (first + 1)..= 0 {
          plan.line_up[layout.close[first]] = context.base
        }
      }
      Obj(fields, bindings, assertions, _) => {
        let next = child_at(first + 1, context, false)
        for i in (first + 1)..= 0 {
          let close = layout.close[first]
          plan.base[close] = context.base
          plan.line_up[close] = context.base
        }
      }
      ArrayComp(body, cs) => {
        let next = child_at(first + 1, context, false)
        for i in (first + 1)..= 0 {
          plan.line_up[layout.close[first]] = context.base
        }
      }
      ObjectComp(key, body, cs, bindings) => {
        let next = child_at(first + 1, context, false)
        for i in (first + 1)..= 0 {
          plan.line_up[layout.close[first]] = context.base
        }
      }
      Call(target, arguments, _) => {
        visit(target)
        let open = finish(target)
        if tokens.get(open) is Some({ text: "(", .. }) {
          let strong = arguments.length() > 1 &&
            arguments[1:].any(arg => tokens[start(arg.1)].has_newline())
          let next = child_at(open + 1, context, strong)
          let close = layout.close[open]
          for i in (open + 1)..= 0 {
            plan.line_up[close] = context.base
          }
        } else {
          for (_, value) in arguments {
            push(value, new_child(value))
          }
        }
      }
      Index(target, index) => {
        visit(target)
        if index.end is None && tokens[start(index)].text == "." {
          let name = start(index) + 1
          let next = child_at(name, context, false)
          plan.base[name] = next.base
          plan.line_up[name] = next.line_up
        } else {
          for open in finish(target).. {
        visit(target)
        for open in finish(target).. {
        let next = new_child(condition)
        push(condition, next)
        if message is Some(value) {
          push(value, next)
        }
        visit(body)
      }
      Unary(_, value) | Fail(value) => push(value, new_child(value))
      ImportFile(_, _) | ImportBinary(_) =>
        if first + 1 < end {
          let next = child_at(first + 1, context, false)
          plan.base[first + 1] = next.base
          plan.line_up[first + 1] = next.line_up
        }
      Literal(_)
      | NumberLiteral(_)
      | Var(_)
      | StandardFunction(_)
      | StandardValue(_) => ()
    }
  }
  // The semantic tree intentionally removes grouping parentheses. Restore
  // their strong base before resolving output-column references.
  for open, token in tokens {
    writer.tick()
    if token.text != "(" || !format_grouping_paren(tokens, layout, open) {
      continue
    }
    let close = layout.close[open]
    let old = plan.base[open]
    let next = if on_new_line(open + 1) {
      { ..old, levels: old.levels + 1, }
    } else {
      { anchor: open + 1, levels: 0, }
    }
    let translate = fn(value : FormatIndentValue) {
      if value.anchor == -2 {
        value
      } else if value.anchor <= open || value.anchor > close {
        {
          anchor: next.anchor,
          levels: next.levels +
          value.levels -
          (if value.anchor == old.anchor { old.levels } else { 0 }),
        }
      } else {
        value
      }
    }
    for i in (open + 1)..