-----------------------------------------------------------------------------
-- Defines a class for a document-to-table mapper on top of Versium.
--
--
-- (c) 2007, 2008  Yuri Takhteyev (yuri@freewisdom.org)
-- License: MIT/X, see http://sputnik.freewisdom.org/en/License
-----------------------------------------------------------------------------

module(..., package.seeall)

require("saci.node")
local Saci = {}
local Saci_mt = {__metatable={}, __index=Saci}

-----------------------------------------------------------------------------
-- Creates a new instance of Saci.
-- 
-- @param module_name    versium module name to use for storage.
-- @param versium_params parameters to use when creating the storage module
--                       instance.
-- @param root_prototype [optional] the id of the node to be used as the root
--                       prototype (defaults to "@Root").
-- @return               an instance of "Saci".
-----------------------------------------------------------------------------
function new(module_name, versium_params, root_prototype_id)
   local repo = setmetatable({}, Saci_mt)
   repo.root_prototype_id = root_prototype_id or "@Root"
   assert(module_name)
   module_name = module_name
   local versium_module = require(module_name)
   repo.versium = versium_module.new(versium_params)
   repo.permission_groups = {
      all_users     = function(user)   return true end,
      Authenticated = function(user)   return user ~= nil and user:len() > 0 end,
      Anonymous     = function(user)   return not user end,
      all_actions   = function(action) return true end,
   }
   repo:reset_cache()
   return repo
end


function Saci:reset_cache()
   self.cache = {}
   self.cache_stub = {}
end
-----------------------------------------------------------------------------
-- Returns true if the node exists and false otherwise.
-- 
-- @param id             an id of an node.
-- @return               true or false.
-----------------------------------------------------------------------------
function Saci:node_exists(id)
   return self.versium:node_exists(id)
end

--[--------------------------------------------------------------------------
-- Prepares a Lua value for serialization into a stored saci node.  This
-- function will only output boolean, numeric, and number values.
--
-- @param data           The data to be serialized
-- @return               The string representation of the data
-----------------------------------------------------------------------------
local function serialize(data)
	local data_type = type(data)
	if data_type == "boolean" or data_type == "number" then
		return tostring(data)
	elseif data_type ~= "string" then
      return string.format("nil -- Could not serialize '%s'", tostring(data))
	end

	-- if the string contains any newlines, find a version of long quotes that will work
	if data:find("\n") then
		local count = 0

      -- check to see if the string ends with a ], in which case we need
      -- to start off using ]=] to prevent issues.
      if data:find("%]$") then
         count = 1
      end

      -- generate the open/close tags
		local open = string.format("[%s[", string.rep("=", count))
		local close = string.format("]%s]", string.rep("=", count))

      -- search through the data looking for conflicts with open/close
		while data:find(open, nil, true) or data:find(close, nil, true) do
			count = count + 1
			open = string.format("[%s[", string.rep("=", count))
			close = string.format("]%s]", string.rep("=", count))
		end

		return string.format("%s%s%s", open, data, close)
	else
		return string.format("%q", data)
	end
end

-----------------------------------------------------------------------------
-- Turns a node represented as a Lua table into a string representation which
-- could later be inflated again.
--
-- @param node           A versium node as a table.
-- @return               The string representation of the versium node.
-----------------------------------------------------------------------------
function Saci:deflate(node, fields)
   local buffer = ""
   local keysort = {}

   -- Sort the keys of the node so output is consistent
   for k,v in pairs(node) do
      if k ~= "__index" then
         table.insert(keysort, k)
      end
   end
   table.sort(keysort, function(x, y)
                          if fields and fields[x] and fields[y] then
                             return (fields[x][1] or 1000) < (fields[y][1] or 1000)
                          else
                             return x < y
                          end
                       end)

   for idx,key in ipairs(keysort) do
      local value = serialize(node[key])
      local padded_key = key
      if key:len() < 15 then
         padded_key = (key.."               "):sub(1,15)
      end
      buffer = string.format("%s\n%s= %s", buffer, padded_key, value)
   end

   return buffer
end

