module generator

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

  vendor.mdhtml
)

let ensure_parent(engine: *mut Engine, path: str): bool {
  let error = std.fs.create_directory(std.fs.dirname(path), true, true)
  if error.is_error() { return fail_path(engine, "could not create output directory: ", path) }
  return true
}

let output_page_name(
  engine: *mut Engine,
  relative: str,
): (std.fs.Path, bool) {
  let extension = std.fs.extension(relative)
  if extension.eq(".html") { return join_relative(engine, "", relative) }
  if !extension.eq(".md") {
    fail_path(engine, "unsupported page extension: ", relative)
    return std.fs.Path.init(engine.allocator), false
  }
  let without_extension = relative[:@len(relative) - @len(extension)]
  let mut result = std.fs.Path.init(engine.allocator)
  if !result.push(without_extension) || !result.bytes.append_slice(".html".([]u8)) {
    fail(engine, "out of memory")
    return result, false
  }
  return result, true
}

let render_page(
  engine: *mut Engine,
  source_path: str,
  relative: str,
): bool {
  let source, read_error = std.fs.read_file(engine.allocator, source_path)
  if read_error.is_error() { return fail_path(engine, "could not read page: ", relative) }
  let document, parsed = parse_document(engine, source.as_str(), relative)
  if !parsed { return false }

  let mut body = std.strings.StringBuilder.init(engine.allocator)
  if std.fs.extension(relative).eq(".md") {
    let mut markdown = std.strings.StringBuilder.init(engine.allocator)
    if !append_text(markdown.&mut, document.body) {
      return fail(engine, "out of memory")
    }
    let reader = markdown.&mut.(*mut dyn std.io.Reader)
    let writer = body.&mut.(*mut dyn std.io.Writer)
    if !vendor.mdhtml.md_to_html(reader, writer) {
      return fail_path(engine, "could not render Markdown page: ", relative)
    }
  } else if !append_text(body.&mut, document.body) {
    return fail(engine, "out of memory")
  }

  let mut context = Context.{ fields=document.fields }
  let template, has_template = find_field(context.fields.as_slice(), "template")
  let mut rendered = body
  if has_template {
    match template {
      .String(template_name) => {
        let mut stack = std.collections.DynamicArray(str).init(engine.allocator)
        let result, ok = render_template(
          engine,
          template_name,
          context.&,
          body.as_str(),
          stack.&mut,
        )
        if !ok { return false }
        rendered = result
      }
      _ => {
        return fail_path(engine, "page template must be a string in ", relative)
      }
    }
  }

  let output_relative, output_name_ok = output_page_name(engine, relative)
  if !output_name_ok { return false }
  let output_path, output_path_ok = make_path(
    engine,
    engine.output,
    output_relative.as_str(),
  )
  if !output_path_ok || !ensure_parent(engine, output_path.as_str()) { return false }
  let write_error = std.fs.write_file(output_path.as_str(), rendered.as_str().([]u8))
  if write_error.is_error() { return fail_path(engine, "could not write page: ", output_path.as_str()) }
  return true
}

let classify_unknown(path: str): (std.fs.DirectoryEntryKind, bool) {
  let directory, directory_error = std.fs.is_directory(path)
  if directory_error.is_none() && directory { return .Directory, true }
  let file, file_error = std.fs.is_file(path)
  if file_error.is_none() && file { return .File, true }
  return .Unknown, false
}

let build_pages(engine: *mut Engine, relative_directory: str): bool {
  let directory, directory_ok = make_path(engine, "pages", relative_directory)
  if !directory_ok { return false }
  let mut iterator, open_error = std.fs.DirectoryIterator.open(directory.as_str())
  if open_error.is_error() { return fail_path(engine, "could not open pages directory: ", directory.as_str()) }
  defer iterator.close()
  let mut name = std.strings.StringBuilder.init(engine.allocator)

  for true {
    let mut kind, found, next_error = iterator.next(name.&mut)
    if next_error.is_error() { return fail_path(engine, "could not enumerate pages directory: ", directory.as_str()) }
    if !found { break }
    let relative, relative_ok = join_relative(engine, relative_directory, name.as_str())
    if !relative_ok { return false }
    let source, source_ok = make_path(engine, "pages", relative.as_str())
    if !source_ok { return false }
    if kind == .Unknown {
      let classified, known = classify_unknown(source.as_str())
      if !known { return fail_path(engine, "unknown page entry kind: ", source.as_str()) }
      kind = classified
    }
    if kind == .Directory {
      if !build_pages(engine, relative.as_str()) { return false }
    } else if kind == .File {
      if !render_page(engine, source.as_str(), relative.as_str()) { return false }
    } else {
      return fail_path(engine, "unsupported page entry: ", source.as_str())
    }
  }
  return true
}

