--
-- property_handling.lua
--
-- Simple module for handling property files typical in Guinnux/RSM/RAM projects.
--
-- Author: Ben Phillips (Keystone Electronic Solutions)
--

require("luamods/command")

--
-- Helper functions.
--

-- split a string around =
local function splitKeyValue(str)
  local _, _, key, value = string.find(str, "%s*([^%s]*)%s*=%s*(.*)%s*")
  return key, value
end

-- create an array representing the nested key in a table.
local function explodeAroundDot(str)
  local matches = {}
  for match in string.gmatch(str, "([^.]+)") do
    table.insert(matches, match)
  end
  return matches
end

-- Get key tree to a value which is also returned.
local function getTreeSegment(line)
  local key, value = splitKeyValue(line)
  return explodeAroundDot(key), value
end

-- Convert an array of ordered property file lines into a tree of said properties.
local function tableFromLines(lineArray)
  local properties = {}
  -- local currentSection = nil -- uncomment the stuff about curretnSection to get ini file like config files

  -- Build property tree by reading all lines and adding tree segments.
  for line_index, line in ipairs(lineArray) do

    -- Check for section named with [<name>], otherwise insert table entry if not empty line or comment.
    -- local sectionTag = line:match("^%[(%w+)%]$")
    -- if sectionTag then
    --   -- currentSection = sectionTag

    --elseif
    if not line:find("^%s*$") and not line:find('^%s*#.*$') then

      local keyTree, value = getTreeSegment(line)
      -- if currentSection then table.insert(keyTree, 1, currentSection) end

      local levelRef = properties

      -- Build full path in tree until reaching the value.
      for idx = 1, #keyTree - 1, 1 do
        if not levelRef[keyTree[idx]] then
          -- entry does not exist yet, create it.
          levelRef[keyTree[idx]] = {}
        elseif type(levelRef[keyTree[idx]]) == "string" then
          -- We reached a string but their are still more keys.
          -- We must make a new table and insert the value as element 1.
          local strVal = levelRef[keyTree[idx]]
          levelRef[keyTree[idx]] = { strVal }
        end
        levelRef = levelRef[keyTree[idx]]
      end

      -- Insert the value at the end.
      levelRef[keyTree[#keyTree]] = value
    end
  end

  return properties
end

local PropertyConfig = {}
PropertyConfig.__index = PropertyConfig

setmetatable(PropertyConfig, {
  __call = function (cls, ...)
    return cls.new(...)
  end,
})

function PropertyConfig.new(configFilePath)
  local self = setmetatable({}, PropertyConfig)

  self.hasChanged = false -- has the in-memory config text changed from the file.
  self.configFilePath = configFilePath -- path to config file.
  self.onChangeActions = {} -- array of functions to run when a change is saved to this config file.

  local configFile, err = io.open(configFilePath, "r")
  self.properties = nil -- property tree of current config.
  self.configText = "" -- The config file in text.

  if configFile == nil then
    command.log("Failed to open "..configFilePath..". Error reads: "..err)
    return nil
  else
    local configLines = {}
    for line in configFile:lines() do
      table.insert(configLines, line)
      self.configText = self.configText..line.."\n"
    end
    configFile:close()
    self.properties = tableFromLines(configLines)
  end

  return self
end

-- Static function to get a value in a nested table or return nil. Lua exceptions
-- aren't pleasant.
function PropertyConfig.derefTblElement(tbl, keyArray)
  if tbl == nil then return nil end

  local ref = tbl
  for index, val in ipairs(keyArray) do
    ref = ref[val]
    if not ref then return nil end
  end

  return ref
end

function PropertyConfig:get(keyArray)
  return PropertyConfig.derefTblElement(self.properties, keyArray)
end

-- Write value into the config entry for keyTree. If the entry does not exist, we
-- will create it after the first config at the same level.
function PropertyConfig:updateConfigText(keyTree, value)
  self.hasChanged = true

  -- String representation of keyTree.
  local key = table.concat(keyTree, ".")

  -- Generate lua pattern matching strings we can reuse.
  -- Only matches the key and equal sign. Can handle spaces and tabs.
  local keyMatch = "[ \t]*"..key.."%s*="
  -- Matches the whole config line for keyTree. There is possibly one space at the beginning of the value.
  local configLineMatch = keyMatch.."[ ]?[^\n]*"

  local configLine = self.configText:match(configLineMatch)
  if not configLine then
    -- Generate the config line we need.
    configLine = table.concat(keyTree, ".").."="..value
    -- Find the closest property in tree and insert new config after that.
    for keyIdx = #keyTree - 1, 1, -1 do
      local keyStr = table.concat({table.unpack(keyTree, 1, keyIdx)}, ".")
      local keyExists = self.configText:find(keyStr) -- Capture the entire config line.
      local workingConfigTable = {}
      local found = false
      if keyExists then
        for line in string.gmatch(self.configText, "[^\n]*\n") do
          line = line:sub(1, -2)
          table.insert(workingConfigTable, line)
          -- Find the key in question and insert the new entry on the next line.
          if not string.find(line, "^[ ]*[#]+") and not found then -- not comment config line.
            if string.find(line, "^[ ]*"..keyStr..".*") then
              found = true
              table.insert(workingConfigTable, configLine)
            end
          end
        end
        self.configText = table.concat(workingConfigTable, "\n")
        return -- We have done all the necessary edits.
      else
        -- At this point I don't know where to put the line. So I just append it on its own line.
        self.configText = self.configText.."\n"..configLine
        return -- We have done all the necessary edits.
      end
    end

    -- If we get here, we could not find a related subtree.
    -- Find the closest non-comment line and insert the
    local closestValidLine = self.configText:match("([^\n]*[^#][^\n]*)") -- any line starting with # is a no go.
    self.configText = self.configText:gsub(closestValidLine, closestValidLine.."\n"..configLine, 1)
  else
    -- The entry does exists so we will change the existing line.
    -- Capture the key text in the config to preserve how it was written and just replace the value part.
    -- We preserve one space if it was written after the equals.
    local preValueText = "("..keyMatch..")([ ]?)[^\n]*"
    self.configText = self.configText:gsub(preValueText, "%1".."%2"..value)
  end
end

function PropertyConfig:applyIfDifferent(keyTree, newVal)
  -- Get ref to the final table in a table tree
  local tblLevel = self.properties
  for idx = 1, #keyTree - 1, 1 do
    local currentLevel = tblLevel[keyTree[idx]]
    -- If, at any point, the table tree is nil we must create the entries.
    if currentLevel == nil then
      tblLevel[keyTree[idx]] = {}
    end

    tblLevel = tblLevel[keyTree[idx]]
  end

  -- Update the table with the new value and record the fact if it did make a change.
  local oldVal = tblLevel[keyTree[#keyTree]] or ""
  local changed = false
  if newVal ~= oldVal then
    changed = true
  end
  tblLevel[keyTree[#keyTree]] = newVal

  -- If a change was made, update the config text.
  if changed then
    self:updateConfigText(keyTree, tblLevel[keyTree[#keyTree]])
  end
end

function PropertyConfig:registerOnChangeAction(actionCallback)
  table.insert(self.onChangeActions, actionCallback)
end

-- Perform all registered actions on a change to the config file.
function PropertyConfig:onChange()
  for index, action in ipairs(self.onChangeActions) do
    action(self)
  end
end

-- Save the config file to the fs.
function PropertyConfig:save()
  command.writefile(self.configFilePath, self.configText)
end

-- Save the config file to the config file on the fs. Returns true or false as to
-- whether any changes were actually made.
function PropertyConfig:saveIfChanged()
  if self.hasChanged then
    self:save()
    self:onChange()
    return true
  else
    return false
  end
end

return PropertyConfig
