module generator

import (
  std.collections
  std.fs
  std.io
  std.strings
)

let append_value(
  engine: *mut Engine,
  output: *mut std.strings.StringBuilder,
  value: Value,
): bool = 
  match value {
    .String(string) => append_text(output, string),
    .Number(number) => {
      let writer = output.(*mut dyn std.io.Writer)
      let error = std.io.wprint(writer, "{}", number)
      if error.is_error() { return fail(engine, "could not render value") }
      true
    }
    .Boolean(boolean) => {
      let writer = output.(*mut dyn std.io.Writer)
      let error = std.io.wprint(writer, "{}", boolean)
      if error.is_error() { return fail(engine, "could not render value") }
      true
    }
  }

let values_equal(left: Value, right: Value): bool =
  match left, right {
    .String(left_string), .String(right_string) => left_string.eq(right_string),
    .Number(left_number), .Number(right_number) => left_number == right_number,
    .Boolean(left_boolean), .Boolean(right_boolean) => left_boolean == right_boolean,
    _ => false,
  }

let evaluate_condition(
  engine: *mut Engine,
  expression: str,
  context: *Context,
): (bool, bool) {
  let text = expression.trim()
  let equal_at, has_equal = text.find("==")
  let not_equal_at, has_not_equal = text.find("!=")
  if has_equal && has_not_equal {
    fail(engine, "condition may contain only one comparison")
    return false, false
  }

  if has_equal || has_not_equal {
    let operator_at = if has_equal { equal_at } else { not_equal_at }
    let key = text[:operator_at].trim()
    let literal_text = text[operator_at + 2:].trim()
    if !valid_identifier(key) {
      fail(engine, "invalid conditional variable")
      return false, false
    }
    let actual, found = find_field(context.fields.as_slice(), key)
    if !found {
      fail_path(engine, "unknown conditional variable: ", key)
      return false, false
    }
    let literal, valid = parse_value(literal_text)
    if !valid {
      fail(engine, "invalid conditional literal")
      return false, false
    }
    let equal = values_equal(actual, literal)
    return if has_equal { equal } else { !equal }, true
  }

  if !valid_identifier(text) {
    fail(engine, "invalid conditional variable")
    return false, false
  }
  let value, found = find_field(context.fields.as_slice(), text)
  if !found {
    fail_path(engine, "unknown conditional variable: ", text)
    return false, false
  }
  return match value {
    .Boolean(boolean) => boolean,
    .Number(number) => number != 0,
    .String(string) => @len(string) != 0,
  }, true
}

let append_cycle(
  engine: *mut Engine,
  stack: *mut std.collections.DynamicArray(str),
  name: str,
): bool {
  _ = append_text(engine.error, "template cycle: ")
  let mut start: usz = 0
  for item, index in stack.as_slice() {
    if item.eq(name) {
      start = index
      break
    }
  }
  for index in start..stack.count {
    if index > start { _ = append_text(engine.error, " -> ") }
    _ = append_text(engine.error, stack.ptr[index])
  }
  _ = append_text(engine.error, " -> ")
  _ = append_text(engine.error, name)
  return false
}

let render_template(
  engine: *mut Engine,
  name: str,
  context: *Context,
  content: str,
  stack: *mut std.collections.DynamicArray(str),
): (std.strings.StringBuilder, bool) {
  let mut result = std.strings.StringBuilder.init(engine.allocator)
  if !template_name_valid(name) {
    fail_path(engine, "invalid template path: ", name)
    return result, false
  }
  for active in stack.as_slice() {
    if active.eq(name) {
      append_cycle(engine, stack, name)
      return result, false
    }
  }
  if !stack.append(name) {
    fail(engine, "out of memory")
    return result, false
  }

  let path, path_ok = make_path(engine, "templates", name)
  if !path_ok {
    _ = stack.pop()
    return result, false
  }
  let source, read_error = std.fs.read_file(engine.allocator, path.as_str())
  if read_error.is_error() {
    fail_path(engine, "could not read template: ", name)
    _ = stack.pop()
    return result, false
  }
  let document, parsed = parse_document(engine, source.as_str(), name)
  if !parsed {
    _ = stack.pop()
    return result, false
  }

  let mut local, copied = copy_context(engine.allocator, context)
  if !copied {
    fail(engine, "out of memory")
    _ = stack.pop()
    return result, false
  }
  for field in document.fields.as_slice() {
    if !set_field(local.&mut, field) {
      fail(engine, "out of memory")
      _ = stack.pop()
      return result, false
    }
  }

  let mut cursor: usz = 0
  let stop = render_region(
    engine,
    document.body,
    cursor.&mut,
    local.&,
    content,
    stack,
    result.&mut,
    true,
  )
  if stop != .Eof {
    if stop != .Error { fail_path(engine, "unexpected conditional terminator in ", name) }
    _ = stack.pop()
    return result, false
  }

  let parent, has_parent = find_field(document.fields.as_slice(), "template")
  if has_parent {
    match parent {
      .String(parent_name) => {
        let parent_result, parent_ok = render_template(
          engine,
          parent_name,
          local.&,
          result.as_str(),
          stack,
        )
        _ = stack.pop()
        return parent_result, parent_ok
      }
      _ => {
        fail_path(engine, "template front matter must be a string in ", name)
        _ = stack.pop()
        return result, false
      }
    }
  }

  _ = stack.pop()
  return result, true
}