let copy_static(engine: *mut Engine, relative_directory: str): bool {
  let directory, directory_ok = make_path(engine, "static", relative_directory)
  if !directory_ok { return false }
  let mut iterator, open_error = std.fs.DirectoryIterator.open(directory.as_str())
  if open_error.is_error() { return fail_path(engine, "could not open static directory: ", directory.as_str()) }
  defer iterator.close()
  let mut name = std.strings.StringBuilder.init(engine.allocator)

  for true {
    let mut kind, found, next_error = iterator.next(name.&mut)
    if next_error.is_error() { return fail_path(engine, "could not enumerate static directory: ", directory.as_str()) }
    if !found { break }
    let relative, relative_ok = join_relative(engine, relative_directory, name.as_str())
    if !relative_ok { return false }
    let source, source_ok = make_path(engine, "static", relative.as_str())
    if !source_ok { return false }
    if kind == .Unknown {
      let classified, known = classify_unknown(source.as_str())
      if !known { return fail_path(engine, "unknown static entry kind: ", source.as_str()) }
      kind = classified
    }
    if kind == .Directory {
      if !copy_static(engine, relative.as_str()) { return false }
    } else if kind == .File {
      let destination, destination_ok = make_path(
        engine,
        engine.output,
        relative.as_str(),
      )
      if !destination_ok || !ensure_parent(engine, destination.as_str()) { return false }
      let contents, read_error = std.fs.read_file(engine.allocator, source.as_str())
      if read_error.is_error() { return fail_path(engine, "could not read static file: ", source.as_str()) }
      let write_error = std.fs.write_file(destination.as_str(), contents.as_str().([]u8))
      if write_error.is_error() { return fail_path(engine, "could not write static file: ", destination.as_str()) }
    } else {
      return fail_path(engine, "unsupported static entry: ", source.as_str())
    }
  }
  return true
}

pub let build_site(
  root: str,
  output: str,
  error: *mut std.strings.StringBuilder,
): bool {
  error.clear()
  let mut arena = std.alloc.Arena.init()
  defer arena.deinit()
  let allocator = arena.as_allocator()
  let mut engine = Engine.{
    allocator=allocator,
    root=root,
    output=output,
    error=error,
  }
  if @len(output) == 0 {
    return fail(engine.&mut, "output directory cannot be empty")
  }

  let pages, pages_ok = make_path(engine.&mut, "pages")
  if !pages_ok { return false }
  let templates, templates_ok = make_path(engine.&mut, "templates")
  if !templates_ok { return false }
  let static, static_ok = make_path(engine.&mut, "static")
  if !static_ok { return false }
  let output_path, output_path_ok = make_path(engine.&mut, output)
  if !output_path_ok { return false }

  let pages_is_directory, pages_error = std.fs.is_directory(pages.as_str())
  if pages_error.is_error() || !pages_is_directory {
    return fail_path(engine.&mut, "missing pages directory: ", pages.as_str())
  }
  let templates_is_directory, templates_error = std.fs.is_directory(templates.as_str())
  if templates_error.is_error() || !templates_is_directory {
    return fail_path(engine.&mut, "missing templates directory: ", templates.as_str())
  }
  let static_is_directory, static_error = std.fs.is_directory(static.as_str())
  if static_error.is_error() || !static_is_directory {
    return fail_path(engine.&mut, "missing static directory: ", static.as_str())
  }
  let output_error = std.fs.create_directory(output_path.as_str(), true, true)
  if output_error.is_error() {
    return fail_path(
      engine.&mut,
      "could not create output directory: ",
      output_path.as_str(),
    )
  }

  if !build_pages(engine.&mut, "") { return false }
  return copy_static(engine.&mut, "")
}