-- gall.tree
--
-- Git Abstraction Layer for Lua -- Tree object interface
--
-- Copyright 2012 Daniel Silverstone <dsilvers@digital-scurf.org>
--
--

---
-- Tree object interface
--
-- @module gall.tree

local ll = require "gall.ll"

local PARSED = {}

local objs = setmetatable({}, {__mode="k"})
local repos = setmetatable({}, {__mode="k"})
local parsed = setmetatable({}, {__mode="k"})

local tree_method = {}

local shapattern = ("[a-f0-9]"):rep(40)
local shacapture = "(" .. shapattern .. ")"
local modepattern = ("[0-7]"):rep(6)
local modecapture = "(" .. modepattern .. ")"
-- startmode, endmode, startsha, endsha, action, optional score, filename(s)
local treediffline = table.concat {
   ":", modecapture, " ", modecapture, " ", shacapture, " ", shacapture, " ",
   "(.)", "([0-9]*)", "\t", "([^\n]+)", "\n"
}

local mode_to_kind_map = {
   ["000000"] = "missing",
   ["160000"] = "submodule",
   ["100644"] = "blob",
   ["100755"] = "xblob",
   ["120000"] = "symlink",
}
			 
local function unescape_filename(fn)
   local zfn = fn:gsub("\\\\", "\0")
   zfn = zfn:gsub("\\n", "\n")
   zfn = zfn:gsub("\\t", "\t")
   return (zfn:gsub("%z", "\\"))
end

---
-- Representation of a tree in a Git repository.
--
-- Apart from the @{diff_to} method which will mask an entry called `diff_to`,
-- all the fields of a tree instance are the entries in the Git tree.
--
-- The values of those fields are tables with `permissions`, `name`, `type`,
-- and `obj` entries.
--
-- @type tree

---
-- Generate the delta between two trees
--
-- Treedelta entries are tables containing:
--
-- * Start mode and end mode
-- * Start kind and end kin
-- * Start sha and end sha
-- * An indication of the action
-- * An optional score of the action
-- * The filename in question
-- * If a copy or rename, src_name and dst_name too
--
-- The treedelta table is both an array of those entries and a map keyed on
-- the filename.
--
-- @function diff_to
-- @tparam tree other Another tree instance
-- @treturn[1] table The tree delta
-- @treturn[2] nil Nil on error
-- @treturn[2] string The error message

