-----------------------------------------------------------------------------
-- Creates a representation of a single data "node" with support for
-- inheritance and field activation.
--
-- (c) 2007, 2008  Yuri Takhteyev (yuri@freewisdom.org)
-- License: MIT/X, see http://sputnik.freewisdom.org/en/License
-----------------------------------------------------------------------------

module(..., package.seeall)
require("saci.sandbox")
require("diff")

--x--------------------------------------------------------------------------
-- A table of functions used for "activating" fields, that is turning them
-- from strings into tables or functions.
-----------------------------------------------------------------------------
local Activators = {}

--x--------------------------------------------------------------------------
-- Turns Lua code into a table of values defined by that code.
--
-- @param value          Lua code as string.
-- @param repo           the Saci repository.
-- @return               the environment created by running the code.
-----------------------------------------------------------------------------

Activators.lua = function(value, repo)
   local sandbox = saci.sandbox.new(repo.sandbox_values)
   sandbox.logger = repo.logger
   return sandbox:do_lua(value)
end

--x--------------------------------------------------------------------------
-- Turns a list of tokens (e.g. node IDs) represented as one token per line
-- into a table of tokens.
--
-- @param value          a list of tokens as a \n-delimited string.
-- @param repo           The Saci repository.
-- @return               a table of tokens.
-----------------------------------------------------------------------------

Activators.list = function(value, repo)
   local nodes = {}
   for line in (value or ""):gmatch("[^%s]+") do
      table.insert(nodes, line)
   end
   return nodes
end



local Node = {}
local Node_mt = {
   __index = function(t,key)
                return t.active_values[key] or t.inherited_values[key]
                       or t.raw_values[key] or Node[key]
   end
}

-----------------------------------------------------------------------------
-- Creates a new instance of Node.  This is the only function this module 
-- exposes and the only one you should be using directly.  The instance that
-- this function returns has methods that can then be used to manipulate the
-- node.
-- 
-- @param args           a table arguments, including the following fields:
--                       args.data (the raw data for the node, required),
--                       args.id (the id of the node, required),
--                       args.repository (the saci instance, required)
--
-- @repository           the repository to which this Node belongs.
-- @return               an instance of "SputnikRepository".
-----------------------------------------------------------------------------
function new(args)
   local node = setmetatable({raw_values={}, inherited_values={}, active_values={}}, Node_mt)
   --assert(args.data)
   --assert(args.id)
   node.data = args.data
   node.id = args.id
   --assert(args.repository)
   node.repository = args.repository
   node.saci = node.repository
   node.raw_values = saci.sandbox.new():do_lua(args.data)
   --assert(rawget(node, "raw_values"), "the sandbox should give us a table")
   node:apply_inheritance()
   node:activate()
   return node
end

---------------------------------------------------------------------------------------------------
-- Returns the edits to the node.
--
-- @param prefix         an optional date prefix (e.g., "2007-12").
-- @return               edits as a table.
---------------------------------------------------------------------------------------------------
function Node:get_history(prefix)
   return self.repository:get_node_history(self.id, prefix)
end
  
---------------------------------------------------------------------------------------------------
-- Returns the node as a string (used for debugging).
---------------------------------------------------------------------------------------------------
function Node:tostring()
   local buf = "================= "..self._id.." ========\n"
   for field, fieldinfo in pairs(self.fields) do
      buf = buf.."~~~~~~~ "..field.." ("..(fieldinfo.proto or "")..") ~~~~\n"
      buf = buf..(self._inactive[field] or "")
      buf = buf.."\n"
   end
   buf = buf.."~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n"
   return buf
end

-----------------------------------------------------------------------------
-- Inheritance rules: functions that determine what the node's value should
-- be based on it's prototype's value and it's own "raw" value.
-----------------------------------------------------------------------------

local inheritance_rules = {}

-- Concatenates the inherited value and own value, inserting an "\n" between
-- them.  Basically, this is what we need to be able to concatenate two
-- chunks of Lua code.
function inheritance_rules.concat(proto_value, own_value)
   local buf = ""
   if proto_value and proto_value:len() > 0 then
      buf = proto_value
   end
   if own_value and own_value:len() > 0 then
      if buf:len() > 0 then
         buf = buf.."\n"..own_value
      else
         buf = own_value
      end
   end
   return buf