-----------------------------------------------------------------------------
-- Retrieves data from Versium and creates a Saci node from it.  If Versium
-- returns nil then Saci will check if it has a method get_fallback_node()
-- (which must be set by the client) and will use it to retrieve a fallback
-- node if it is defined.  If not, it will just return nil.
--
-- @param id             the id of the desired node.
-- @param version        the desired version of the node (defaults to latest).
-- @return               (1) a newly created instance of saci.node.Node,
--                       (2) 'true' if the node returned is a stub (nil
--                           otherwise).
-----------------------------------------------------------------------------
function Saci:get_node(id, version)
   --assert(id)
   --assert(type(id)=="string")

   -- first, check if we have this node in cache
   local cache_key = id
   if version then
      cache_key = cache_key.."."..version
   end
   if self.cache[cache_key] then
      return self.cache[cache_key], self.cache_stub[cache_key]
   end

   -- second, check if we have it in versium
   local data = self.versium:get_node(id, version)
   if data then
      local node = self:make_node(data, id)
      self.cache[cache_key] = node
      return node
   end

   -- third, check with the node's parent
   local parent_id, rest = string.match(id, "^(.+)/(.-)$")
   if parent_id then
      local parent = self:get_node(parent_id)
      if parent then
         local node = parent:get_child(rest)
         if node then
            self.cache[cache_key] = node
            return node
         end
      end
   end

   -- no luck, check if we have a fallback function
   if self.get_fallback_node then
      local prototype = nil
      local node, stub = self:get_fallback_node(id, version)
      self.cache[cache_key] = node
      self.cache_stub[cache_key] = stub
      return node, stub
   end
end

-----------------------------------------------------------------------------
-- Returns revision information for the specified version of the node with a
-- given id, or for the latest version.
--
-- @param id             the id of the desired node
-- @param version        [optional] the id of the revision that we want to
--                       know about (defaults to latest version).
-- @return               a table with revision metadata, just like versium's
--                       get_node_info()
-----------------------------------------------------------------------------
function Saci:get_node_info(id, version)
   return self.versium:get_node_info(id, version)
end

-----------------------------------------------------------------------------
-- Creates a node from a data string.
--
-- @param data           data for the node
-- @param id             the id of the desired node
-- @return               the version tag for the latest version of the node
-----------------------------------------------------------------------------
function Saci:make_node(data, id)
   return saci.node.new{data=data, id=id, repository=self}
end

-----------------------------------------------------------------------------
-- Saves a node.
--
-- @param node           the node to be saved.
-- @param author         the .user name associated with the change (required).
-- @param comment        a comment associated with this change (optional).
-- @param extra          extra params (optional).
-- @return               nothing
-----------------------------------------------------------------------------
function Saci:save_node(node, author, comment, extra, timestamp)
   assert(node.id)
   self.versium:save_version(node.id, self:deflate(node.raw_values, node.fields),
                             author, comment, extra, timestamp)
end

-----------------------------------------------------------------------------
-- Returns the history of edits to the node, optionally filtered by time
-- prefix (e.g., "2007-12") and/or capped at a certain number.
--
-- @param id             the id of the node.
-- @param date_prefix    [optional] a date prefix (e.g., "2007-12").
-- @param limit          [optional] a maxium number of records to return.
-- @return               history as a table.
-----------------------------------------------------------------------------
function Saci:get_node_history(id, date_prefix, limit)
   assert(id)
   return self.versium:get_node_history(id, date_prefix, limit) or {}
end

function Saci:get_complete_history(id_prefix, date_prefix, limit)
   local id_prefix = id_prefix or ""
   if self.versium.get_complete_history then
      return self.versium:get_complete_history(id_prefix, date_prefix, limit)
   end

   local edits = {}
   local preflen = id_prefix:len()
   local ids = self.versium:get_node_ids(id_prefix or nil, limit or nil)
   for i, id in ipairs(ids) do
      if id:sub(1, preflen) == id_prefix then
         for i, edit in ipairs(self:get_node_history(id, date_prefix, limit)) do
            edit.id = id
            table.insert(edits, edit)
         end
      end
   end
   table.sort(edits, function(e1, e2) return e1.timestamp > e2.timestamp end)
   if limit then
      local another_table = {}
      for i=1,limit do
         table.insert(another_table, edits[i])
      end
      edits = another_table
   end   
   return edits
