local xmlParser = {}
--[[----------------------------------------------------------------------------
Class to parse XML

Author: Frikkie Jansen van Rensburg
Email: frikkie@kses.net

Date: Jan 2017

DEPS: Lua Expat, penlight xml
----------------------------------------------------------------------------]]--
-- luaexpat initialisation for xml parsing
local lxp = require("lxp")
local xml = require("pl.xml")

--this copies the table to a new one and discards cyclic references
local function deepcopy(orig)
  local flag = false

  local orig_type = type(orig)
  local copy
  if orig_type == 'table' then
    copy = {}
    for orig_key, orig_value in next, orig, nil do
      if orig_key ~= "__parent__" then
        copy[deepcopy(orig_key)] = deepcopy(orig_value)
      end
    end
    setmetatable(copy, deepcopy(getmetatable(orig)))
  else -- number, string, boolean, etc
    copy = orig
  end
  return copy
end

-- writes a file with a string
local function writeToFile(file,s)
  local f,e = io.open(file,"w")
  if f == nil then
    --print(string.format("Error: %s",e))
    return nil
  else
    f:write(s) -- clear the file
    f:close()
    return true
  end
end

--------------------------------------XML---------------------------------------
local t = {} -- shared "pointer" to store your position in the array

local mt = {}
setmetatable(t,mt)

local d = {}

local function addAttrs(a, fattrs)
  --print(pretty.write(fattrs))
  a["__attrs__"] = {}
  for k,v in ipairs(fattrs) do
    a["__attrs__"][v] = fattrs[v]
  end
end

local function openElement(name, attrs)
  --print(string.format("Open: %s",name))
  local temp = {}
  local flag = false
  -- check whether the tag already exists within the level, or if an array exists within the level
  -- if an array exists, add to it, if the tag exists, create the array and add to that
  -- add meta entry
  if type(t[name]) == "nil" then
    -- tag does not exist; create it and traverse down
    t[name] = {}
    -- create a reference to the parent, needed to traverse back up
    -- parent = t
    t[name]["__parent__"] = t
    if next(attrs) ~= nil then
      addAttrs(t[name],attrs)
    end
    -- traverse down
    t = t[name]
  else
    -- tag exists, check if that tag is an array
    if type(t[name][1]) == "nil" then
      -- the array does not exist, create it
      -- This deepcopy removes the cyclic refs as well
      temp = deepcopy(t[name])
      t[name] = {}
      t[name][1] = temp
      t[name][2] = {}
      parent = t
      t[name][1]["__parent__"] = t
      t[name][2]["__parent__"] = t

      if next(attrs) ~= nil then
        addAttrs(t[name][2],attrs)
      end
      t = t[name][2]
    else
      -- the array exists, add to it
      t[name][#t[name] + 1] = {}
      t[name][#t[name]]["__parent__"] = t

      if next(attrs) ~= nil then
        addAttrs(t[name][#t[name]],attrs)
      end
      t = t[name][#t[name]]
    end
  end
end

local function closeElement(name)
  --traverse back up
  --print(string.format("Close: %s",name))
  t = t["__parent__"]
end

local function addText(text)
  -- set last tag as a leaf node
  t["__value__"] = text
end

local function saveComment(text)
  --mt[#mt+1] = text
end

local function addProlog(text)
  mt[1] = text
end

local callbacks = {
    -- Fires when finding opening tag of element.
    StartElement = function (parser, name, attrs)
        openElement(name, attrs)
    end,
    -- Fires when finding closing tag of element.
    EndElement = function (parser, name)
        closeElement(name)
    end,
    -- Fires when finding text inside an element.
    CharacterData = function (parser, text)
        -- Check for white space which occurs with indentation.
        -- (You will probably preserve this... I'm just doing this for display purposes)
        if not text:match("^([%s]*)$") then
            --print(string.format("Found some useful text: %s",text))
            addText(text:gsub("^%s*(.-)%s*$", "%1"))
        end
    end,
    -- Fires when finding the declaraion
    XmlDecl = function (parser, version, encoding)
      local decl = "<?xml version=\"" .. version .. "\" encoding=\"" .. encoding .. "\"?>"
      addProlog(decl)
    end,
    -- Fires when finding a comment.
    Comment = function (parser, text)
        saveComment(text)
    end
    -- There are more events at https://matthewwild.co.uk/projects/luaexpat/manual.html#parser
}

-- writing functions
local function writeXMLrecursive(e,name,fp)
  local attrs = {}
  local flag = false

  if type(e) == "table" then
    for k,v in pairs(e) do
      if k == "__attrs__" then
        -- do nothing
      elseif k == "__value__" then
        d:text(v)
      else
        --lookahead
        if type(v) == "table" then
          for k1,v1 in pairs(v) do
            if k1 == "__attrs__" then
              attrs = v1
            elseif type(k1) == "number" then
              flag = true
            end
          end
        end
        if fp then
          d = xml.new(k,attrs)
        else
          if type(k) == "number" then
            d:addtag(name,attrs)
          else
            if flag then
              --this may seem convoluted, but we need to create the tag to traverse
              --deeper into the document
              flag = false
              -- mark tags to be removed with __remove__
              d:addtag("__remove__",attrs)
            else
              d:addtag(k, attrs)
            end
          end
        end
        attrs = nil
        writeXMLrecursive(v,k,false)
        d:up()
      end
    end
  end
end

-- MODULE FUNCTIONS
function xmlParser.parse(file)
  t = {}
  mt = {}

  local rt = {}

  local parser = lxp.new(callbacks)

  -- Parse each line of xml... which will call the appropriate callbacks for each line.
  for line in io.lines(file) do
      parser:parse(line)
      --print(line) -- Uncomment if you want to see it happening line by line.
  end


  -- Close the parser.
  parser:parse()
  parser:close()

  -- remove cyclic references
  rt = deepcopy(t)

  setmetatable(rt,mt)
  return rt
end

--local d
function xmlParser.write(file, t)
  local meta = {}

  if (t == nil) or (t == {}) then
    return nil
  end

  meta = getmetatable(t)

  d = nil
  local s = ""
  -- print(string.format("Writing XML file %s\n",file))
  -- generate the xml doc
  writeXMLrecursive(t,"",true)
  -- remove the marked tags
  local s = xml.tostring(d,"","")
  -- remove the marked tags
  s = string.gsub(s,"<__remove__>","")
  s = string.gsub(s,"</__remove__>","")
  -- This is kind of stupid, the stringx.replace function leaves unwanted whitespace.
  -- So I reparse the string into a xml document and restringify it to make it pretty
  -- again, this is a bit of a hack admittedly.
  -- (If it works and it's stupid, it's not stupid at all)
  d = xml.basic_parse(s, false, false)
  s = xml.tostring(d,"","  ")

  d = nil
  --remove the leading newline
  s = string.sub(s,2,#s)
  if meta ~= nil and meta ~= {} then
    if type(meta[1]) == "string" then
      if string.sub(meta[1],1,5) == "<?xml" then
        --append the prolog
        s = meta[1].."\n"..s
      end
    end
  end
  --finally write the xml to the file
  if writeToFile(file, s) then
    return true
  else
    return nil
  end
end

return xmlParser