function tree_method:diff_to(other)
   -- Generate a diff from self to other
   local repo = repos[self]
   local ok, streediff, err = repo:rawgather("diff-tree", "-r", "-M", "-C",
					     ((objs[self] or {}).sha) or "DOES_NOT_EXIST",
					     ((objs[other] or {}).sha) or "DOES_NOT_EXIST")
   if ok ~= 0 then
      return nil, (streediff or "") .. "\n" .. (err or "")
   end
   local treediff = {}
   for startmode, endmode, startsha, endsha, action, score, filenames in
      streediff:gmatch(treediffline) do
      local diffentry = {
	 startmode = startmode,
	 endmode = endmode,
	 startkind = mode_to_kind_map[startmode] or "UNKNOWN",
	 endkind = mode_to_kind_map[endmode] or "UNKNOWN",
	 startsha = startsha,
	 endsha = endsha,
	 action = action,
	 score = (score ~= "") and score or nil,
      }
      if action == "C" or action == "R" then
	 local src, dst = filenames:match("([^\t]+)\t(.+)")
	 diffentry.src_name = unescape_filename(src)
	 diffentry.filename = unescape_filename(dst)
	 diffentry.dst_name = diffentry.filename
      else
	 diffentry.filename = unescape_filename(filenames)
      end
      treediff[#treediff+1] = diffentry
      treediff[diffentry.filename] = diffentry
   end
   return treediff
end

local function treeindex(tree, field)
   if tree_method[field] then
      return tree_method[field]
   end

   if not parsed[tree] then
      local raw = objs[tree].raw
      for l in raw:gmatch("([^\n]+)\n") do
	 local perm, type, sha, name = l:match("^([0-9]+) ([^ ]+) ([0-9a-f]+)\t(.+)$")
	 local t = {
	    permissions = perm,
	    name = name,
	    type = type,
	    obj = repos[tree]:get(sha)
	 }
	 rawset(tree, name, t)
      end
      parsed[tree] = true
   end

   return rawget(tree, field)
end

if ll.git2 then
   function treeindex(tree, field)
      if tree_method[field] then
	 return tree_method[field]
      end

      if not parsed[tree] then
	 local treetab = ll.git2.get_tree_table(repos[tree].git2.repo,
						objs[tree].sha)
	 for _, tab in ipairs(treetab) do
	    local perm = string.format('0x%08X', tab.perms)
	    local sha = tab.sha
	    local name = tab.name
	    local obj = repos[tree]:get(sha)
	    local type = obj.type
	    local t = {
	       permissions = perm,
	       name = name,
	       type = type,
	       obj = obj,
	    }
	    rawset(tree, name, t)
	 end
	 parsed[tree] = true
      end

      return rawget(tree, field)
   end
end

local function treetostring(tree)
   return "<GitTree(" .. tostring(objs[tree].sha) .. ") in " .. tostring(repos[tree]) .. ">"
end

local treemeta = {
   __index = treeindex,
   __tostring = treetostring,
}

--- @section end

---
-- Create a new tree instance representing the given raw object
--
-- Trees are unrealised by default, use @{realise} to cause their data to
-- be loaded into Lua if you want to iterate the tree.
--
-- @function new
-- @tparam repository repo The repository in which the tree can be found
-- @tparam object obj The raw object representing the tree
-- @treturn tree The tree instance for the given raw object

local function _new(repo, obj)
   local ret = setmetatable({}, treemeta)
   objs[ret] = obj
   repos[ret] = repo
   return ret
end

---
-- Realise a tree object
--
-- Tree objects are unrealised by default and realise themselves when you
-- first access their fields.  You can call this function up-front if you
-- want to force a tree to be realised first.
--
-- @function realise
-- @tparam tree t The tree to realise

local function _realise(t)
   if not parsed[t] then
      local ignored = t[parsed]
   end
   return t
end

---
-- Flatten a tree into a single table.
--
-- Normally a tree consists of leaf -> data mappings.  To flatten a tree
-- we walk the tree finding tree objects inside it and add those to the
-- outer tree with their names separated by '/' which is how git does things.
--
-- Use this on a top-level tree to get a table mapping all entries in that
-- tree and any trees beneath it.
--
-- @function flatten
-- @tparam tree t The tree to flatten
-- @treturn table The flattened tree

local function _flatten(t)
   local ret = {}
   local function _inner_flatten(pfx, tt)
      _realise(tt)
      for k, v in pairs(tt) do
	 local leaf = pfx .. k
	 if v.type == "tree" then
	    _inner_flatten(leaf .. "/", v.obj.content)
	 else
	    ret[leaf] = v
	 end
      end
   end
   _inner_flatten("", t)
   return ret
end

---
-- Create a tree instance representing the given flat tree
--
-- @function create
-- @tparam repository repo The repository in which to create the tree(s)
-- @tparam table flat_tree A flat tree (of the kind returned by @{flatten})
-- @treturn[1] tree The tree instance representing the top level of the tree
-- @treturn[2] nil Nil on error
-- @treturn[2] string The error message

local function _create(repo, flat_tree)
   local function __store(tree, element, content)
      local prefix, suffix = element:match("^([^/]+)/(.+)$")
      if not prefix then
	 tree[element] = content
      else
	 tree[prefix] = tree[prefix] or { [""] = true }
	 __store(tree[prefix], suffix, content)
      end
   end
   local t = {[""] = true}
   for k, v in pairs(flat_tree) do
      __store(t, k, v)
   end
   
   -- t is now a 'tree' so we need to turn any tables into trees recursively

   local function __treeify(t)
      -- Step one, ensure any trees inside t are treeified
      for k, v in pairs(t) do
	 if k ~= "" and rawget(v, "") then
	    local _v, reason = __treeify(v)
	    if not _v then
	       return nil, reason
	    end
	    t[k] = _v
	 end
      end
      -- Next, construct a mktree input
      local tree_ent = ""
      local mode = {
	 tree = "040000",
	 blob = "100644",
      }
      for k, v in pairs(t) do
	 if k ~= "" then
	    local ok, obj = pcall(function() return v.obj end)
	    if ok and obj then
	       v = obj
	    end
	    if not mode[v.type] then
	       return nil, "Unknown type " .. v.type
	    end
	    tree_ent = tree_ent .. 
	       ("%s %s %s\t%s\0"):format(mode[v.type], v.type, v.sha, k)
	 end
      end

      local why, sha = repo:_run_with_input(tree_ent, ll.chomp,
					    "mktree", "-z")
      if why ~= 0 then
	 return nil, "mktree returned " .. tostring(why)
      end
      return repo:get(sha)
   end

   return __treeify(t)
end

---
-- The SHA1 OID of the empty tree
--
-- @field empty_sha

return {
   realise = _realise,
   flatten = _flatten,
   new = _new,
   create = _create,

   -- Magic SHA1 for empty tree
   empty_sha = "4b825dc642cb6eb9a060e54bf8d69288fbee4904"
}