end



-----------------------------------------------------------------------------
-- Retrieves multiple data nodes from Versium and returns Saci nodes created
-- from them.  If Versium returns an empty table, then Saci will also return
-- an empty table.  This function always returns the most recent version of 
-- each node
--
-- @param prefix         the desired node prefix
-- @return               a table containing the returned Saci nodes, indexed
--                       by node name.
-----------------------------------------------------------------------------
function Saci:get_nodes_by_prefix(prefix, limit, visible, id_filter)
   local versium_nodes = self:get_versium_nodes_by_prefix(prefix, limit)
   local nodes = {}
   local ids = {}
   local cache = self.cache
   local num_hidden = 0
   for id, vnode in pairs(versium_nodes) do
      if (not id_filter) or id_filter(id) then
         local node
         if cache[id] then
            node = cache[id]
         else
            node = self:make_node(vnode, id) 
            cache[id] = node
         end
         if visible then
            if node:check_permissions(user, "show") then
               nodes[id] = node
               ids[#ids+1] = id
            else
               num_hidden = num_hidden + 1
            end
         else
            nodes[id] = node
            ids[#ids+1] = id            
         end
      end
   end
   table.sort(ids)
   return nodes, ids, num_hidden
end

-----------------------------------------------------------------------------
-- Retrieves multiple data nodes from Versium, and returns them _without_
-- creating Saci nodes from them.
--
-- @param prefix         the desired node prefix
-- @return               a table containing the returned versium nodes,
--                       indexed by node name.
-----------------------------------------------------------------------------
function Saci:get_versium_nodes_by_prefix(prefix, limit)
   -- Get the nodes, either all at once, of one by one   
   if self.versium.get_nodes_by_prefix then
      return self.versium:get_nodes_by_prefix(prefix, limit)
   else
      local versium_nodes = {}
      for i, id in ipairs(self.versium:get_node_ids(prefix, limit)) do
         versium_nodes[id] = self.versium:get_node(id)
      end
      return versium_nodes
   end
end


-----------------------------------------------------------------------------
-- Returns a list of Saci nodes with fields matching the query.
--
-- @param query          a query as a string, with implicit "or" and with
--                       negatable terms, e.g., "lua git -storage"
-- @param fields         a list of fields to search.
-- @param prefix         a prefix within which to search.
-----------------------------------------------------------------------------
function Saci:find_nodes(fields, patterns, prefix)
   assert(fields)
   assert(patterns)
   if type(fields) == "string" then fields = {fields} end
   if type(patterns) == "string" then patterns = {patterns} end
   local nodes = {}
   -- find nodes matching the patterns
   local basic_match, matched, node, value

   local function basic_match(vnode) -- check if the pattern matches raw node
      for _, pattern in ipairs(patterns) do
         if vnode:lower():match(pattern) then return true end
      end
      return false
   end

   for id, vnode in pairs(self:get_versium_nodes_by_prefix(prefix)) do
      if basic_match(vnode) then -- ok, the pattern is somewhere there, let's look
         node = self:make_node(vnode, id)
         if node:multimatch(fields, patterns) then
            table.insert(nodes, node)
         end
      end
   end

   return nodes
end      

function Saci:query_nodes(fields, query, prefix)   
   query = " "..query.." "
   fields = fields or {"content"}
   local positive_terms = {}
   local negative_terms = {}
   for term in query:gmatch("%S+") do
      if term:match("%:") then
         local key, value = term:match("^(%w+):(.+)")
         if key == "prefix" then
            prefix=value
         end
      elseif term:sub(1,1)=="-" then
         table.insert(negative_terms, "%W("..term:sub(2)..")%W")
      else
         table.insert(positive_terms, "%W("..term..")%W")
      end
   end

   local nodes = self:find_nodes(fields, positive_terms, prefix)
   local nodes_without_negatives = {}
   for i, node in ipairs(nodes) do
      if not node:multimatch(fields, negative_terms) then
         table.insert(nodes_without_negatives, node)
      end
   end
   return nodes_without_negatives
end


-- vim:ts=3 ss=3 sw=3 expandtab
