-- Copyright (c) 2010 by Robert G. Jakabosky <bobby@neoawareness.com>
--
-- Permission is hereby granted, free of charge, to any person obtaining a copy
-- of this software and associated documentation files (the "Software"), to deal
-- in the Software without restriction, including without limitation the rights
-- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-- copies of the Software, and to permit persons to whom the Software is
-- furnished to do so, subject to the following conditions:
--
-- The above copyright notice and this permission notice shall be included in
-- all copies or substantial portions of the Software.
--
-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-- THE SOFTWARE.

local tinsert,tremove=table.insert,table.remove
local tappend=function(dst,src) for _,v in pairs(src) do dst[#dst+1] = v end end

-- records that are not contained in other records
local root_records={}
local record_type_groups={}
local clear_funcs={}
local global_scope={}
function clear_all_records()
	root_records={}
	record_type_groups={}
	-- run clear functions
	for i=1,#clear_funcs do
		local func = clear_funcs[i]
		func()
	end
	global_scope={}
end
function reg_clear_func(func)
	clear_funcs[#clear_funcs + 1] = func
end

-- get a named record with rec_type
function get_named_record(rec_type, name)
	local recs = record_type_groups[rec_type]
	if recs ~= nil then
		return recs[name]
	end
end

local function group_add_record(rec, rec_type)
	-- add this record to the list of records with the same type.
	local type_group = record_type_groups[rec_type]
	if type_group == nil then
		type_group = {}
		record_type_groups[rec_type] = type_group
	end
	type_group[#type_group+1] = rec
	if name ~= nil then
		type_group[name] = rec
	end
end

local variable_format="%s_idx%d"
function set_variable_format(format)
	variable_format = format
end
function format_variable(name, idx)
	return variable_format:format(name, idx)
end

-- Meta-table used by all records.
local ignore_record={_rec_type = "ignore"}
local rec_meta
rec_meta={
clear = function(self)
	self._vars = {}
	self._data_parts = {}
	self._rec_counts = {}
end,
--
-- add data output functions to record.
--
-- replace variables in "part"
vars_part = function(self, part)
	local tmpl = self:dump_parts({part})
	tmpl = tmpl:gsub("%${(.-)}", self._vars)
	self._data_parts[part] = {tmpl}
	return tmpl
end,
-- replace variables in "parts"
vars_parts = function(self, parts)
	local out={}
	parts = self:parts(parts)
	-- apply variables to all "parts".
	for i=1,#parts do
		local part = parts[i]
		local d = self:vars_part(part)
		out[#out+1] = d
	end
	return out
end,
-- append data to "part"
write_part = function(self, part, data)
	if type(data) ~= "table" then
		if data == nil then return end
		data = { tostring(data) }
	end
	local out=self._data_parts[part]
	if out == nil then
		out = {}
		self._data_parts[part] = out
	end
	-- append data.
	tappend(out, data)
end,
parts = function(self, parts)
	-- make sure "parts" is a table.
	if parts == nil then
		parts = {}
		for part in pairs(self._data_parts) do parts[#parts+1] = part end
	elseif type(parts) ~= "table" then
		parts = { parts }
	end
	return parts
end,
dump_parts = function(self, parts, sep)
	local out={}
	parts = self:parts(parts)
	-- return all parts listed in "parts".
	local data = self._data_parts
	for i=1,#parts do
		local part = parts[i]
		local d_part=data[part]
		if d_part then
			tappend(out, d_part)
		end
		if sep ~= nil then
			out[#out+1] = sep
		end
	end
	return table.concat(out)
end,
-- copy parts from "src" record.
copy_parts = function(self, src, parts)
	parts = src:parts(parts)
	for i=1,#parts do
		local part = parts[i]
		self:write_part(part, src:dump_parts(part))
	end
end,
--
-- functions for counting sub-records
--
get_sub_record_count = function(self, _rec_type)
	local count = self._rec_counts[_rec_type]
	if count == nil then count = 0 end
	return count
end,
count_sub_record = function(self, rec)
	local count = self:get_sub_record_count(rec._rec_type)
	count = count + 1
	self._rec_counts[rec._rec_type] = count
	rec._rec_idx = count
end,
--
-- functions for adding named variables
--
add_var = function(self, key, value)
	self._vars[key] = value
end,
add_rec_var = function(self, rec, name, vname, idx)
	local name = name or rec.name
	local idx = idx or rec._rec_idx
	self._vars[name] = vname or format_variable(name, idx)
	self._vars[name .. "::idx"] = idx
end,
--
-- sub-records management functions
--
make_sub_record = function(self, parent)
	local root_idx
	-- find record in roots list
	for idx=1,#root_records do
		if root_records[idx] == self then
			root_idx = idx
			break
		end
	end
	-- remove it from the roots list
	if root_idx ~= nil and root_records[root_idx] == self then
		tremove(root_records, root_idx)
	end
	rawset(self, "_parent", parent)
end,
insert_record = function(self, rec, pos)
	rec:make_sub_record(self)
	if pos ~= nil then
		tinsert(self, pos, rec)
	else
		self[#self+1] = rec
	end
end,
add_record = function(self, rec)
	self:insert_record(rec)
end,
find_record = function(self, rec)
	for i=1,#self do
		local sub = self[i]
		if sub == rec then
			return i
		end
	end
end,
replace_record = function(self, old_rec, new_rec)
	for i=1,#self do
		local sub = self[i]
		if sub == old_rec then
			self[i] = new_rec
			return i
		end
	end
end,
remove_record = function(self, rec)
	for i=1,#self do
		local sub = self[i]
		if sub == rec then
			rawset(self, i, ignore_record) -- have to insert an empty table in it's place.
			rawset(sub, "_parent", nil)
			return
		end
	end
end,
--
-- delete a record and all it's sub-records
--
delete_record = function(self)
	-- remove from parent.
	if self._parent ~= nil then
		self._parent:remove_record(self)
	end
	-- delete sub-records
	for i=1,#self do
		local sub = self[i]
		if is_record(sub) and sub._parent == self then
			self[i] = nil
			sub:delete_record()
			rawset(sub, "_parent", nil)
		end
	end
	-- ignore this record and it sub-records
	self._rec_type = "ignore"
end,
--
-- Copy record and all it's sub-records.
--
copy_record = function(self)
	local copy = {}
	-- copy values from current record.
	for k,v in pairs(self) do
		copy[k] = v
	end
	rawset(copy, "_parent", nil) -- unlink from old parent
	-- copy sub-records
	for i=1,#copy do
		local sub = copy[i]
		if is_record(sub) then
			local sub_copy = sub:copy_record()
			rawset(copy, i, sub_copy)
			rawset(sub_copy, "_parent", copy)
		end
	end
	setmetatable(copy, rec_meta)
	group_add_record(copy, copy._rec_type)
	return copy
end,
--
-- Symbol resolver
--
add_symbol = function(self, name, obj, scope)
	-- default scope 'local'
	if scope == nil then scope = "local" end
	-- if scope is global then skip local maps.
	if scope == 'global' then
		global_scope[name] = obj
		return
	end
	-- add symbol to local map
	self._symbol_map[name] = obj
	-- if scope is doesn't equal our scope
	if scope ~= self.scope and self._parent ~= nil then
		self._parent:add_symbol(name, obj, scope)
	end
end,
get_symbol = function(self, name)
	-- check our mappings
	local obj = self._symbol_map[name]
	-- check parent if we don't have a mapping for the symbol
	if obj == nil and self._parent ~= nil then
		obj = self._parent:get_symbol(name)
	end
	-- next check the imports for the symbol
	if obj == nil then
		local imports = self._imports
		for i=1,#imports do
			local import = imports[i]
			obj = import:get_symbol(name)
			if obj ~= nil then
				break
			end
		end
	end
	-- next check the globals for the symbol
	if obj == nil then
		obj = global_scope[name]
	end
	return obj
end,
-- import symbols from a "file" record
add_import = function(self, import_rec)
	local imports = self._imports
	-- if already imported then skip
	if imports[import_rec] then return end
	imports[import_rec] = true
	-- append to head of imports list so that the last import overrides symbols
	-- from the previous imports
	table.insert(imports, 1, import_rec)
end,
}
rec_meta.__index = rec_meta
function is_record(rec)
	-- use a metatable to identify records
	return (getmetatable(rec) == rec_meta and rec._rec_type ~= nil)
end
setmetatable(ignore_record, rec_meta)

local function remove_child_records_from_roots(rec, seen)
	-- make sure we don't get in a reference loop.
	if seen == nil then seen = {} end
	if seen[rec] then return end
	seen[rec] = true
	-- remove from root list.
	for i=1,#rec do
		local val = rec[i]
		if is_record(val) then
			val:make_sub_record(rec)
		end
	end
end

local function end_record(rec)
	if type(rec) ~= 'function' then return rec end

	local rc, result = pcall(rec, nil)
	if not rc then
		print("Error processing new record: " .. result)
		return rec
	end
	return end_record(result)
end

function make_record(rec, rec_type, name, scope)
	if rec == nil then rec = {} end
	if type(rec) ~= "table" then rec = { rec } end
	-- set record's name.
	if name == nil then name = rec_type end
	rec.name = name
	-- record's symbol scope
	rec.scope = scope
	rec._symbol_map = {}
	rec._imports = {}

	-- make "rec" into a record.
	rec._rec_type = rec_type
	setmetatable(rec, rec_meta)

	-- complete partial child records.
	for i=1,#rec do
		local val = rec[i]
		if type(val) == 'function' then
			val = end_record(val)
			rec[i] = val
		end
	end

	-- remove this record's child records from the root list.
	remove_child_records_from_roots(rec)

	-- add this record to the root list.
	root_records[#root_records + 1] = rec

	group_add_record(rec, rec_type)

	return rec
end

--
-- Record parser
--
local function record_parser(callbacks, name)
	name = name or "parse"
	local function call_meth(self, rec_type, post, rec, parent)
		local func = self[rec_type .. post]
		if func == nil then
			func = self["unknown" .. post]
			if func == nil then return end
		end
		return func(self, rec, parent)
	end
	local seen={}
	callbacks = setmetatable(callbacks, {
	__call = function(self, rec, parent)
		-- make sure it is a valid record.
		if not is_record(rec) or seen[rec] or rec._rec_type == "ignore" then return end
		if parent then
			parent:count_sub_record(rec) -- count sub-records.
		end
		-- keep track of records we have already processed
		seen[rec] = true
		local rec_type = rec._rec_type
		-- clear record's data output & sub-record counts.
		rec:clear()
		-- start record.
		call_meth(self, rec_type, "", rec, parent)
		-- transverse into sub-records
		for _,v in ipairs(rec) do
			self(v, rec)
		end
		-- end record
		call_meth(self, rec_type, "_end", rec, parent)
		-- update "last_type"
		self.last_type = rec_type
	end
	})
	return callbacks
end

function process_records(parser)
	record_parser(parser)
	-- process each root record
	for i=1,#root_records do
		parser(root_records[i])
	end
	return parser
end

local stages = {}
function reg_stage_parser(stage, parser)
	local parsers = stages[stage]
	if parsers == nil then
		-- new stage add it to the end of the stage list.
		stages[#stages + 1] = stage
		parsers = {}
		stages[stage] = parsers
	end
	parsers[#parsers + 1] = parser
end

-- setup default stages
local default_stages = { "symbol_map", "imports" }

-- run all parser stages.
function run_stage_parsers()
	for i=1,#stages do
		local stage = stages[i]
		local parsers = stages[stage]
		for x=1,#parsers do
			process_records(parsers[x])
		end
	end
end

function move_recs(dst, src, idx)
	-- move records from "rec" to it's parent
	for i=1,#src do
		local rec = src[i]
		if is_record(rec) and rec._rec_type ~= "ignore" then
			src:remove_record(rec) -- remove from src
			if idx then
				dst:insert_record(rec, idx) -- insert into dst
				idx = idx + 1
			else
				dst:add_record(rec) -- add to dst
			end
		end
	end
	-- now delete this empty container record
	src:delete_record()
end

--
-- Record functions -- Used to create new records.
--
function make_generic_rec_func(rec_type, no_name)
	if _G[rec_type] ~= nil then error("global already exists with that name: " .. rec_type) end
	if not no_name then
		_G[rec_type] = function(name)
			return function(rec)
				rec = make_record(rec, rec_type, name)
				return rec
			end
		end
	else
		_G[rec_type] = function(rec)
			rec = make_record(rec, rec_type, rec_type)
			return rec
		end
	end
end

local path_char = package.config:sub(1,1)
local path_match = '(.*)' .. path_char
local path_stack = {''}
function subfile_path(filename)
	local level = #path_stack
	local cur_path = path_stack[level] or ''
	return cur_path .. filename
end
function subfiles(files)
	local level = #path_stack
	local cur_path = path_stack[level]
	local rc
	level = level + 1
	-- use a new roots list to catch records from subfiles
	local prev_roots = root_records
	root_records={}

	-- process subfiles
	for i=1,#files do
		local file = files[i]
		-- add current path to file
		file = cur_path .. file
		-- seperate file's path from the filename.
		local file_path = file:match(path_match) or ''
		if #file_path > 0 then
			file_path = file_path .. path_char
		end
		-- push the file's path onto the path_stack only if it is different.
		if cur_path ~= file_path then
			path_stack[level] = file_path
		end
		-- check file path
		print("Parsing records from file: " .. file)
		rc = {dofile(file)}
		-- pop path
		if cur_path ~= file_path then
			path_stack[level] = nil
		end
	end
	-- move sub-records into new array
	local rec={}
	for i=1,#root_records do
		rec[i] = root_records[i]
	end

	-- switch back to previous roots list
	root_records = prev_roots

	-- make this into a record holding the sub-records from each of the sub-files
	rec = make_record(rec, "subfiles")
	if #rc > 0 then
		return rec, rc
	end
	return rec
end
-- process some container records
reg_stage_parser("containers", {
subfiles = function(self, rec, parent)
	local idx = parent:find_record(rec)
	move_recs(parent, rec, idx)
end,
})

local subfolders={}
function subfolder(folder)
	return function(...)
	local files=select(1, ...)
	if type(files) ~= 'table' then
		files = {...}
	end
	-- push subfolder
	subfolders[#subfolders+1] = folder
	-- build full path
	folder = table.concat(subfolders, "/") .. "/"
	for i=1,#files do
		files[i] = folder .. files[i]
	end
	-- use subfile record.
	local rec = subfiles(files)
	-- pop subfolder
	subfolders[#subfolders] = nil
	return rec
end
end

function import(name)
	rec = make_record({}, "import", name)
	return rec
end
-- resolve imports
reg_stage_parser("imports", {
import = function(self, rec, parent)
end,
})