let render_region(
  engine: *mut Engine,
  text: str,
  cursor: *mut usz,
  context: *Context,
  content: str,
  stack: *mut std.collections.DynamicArray(str),
  output: *mut std.strings.StringBuilder,
  emit: bool,
): Stop {
  for cursor.* < @len(text) {
    let remaining = text[cursor.*:]
    let variable_at, has_variable = remaining.find("{{")
    let tag_at, has_tag = remaining.find("{%")
    if !has_variable && !has_tag {
      if emit && !append_text(output, remaining) {
        fail(engine, "out of memory")
        return .Error
      }
      cursor.* = @len(text)
      return .Eof
    }

    let use_variable = has_variable && (!has_tag || variable_at < tag_at)
    let token_at = if use_variable { variable_at } else { tag_at }
    if emit && !append_text(output, remaining[:token_at]) {
      fail(engine, "out of memory")
      return .Error
    }
    cursor.* += token_at

    if use_variable {
      let close_at, found_close = text[cursor.* + 2:].find("}}")
      if !found_close {
        fail(engine, "unterminated variable substitution")
        return .Error
      }
      let name = text[cursor.* + 2:cursor.* + 2 + close_at].trim()
      if !valid_identifier(name) {
        fail(engine, "invalid variable substitution")
        return .Error
      }
      if emit {
        if name.eq("content") {
          if !append_text(output, content) {
            fail(engine, "out of memory")
            return .Error
          }
        } else {
          let value, found = find_field(context.fields.as_slice(), name)
          if !found {
            fail_path(engine, "unknown template variable: ", name)
            return .Error
          }
          if !append_value(engine, output, value) { return .Error }
        }
      }
      cursor.* += 2 + close_at + 2
      continue
    }

    let close_at, found_close = text[cursor.* + 2:].find("%}")
    if !found_close {
      fail(engine, "unterminated template tag")
      return .Error
    }
    let tag = text[cursor.* + 2:cursor.* + 2 + close_at].trim()
    cursor.* += 2 + close_at + 2
    if tag.eq("else") { return .Else }
    if tag.eq("endif") { return .EndIf }

    if tag.starts_with("include ") || tag.starts_with("include\t") {
      let argument = tag[7:].trim()
      let include_value, valid = parse_value(argument)
      match include_value {
        .String(include_name) if valid => {
          if emit {
            let included, ok = render_template(
              engine,
              include_name,
              context,
              content,
              stack,
            )
            if !ok { return .Error }
            if !append_text(output, included.as_str()) {
              fail(engine, "out of memory")
              return .Error
            }
            continue
          }
        }
        _ => {
          fail(engine, "include expects one quoted path")
          return .Error
        }
      }
    }

    if tag.starts_with("if ") || tag.starts_with("if\t") {
      let expression = tag[2:].trim()
      if @len(expression) == 0 {
        fail(engine, "if expects a condition")
        return .Error
      }
      let condition, valid = evaluate_condition(engine, expression, context)
      if !valid { return .Error }
      let first_stop = render_region(
        engine,
        text,
        cursor,
        context,
        content,
        stack,
        output,
        emit && condition,
      )
      if first_stop == .Error { return .Error }
      if first_stop == .Eof {
        fail(engine, "if is missing endif")
        return .Error
      }
      if first_stop == .Else {
        let second_stop = render_region(
          engine,
          text,
          cursor,
          context,
          content,
          stack,
          output,
          emit && !condition,
        )
        if second_stop == .Error { return .Error }
        if second_stop != .EndIf {
          fail(engine, "else is missing endif")
          return .Error
        }
      }
      continue
    }

    fail_path(engine, "unknown template tag: ", tag)
    return .Error
  }
  return .Eof
}