end

-- A simpler inheritance rule that only uses the prototype's value if own
-- value is not defined.
function inheritance_rules.fallback(proto_value, own_value)
   if own_value and own_value~="" then
      return own_value
   else
      return proto_value
   end
end

inheritance_rules.default = inheritance_rules.fallback -- set a default

-----------------------------------------------------------------------------
-- Applies inheritance form page's prototype.  The inherited values are
-- stored in self.inherited_values.
-----------------------------------------------------------------------------
function Node:apply_inheritance()

   --assert(self.id)
   -- If this node is itself the root prototype, then there is nothing else
   -- to do.
   if self.id == self.repository.root_prototype_id then
      self.inherited_values = self.raw_values
      return
   end
   if self.raw_values.prototype == "" then
      self.raw_values.prototype = nil  -- to make it easier to test for it
   end
   local prototype_id = self.raw_values.prototype or self.repository.root_prototype_id

   -- Get the values for the prototype.
   local proto_values = self.repository:get_node(prototype_id).inherited_values
   assert(proto_values.fields, "The prototype node must define fields")
 
   -- Apply inheritance from the prototype, using the information in the
   -- 'fields' field to decide how to handle each field.  

   -- First, we need to figure out what those fields are.  We use
   -- this node's own "fields" table rather than the fields table from the
   -- prototype and the value for fields must _always_ be inherited as a 
   -- matter of bootstrapping.

   local tmp_fields = inheritance_rules.concat(proto_values.fields,
                                               self.raw_values.fields)
   assert(tmp_fields)
   local fields, err = saci.sandbox.new{}:do_lua(tmp_fields)
   assert(fields, err)

   -- Now do the actual inheritance.  This means going through all fields
   -- and applying each of them the "inheritance rule" specified by the
   -- "proto" attribute.
   for field_name, field in pairs(fields) do
      field.name = field_name
      if field.proto then
         local inheritance_fn = inheritance_rules[field.proto]
                                or inheritance_rules.default
         self.inherited_values[field.name] = inheritance_fn(
                                                proto_values[field.name], 
                                                self.raw_values[field.name])   
      end
   end
end


---------------------------------------------------------------------------------------------------
-- Turns string parameters into Lua functions and tables, making them callable.
---------------------------------------------------------------------------------------------------
function Node:activate()
   self.active_values = {}
   local fields, err = saci.sandbox.new{}:do_lua(self.inherited_values.fields)
   if not fields then
      error(err)
   end

   for field, fieldinfo in pairs(fields) do
      if fieldinfo.activate then
         local activator_fn = Activators[fieldinfo.activate]
         local value = self[field] or ""
         self.active_values[field] = activator_fn(value, self.repository)
      end
   end

   return self
end

