module(..., package.seeall)

local depgraph = require "tundra.depgraph"
local util     = require "tundra.util"
local scanner  = require "tundra.scanner"
local dirwalk  = require "tundra.dirwalk"
local platform = require "tundra.platform"
local native   = require "tundra.native"
local njson    = require "tundra.native.json"
local path     = require "tundra.path"

local dag_dag_magic = 0x15890105

local function get_passes(nodes)
  local result = {}
  local seen_passes = {}

  for _, node in ipairs(nodes) do
    local p = node.pass
    if not seen_passes[p] then
      assert(type(p) == "table", "Passes must be tables, have " .. util.tostring(p))
      assert(type(p.BuildOrder) == "number", "Pass BuildOrder must be a number")
      result[#result + 1] = p
      seen_passes[p] = true
    end
  end

  table.sort(result, function (a, b) return a.BuildOrder < b.BuildOrder end)

  local pass_lookup = {}

  for index, pass in ipairs(result) do
    pass_lookup[pass] = index - 1
  end

  return result, pass_lookup
end

local function setup_input_deps(nodes)
  local producers = {}

  local cwd = native.getcwd() .. SEP
  local filter
  if native.host_platform == 'windows' or native.host_platform == 'macosx' then
    filter = function (str) return str:lower() end
  else
    filter = function (str) return str end
  end

  local node_deps = {}

  -- Record producing node for all output files
  for _, n in ipairs(nodes) do
    for _, output in util.nil_ipairs(n.outputs) do
      if not path.is_absolute(output) then
        output = cwd .. output
      end
      output = filter(output)
      if producers[output] then
        errorf("file %s set to be written by more than one target:\n%s\n%s\n",
          output, n.annotation, producers[output].annotation)
      end
      producers[output] = n
    end
    if n.deps then
      node_deps[n] = util.make_lookup_table(n.deps)
    end
  end

  -- Map input files to dependencies
  for _, n in ipairs(nodes) do
    for _, inputf in util.nil_ipairs(n.inputs) do
      if not path.is_absolute(inputf) then
        inputf = cwd .. inputf
      end
      inputf = filter(inputf)
      local producer = producers[inputf]
      local deps_lut = node_deps[n]
      if producer and (not deps_lut or not deps_lut[producer]) then
        n.deps[#n.deps + 1] = producer
        if not deps_lut then
          deps_lut = {}
          node_deps[n] = deps_lut
        end
        deps_lut[producer] = true
      end
    end
  end

end

local function get_scanners(nodes)
  local scanners = {}
  local scanner_to_index = {}
  for _, node in ipairs(nodes) do
    local scanner = node.scanner
    if scanner and not scanner_to_index[scanner] then
      scanner_to_index[scanner] = #scanners
      scanners[#scanners + 1] = scanner
    end
  end
  return scanners, scanner_to_index
end

local function save_passes(w, passes)
  w:begin_array("Passes")
  for _, s in ipairs(passes) do
    w:write_string(s.Name)
  end
  w:end_array()
end

local function save_scanners(w, scanners)
  w:begin_array("Scanners")
  for _, s in ipairs(scanners) do
    w:begin_object()
    w:write_string(s.Kind, 'Kind')
    w:begin_array("IncludePaths")
    for _, path in util.nil_ipairs(s.Paths) do
      w:write_string(path)
    end
    w:end_array()
    -- Serialize specialized state for generic scanners
    if s.Kind == 'generic' then
      w:write_bool(s.RequireWhitespace, 'RequireWhitespace')
      w:write_bool(s.UseSeparators, 'UseSeparators')
      w:write_bool(s.BareMeansSystem, 'BareMeansSystem')
      w:begin_array('Keywords')
      for _, kw in util.nil_ipairs(s.Keywords) do
        w:write_string(kw)
      end
      w:end_array()
      w:begin_array('KeywordsNoFollow')
      for _, kw in util.nil_ipairs(s.KeywordsNoFollow) do
        w:write_string(kw)
      end
      w:end_array()
    end
    w:end_object()
  end
  w:end_array()
end

local function save_nodes(w, nodes, pass_to_index, scanner_to_index)
  w:begin_array("Nodes")
  for idx, node in ipairs(nodes) do
    w:begin_object()
    assert(idx - 1 == node.index)
    if node.action then
      w:write_string(node.action, "Action")
    end
    if node.preaction then
      w:write_string(node.preaction, "PreAction")
    end
    w:write_string(node.annotation, "Annotation")

    w:write_number(pass_to_index[node.pass], "PassIndex")

    if #node.deps > 0 then
      w:begin_array("Deps")
      for _, dep in ipairs(node.deps) do
        w:write_number(dep.index)
      end
      w:end_array()
    end

    local function dump_file_list(list, name)
      if list and #list > 0 then
        w:begin_array(name)
        for _, fn in ipairs(list) do
          w:write_string(fn)
        end
        w:end_array(name)
      end
    end

    dump_file_list(node.inputs, "Inputs")
    dump_file_list(node.outputs, "Outputs")
    dump_file_list(node.aux_outputs, "AuxOutputs")

    -- Save environment strings
    local env_count = 0
    for k, v in util.nil_pairs(node.env) do
      env_count = env_count + 1
    end

    if env_count > 0 then
      w:begin_array("Env")
      for k, v in pairs(node.env) do
        w:begin_object()
        w:write_string(k, "Key")
        w:write_string(v, "Value")
        w:end_object()
      end
      w:end_array()
    end

    if node.scanner then
      w:write_number(scanner_to_index[node.scanner], "ScannerIndex")
    end

    if node.overwrite_outputs then
      w:write_bool(true, "OverwriteOutputs")
    end

    if node.is_precious then
      w:write_bool(true, "PreciousOutputs")
    end

    if node.expensive then
      w:write_bool(true, "Expensive")
    end

    w:end_object()
  end
  w:end_array()
end

local function save_configs(w, bindings, default_variant, default_subvariant)
  local configs = {}
  local variants = {}
  local subvariants = {}
  local config_index = {}
  local variant_index = {}
  local subvariant_index = {}
  local default_config = nil
  local host_platform = platform.host_platform() 

  for _, b in ipairs(bindings) do
    if not configs[b.Config.Name] then
      configs[b.Config.Name]        = #config_index
      config_index[#config_index+1] = b.Config.Name
    end
    if not variants[b.Variant.Name] then
      variants[b.Variant.Name]        = #variant_index
      variant_index[#variant_index+1] = b.Variant.Name
    end
    if not subvariants[b.SubVariant] then
      subvariants[b.SubVariant]             = #subvariant_index
      subvariant_index[#subvariant_index+1] = b.SubVariant
    end

    if b.Config.DefaultOnHost == host_platform then
      default_config = b.Config
    end
  end

  assert(#config_index > 0)
  assert(#variant_index > 0)
  assert(#subvariant_index > 0)

  local function dump_str_array(array, name)
    if array and #array > 0 then
      w:begin_array(name)
      for _, name in ipairs(array) do
        w:write_string(name)
      end
      w:end_array()
    end
  end

  w:begin_object("Setup")
  dump_str_array(config_index, "Configs")
  dump_str_array(variant_index, "Variants")
  dump_str_array(subvariant_index, "SubVariants")

  w:begin_array("BuildTuples")
  for index, binding in ipairs(bindings) do
    w:begin_object()
    w:write_number(configs[binding.Config.Name], "ConfigIndex")
    w:write_number(variants[binding.Variant.Name], "VariantIndex")
    w:write_number(subvariants[binding.SubVariant], "SubVariantIndex")
    local function store_node_index_array(nodes, name)
      w:begin_array(name)
      for _, node in util.nil_ipairs(nodes) do
        w:write_number(node.index)
      end
      w:end_array()
    end
    store_node_index_array(binding.AlwaysNodes, "AlwaysNodes")
    store_node_index_array(binding.DefaultNodes, "DefaultNodes")

    w:begin_object("NamedNodes")
    for name, node in pairs(binding.NamedNodes) do
      w:write_number(node.index, name)
    end
    w:end_object()

    w:end_object()
  end
  w:end_array()

  -- m_DefaultBuildTuple
  w:begin_object("DefaultBuildTuple")
  if default_config then
    w:write_number(configs[default_config.Name], "ConfigIndex")
  else
    w:write_number(-1, "ConfigIndex")
  end

  if default_variant then
    w:write_number(variants[default_variant.Name], "VariantIndex")
  else
    w:write_number(-1, "VariantIndex")
  end

  if default_subvariant then
    w:write_number(subvariants[default_subvariant], "SubVariantIndex")
  else
    w:write_number(-1, "SubVariantIndex")
  end
  w:end_object()

  w:end_object()

end

local function save_signatures(w, accessed_lua_files)
  w:begin_array("FileSignatures")
  for _, fn in ipairs(accessed_lua_files) do
    w:begin_object()
    w:write_string(fn, "File")
    w:end_object()
  end
  w:end_array()

  w:begin_array("GlobSignatures")
  local globs = dirwalk.all_queries()
  for _, glob in ipairs(globs) do
    w:begin_object()
    w:write_string(glob.Path, "Path")
    w:end_object()
  end
  w:end_array()
end

local function check_deps(nodes)
  for _, node in ipairs(nodes) do
    for _ , dep in ipairs(node.deps) do
      if dep.pass.BuildOrder > node.pass.BuildOrder then
        errorf("%s (pass: %s) depends on %s in later pass (%s)", node.annotation, node.pass.Name, dep.annotation, dep.pass.Name)
      end
    end
  end
end

function save_dag_data(bindings, default_variant, default_subvariant, content_digest_exts, misc_options, json_file)

  -- Call builtin function to get at accessed file table
  local accessed_lua_files = util.table_keys(get_accessed_files())

  misc_options = misc_options or {}
  local max_expensive_jobs = misc_options.MaxExpensiveJobs or -1

  printf("save_dag_data: %d bindings, %d accessed files", #bindings, #accessed_lua_files)

  local nodes = depgraph.get_all_nodes()

  -- Set node indices
  for idx, node in ipairs(nodes) do
    node.index = idx - 1
  end

  -- Set up array of passes
  local passes, pass_to_index = get_passes(nodes)

  -- Hook up dependencies due to input files
  setup_input_deps(nodes)

  check_deps(nodes)

  -- Find scanners
  local scanners, scanner_to_index = get_scanners(nodes)

  local w = njson.new(json_file)

  w:begin_object()
  save_configs(w, bindings, default_variant, default_subvariant)
  save_passes(w, passes)
  save_scanners(w, scanners)
  save_nodes(w, nodes, pass_to_index, scanner_to_index)
  save_signatures(w, accessed_lua_files)

  if content_digest_exts and #content_digest_exts > 0 then
    w:begin_array("ContentDigestExtensions")
    for _, ext in ipairs(content_digest_exts) do
      w:write_string(ext)
    end
    w:end_array()
  end

  w:write_number(max_expensive_jobs, "MaxExpensiveCount")

  w:end_object()

  w:close()
end