---------------------------------------------------------------------------------------------------
-- Updates the node with values.
--
-- @param new_values     a table of new values (keyed by field name).
-- @param fields         a table keyed by field name to allow us to filter the new values.
-- @return               nothing.
---------------------------------------------------------------------------------------------------
function Node:update(new_values, fields)
   assert(new_values)
   --assert(fields)
   -- First, update the raw values the new values (only those that are listed in fields!)
   for key, value in pairs(new_values) do
      if (not fields) or (fields[key] and not fields[key].virtual) then
         self.raw_values[key] = value
      end
   end
   self:apply_inheritance()
   self:activate()

   -- Now make a new node, being careful to not get into recursive metatables
   --local vnode = self._vnode
   --setmetatable(self._inactive, {}) -- to avoid recursive metatables
   --local new_smart_node = new(vnode, self.repository, self.repository.config.ROOT_PROTOTYPE)
   -- Now make the current node a copy of the new one (copy the fields and the metatable
   --for k,v in pairs(new_smart_node) do
   --   self[k] = v
   --end
   --setmetatable(self, getmetatable(new_smart_node))
end

---------------------------------------------------------------------------------------------------
-- Returns a diff between this version of the node and some other one.
--
-- @param another_node   some other node
-- @return               diff as a table of tokens.
---------------------------------------------------------------------------------------------------
function Node:diff(another_node)
   local difftab  = {}
   for i, field in ipairs(self:get_ordered_field_names()) do
      if (self.raw_values[field] or "") ~= (another_node.raw_values[field] or "") then
         difftab[field] = diff.diff(tostring(another_node.raw_values[field]),
                                    tostring(self.raw_values[field]))
      end
   end
   return difftab
end

-----------------------------------------------------------------------------
-- Returns the list of fields for this node, ordered according to their
-- weights.
-- 
-- @return               A table of fields.
-----------------------------------------------------------------------------
function Node:get_ordered_field_names()
   local ordered_fields = {}
   for k,v in pairs(self.fields) do
      table.insert(ordered_fields, k)
   end
   table.sort(ordered_fields, function(a,b) return (self.fields[a][1] or 0) < (self.fields[b][1] or 0) end)
   return ordered_fields
end


-----------------------------------------------------------------------------
-- Saves the node (using the data that's already in the node).
-- 
-- @param author         the author associated with the edit.
-- @param comment        a comment for the edit (optional).
-- @param extra          extra params (optional).
-- @return               nothing.
-----------------------------------------------------------------------------
function Node:save(author, comment, extra, timestamp)
   author = author or ""
   self.repository:save_node(self, author, comment, extra, timestamp)
end


-----------------------------------------------------------------------------
-- Checks if the user is allowed to perform a named action.
--
-- @param user           user ID
-- @param action         action ID
-- @return               true or false
-----------------------------------------------------------------------------
function Node:check_permissions(user, action)
   if not self.permissions then return true end
   
   local function in_table(tab, item)
      for i,v in ipairs(tab) do
         if v==item then return true end
      end
      return false
   end
   
   -- checks membership in groups
   local function member(item, group, node)
      if     type(group) == "function" then return group(item, node)
      elseif type(group) == "table"    then return in_table(group, item)
      elseif type(group) == "string"   then return group == item
      else   error("expected a string, a table or a function")
      end
   end

   -- keeps the allowed/not allowed state
   local has_permission = true
   -- toggles the the state
   local function set(user_group, action_group, value)
      if member(user, user_group, self) and member(action, action_group, self) then
         has_permission = value
      end
   end

   -- setup the sandbox
   local sandbox = saci.sandbox.new(self.saci.permission_groups)
   sandbox:add_values{
      allow = function (user, action) set(user, action, true) end,
      deny  = function (user, action) set(user, action, false) end,
   }
   sandbox:do_lua(self.permissions)
   return has_permission
end     


-----------------------------------------------------------------------------
-- Returns a child node, if they are defined.
-- 
-- @param id             child's id.
-- @return               an instance of Node or nil.
-----------------------------------------------------------------------------
function Node:get_child(id)
   if not self.child_defaults then return end
   if self.repository:node_exists(self.id.."/"..id) then
      return nil
   end
   if self.child_defaults[id] then
      return self.repository:make_node(
                cosmo.fill(self.child_defaults[id], self),
                self.id.."/"..id)
   elseif self.child_defaults.any then
      return self.repository:make_node(self.child_defaults.any, self.id.."/"..id)
   elseif self.child_defaults.patterns then
      for i, pattern in ipairs(self.child_defaults.patterns) do
         if id:match(pattern[1]) then
            return self.repository:make_node(pattern[2], self.id.."/"..id)
         end
      end
   end
end

local function make_immediate_child_filter(parent_id)
   local length = parent_id:len()
   return function(id)
      return not id:sub(length+2):find("/")
   end
end

function Node:get_children(immediate, limit, visible)
   local id_filter = make_immediate_child_filter(self.id)
   return self.repository:get_nodes_by_prefix(self.id.."/", limit, id_filter)
end

function Node:get_visible_children(immediate, limit, visible)
   local id_filter = make_immediate_child_filter(self.id)
   return self.repository:get_nodes_by_prefix(self.id.."/", limit, true, id_filter)
end

function Node:get_parent_id()
   local parent_id, rest = string.match(self.id, "^(.+)/(.-)$")
   return parent_id, rest
end

function Node:multimatch(fields, patterns, match_case)
   local value
   for _, field in ipairs(fields) do
      value = self[field]
      if value and type(value)=="string" then
         --value = " "..value:lower().." "
         if not match_case then
            value = value:lower()
         end
         for __, pattern in ipairs(patterns) do
            if value:match(pattern) then return true end
         end
      end
   end
end

