-- Binary Template plugin for REHex
-- Copyright (C) 2021-2023 Daniel Collins <solemnwarning@solemnwarning.net>
--
-- This program is free software; you can redistribute it and/or modify it
-- under the terms of the GNU General Public License version 2 as published by
-- the Free Software Foundation.
--
-- This program is distributed in the hope that it will be useful, but WITHOUT
-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
-- FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
-- more details.
--
-- You should have received a copy of the GNU General Public License along with
-- this program; if not, write to the Free Software Foundation, Inc., 51
-- Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

local M = {}

--local lpeg = require 'lpeg'
local lpeg = require 'lulpeg.lulpeg'
setmetatable(_ENV, { __index=lpeg })

local RESERVED_WORDS = {
	["if"]        = true,
	["else"]      = true,
	["struct"]    = true,
	["typedef"]   = true,
	["while"]     = true,
	["for"]       = true,
	["break"]     = true,
	["continue"]  = true,
	["return"]    = true,
	["unsigned"]  = true,
}

-- Global table of line start/end positions in the input stream
-- Generated by build_lines_table() and searched by input_pos_to_file_and_line_num()
local input_lines

local function build_lines_table(text)
	input_lines = {}
	
	local filename = "UNKNOWN FILE"
	local line_num = 1
	
	local line_start = 1
	
	local i = 1
	while i <= text:len()
	do
		local m_filename, m_line_num = text:match("^#file%s+([^\n]+)%s+(%d+)\n", i)
		
		if m_filename ~= nil
		then
			filename = m_filename
			line_num = math.floor(m_line_num)
			
			i = text:find("\n", i)
			line_start = i + 1
		elseif text:sub(i, i) == "\n" or i == text:len()
		then
			table.insert(input_lines, {
				["filename"] = filename,
				["line_num"] = line_num,
				
				["line_start"] = line_start,
				["line_end"]   = i,
			})
			
			line_num = line_num + 1
			line_start = i + 1
		end
		
		i = i + 1
	end
end

local function input_pos_to_file_and_line_num(pos)
	local lower = 1
	local upper = #input_lines
	
	while lower <= upper
	do
		local i = lower + math.floor((upper - lower) / 2)
		
		if input_lines[i].line_end < pos
		then
			lower = i + 1
		elseif input_lines[i].line_start > pos
		then
			upper = i - 1
		else
			return input_lines[i].filename, input_lines[i].line_num
		end
	end
	
	return "UNKNOWN FILE", 0
end

local function _PARSE_ERROR(message, pos_func)
	if message == nil
	then
		message = "Parse error"
	end
	
	return P(function(text, pos)
		-- Getting here means we're trying to parse something and none of the real captures have
		-- matched, so any actual text is a parse error.
		
		if pos_func ~= nil
		then
			pos = pos_func()
		end
		
		if pos >= text:len()
		then
			pos = text:len()
		end
		
		local pos_filename, pos_line_num = input_pos_to_file_and_line_num(pos)
		local fragment = text:sub(pos, pos + 10):gsub("\n.*", "")
		
		error(message .. " at " .. pos_filename .. ":" .. pos_line_num .. " (at '" .. fragment .. "')")
	end)
end

local function _consume_directive(text, pos)
	-- Directives from the preprocessor begin at column zero, anything else is from the
	-- template source.
	
	if (pos == 2 or text:sub(pos - 2, pos - 2) == "\n") and text:sub(pos - 1, pos - 1) == "#"
	then
		local directive_end = text:find("\n", pos);
		return directive_end + 1;
	end
	
	return nil
end

local function _capture_position(text, pos)
	return pos, "_input_stream_pos", pos
end

local BACKSLASH_ESCAPES = {
	-- https://en.wikipedia.org/wiki/Escape_sequences_in_C
	["a"]  = "\a",
	["b"]  = "\b",
	["e"]  = string.char(0x1B),
	["f"]  = "\f",
	["n"]  = "\n",
	["r"]  = "\r",
	["t"]  = "\t",
	["v"]  = "\v",
	["\""] = "\"",
	["\\"] = "\\",
	["'"]  = "'",
	["?"]  = "?",
}

local function _capture_string(text, pos)
	local s = ""
	local i = pos
	local len = text:len()
	
	while i <= len
	do
		local c = text:sub(i, i)
		
		if c == "\\"
		then
			local ob,oe,oc = text:find("^\\([0-7][0-7]?[0-7]?)", i)
			local hb,he,hc = text:find("^\\x([0-9A-Fa-f][0-9A-Fa-f])", i)
			local cb,ce,cc = text:find("^\\(.)", i)
			
			if ob ~= nil
			then
				s = s .. string.char(tonumber(oc, 8))
				i = oe + 1
			elseif hb ~= nil
			then
				s = s .. string.char(tonumber(hc, 16))
				i = he + 1
			elseif cb ~= nil and BACKSLASH_ESCAPES[cc] ~= nil
			then
				s = s .. BACKSLASH_ESCAPES[cc]
				i = ce + 1
			else
				local filename, line_num = input_pos_to_file_and_line_num(i)
				error("Unrecognised \\ escape at " .. filename .. ":" .. line_num)
			end
		elseif c == '"'
		then
			return i + 1, s
		else
			s = s .. c
			i = i + 1
		end
	end
	
	local filename, line_num = input_pos_to_file_and_line_num(pos - 1)
	error("Unmatched \" at " .. filename .. ":" .. line_num)
end

local function _capture_char_literal(text, pos)
	local char_value = nil
	
	local ob,oe,oc = text:find("^\\([0-7][0-7]?[0-7]?)", pos)
	local hb,he,hc = text:find("^\\x([0-9A-Fa-f][0-9A-Fa-f])", pos)
	local cb,ce,cc = text:find("^\\(.)", pos)
	local pb,pe,pc = text:find("^([^'])", pos)
	
	if ob ~= nil
	then
		char_value = tonumber(oc, 8)
		pos = oe + 1
	elseif hb ~= nil
	then
		char_value = tonumber(hc, 16)
		pos = he + 1
	elseif cb ~= nil
	then
		if BACKSLASH_ESCAPES[cc] ~= nil
		then
			char_value = string.byte(BACKSLASH_ESCAPES[cc])
			pos = ce + 1
		else
			local filename, line_num = input_pos_to_file_and_line_num(pos)
			error("Unrecognised \\ escape at " .. filename .. ":" .. line_num)
		end
	elseif pb ~= nil
	then
		char_value = string.byte(pc)
		pos = pe + 1
	else
		local filename, line_num = input_pos_to_file_and_line_num(pos - 1)
		error("Expected character after ' at " .. filename .. ":" .. line_num)
	end
	
	local sq = text:find("^'", pos)
	
	if sq == nil
	then
		local filename, line_num = input_pos_to_file_and_line_num(pos - 1)
		error("Unmatched ' at " .. filename .. ":" .. line_num)
	end
	
	return sq + 1, char_value
end

local function _capture_type(text, pos)
	local prefix_patterns = {
		{ "^signed%s+",    "signed"   },
		{ "^unsigned%s+",  "unsigned" },
		{ "^const%s+",     "const"    },
	}
	
	local main_patterns = {
		{ "^(enum)%s+([%a_][%d%a_]*)%s*" },
		{ "^(struct)%s+([%a_][%d%a_]*)%s*" },
		{ "^([%a_][%d%a_]*)%s*" },
	}
	
	local postfix_patterns = {
		{ "^%&%s*",       "&"  },
		{ "^%[%s*%]%s*",  "[]" },
	}
	
	local matched_words = {}
	
	local match_patterns = function(patterns)
		for _, pattern in ipairs(patterns)
		do
			local captures = table.pack(text:find(pattern[1], pos))
			
			local match_begin = captures[1]
			table.remove(captures, 1)
			
			local match_end = captures[1]
			table.remove(captures, 1)
			
			if match_begin ~= nil
			then
				if pattern[2] ~= nil
				then
					pos = match_end + 1
					table.insert(matched_words, pattern[2])
				elseif not RESERVED_WORDS[ captures[#captures] ]
				then
					pos = match_end + 1
					
					for _, word in ipairs(captures)
					do
						table.insert(matched_words, word)
					end
				end
				
				return true
			end
		end
		
		return false
	end
	
	while match_patterns(prefix_patterns) do end
	if not match_patterns(main_patterns) then return nil end
	while match_patterns(postfix_patterns) do end
	
	return pos, table.concat(matched_words, " ")
end

local function _skip_type(text, pos)
	local ret = _capture_type(text, pos)
	return ret
end

local _capture_name = Cmt(
	(R("az", "AZ") + P("_")) * (R("az", "AZ", "09") + P("_")) ^ 0,
	function(text, pos, name)
		if RESERVED_WORDS[name] ~= nil
		then
			-- This is a reserved word, don't match
			return
		end
		
		return pos, name
	end
)

local spc = S(" \t\r\n")^0
local spc_req = S(" \t\r\n")^1
local digit = R('09')
local xdigit = R('09') + R('AF') + R('af')
local odigit = R('07')
local number_hex = ( P("0x") * C( xdigit^1                          ) / function(c) return tonumber(c, 16) end )
local number_oct = (           C( P("0") * odigit^1                 ) / function(c) return tonumber(c,  8) end )
local number_dec = (           C( digit^1 * ( P('.') * digit^1 )^-1 ) / function(c) return tonumber(c)     end )
local number = (number_hex + number_oct + number_dec) * spc
local name = _capture_name * spc
local name_nospc = _capture_name
local comma  = P(",") * spc

local _open_statements

local _parser = spc * P{
	"TEMPLATE";
	TEMPLATE =
		Ct( (V("STMT") + #P(1) * _PARSE_ERROR()) ^ 0),
	
	VALUE_NUM = Cc("num") * number,
	VALUE_STR = Cc("str") * P('"') * P(_capture_string) * spc,
	VALUE_CHAR = Cc("num") * P("'") * P(_capture_char_literal) * spc,
	
	VALUE_REF = Cc("ref") * Ct(
		name_nospc * (P("[") * spc * V("EXPR") * P("]"))^-1 *
		(P(".") * name_nospc * (P("[") * spc * V("EXPR") * P("]"))^-1)^0
		) * spc,
	
	VALUE = P(_capture_position) * (V("VALUE_NUM") + V("VALUE_STR") + V("VALUE_CHAR") + V("VALUE_REF")),
	
	STMT =
		P(1) * P(_consume_directive) * spc +
		V("BLOCK") +
		V("IF") +
		V("FOR") +
		V("WHILE") +
		V("SWITCH") +
		V("STRUCT_DEFN") +
		V("ENUM_DEFN") +
		V("TYPEDEF") +
		V("FUNC_DEFN") +
		V("LOCAL_VAR_DEFN") +
		V("VAR_DEFN") +
		V("RETURN") +
		V("BREAK") +
		V("CONTINUE") +
		V("EXPR") * P(";") * spc +
		P(";") * spc,
	
	BLOCK = Ct( P(_capture_position) * Cc("block") * Ct( V("STATEMENT_BODY") ) ),
	
	BRACE_BLOCK_OPEN =
		-- From the "{" character...
		P("{")
		
		-- ...store the position so we can report if there is no matching "}"...
		* P(function(text, pos)
			table.insert(_open_statements, pos - 1)
			return pos
		end)
		* spc,
	
	BRACE_BLOCK_CONTINUE =
		-- ...caller should use this to check if there is further input within the block
		--    before falling through to BRACE_BLOCK_CLOSE...
		#P(1) * -P("}"),
	
	BRACE_BLOCK_CLOSE =
		-- ...where we hit the final mandatory "}"
		(P("}") + _PARSE_ERROR("Unmatched '{'", function() return _open_statements[#_open_statements] end))
		
		-- We read in a full block, get rid of the start position we stashed earlier.
		* P(function(text, pos)
			table.remove(_open_statements)
			return pos
		end)
		* spc,
	
	STATEMENT_BODY =
		V("BRACE_BLOCK_OPEN")
		* (V("BRACE_BLOCK_CONTINUE") * (V("STMT") * spc + _PARSE_ERROR())) ^ 0
		* V("BRACE_BLOCK_CLOSE"),
	
	-- An expression will slurp up as many valid tokens as it finds rather than only consuming
	-- valid expressions, which is a problem when the expression terminator is itself a valid
	-- token in an expression, e.g. variable attributes are terminated by ">", so we have the
	-- _IN_VAR_ATTR variants which won't slurp those characters (unless inside parentheses) and
	-- also form the base of the main EXPR match.
	
	EXPR_IN_VAR_ATTR =
		Ct( P(_capture_position) * Cc("_expr") * Ct(
			(
				Ct( P(_capture_position) * Cc("cast") * P("(") * spc * P(_capture_type) * P(")") * V("EXPR_IN_VAR_ATTR") ) +
				V("EXPR2_IN_VAR_ATTR")
			) ^ 1
		) ),
	
	EXPR2_IN_VAR_ATTR =
		P("(") * spc * V("EXPR") * P(")") * spc +
		Ct( P(_capture_position) * Cc("_ternary_t") * P("?") * spc * V("EXPR") * P(":") * spc) +
		Ct( P(_capture_position) * Cc("call") * name * Ct( S("(") * spc * (V("EXPR") * (comma * V("EXPR")) ^ 0) ^ -1 * S(")") ) * spc ) +
		Ct( P(_capture_position) * Cc("postfix-increment") * Ct( V("VALUE") ) * P("++") * spc) +
		Ct( P(_capture_position) * Cc("postfix-decrement") * Ct( V("VALUE") ) * P("--") * spc) +
		Ct( V("VALUE") ) +
		Ct( P(_capture_position) * Cc("_token") *
			C( P("<=") + P(">=") + P("==") + P("!=") + P("&&") + P("||") + P("+=") + P("-=") + P("*=") + P("/=") + P("%=") + P("<<=") + P(">>=") + P("&=") + P("^=") + P("|=") + P("<<") + P(">>") + P("++") + P("--") + S("!~*/%+-&^|=") ) * spc),
	
	EXPR =
		Ct( P(_capture_position) * Cc("_expr") * Ct(
			(
				Ct( P(_capture_position) * Cc("cast") * P("(") * spc * P(_capture_type) * P(")") * V("EXPR") ) +
				V("EXPR2")
			) ^ 1
		) ),
	
	EXPR2 =
		V("EXPR2_IN_VAR_ATTR") +
		Ct( P(_capture_position) * Cc("_token") *
			C( S("<>") ) * spc),
	
	EXPR_OR_NIL = V("EXPR") + Cc(nil) * spc,
	ZERO_OR_MORE_EXPRS = (V("EXPR") * (comma * V("EXPR")) ^ 0) ^ -1,
	
	VAR_ATTR = Ct(
		P(_capture_position) * name *
		(P("=") * spc * V("EXPR_IN_VAR_ATTR") + Cc(nil))),
	
	--  {
	--      "file.bt", <line>,
	--      "variable",
	--      <variable type>,
	--      <variable name>,
	--      { <struct parameters> } OR nil,
	--      <array size expr> OR nil,
	--      { { "file.bt", <line>, <attribute name>, <attribute value> OR nil }, ... } OR nil,
	--  }
	VAR_DEFN = Ct(
		P(_capture_position) * Cc("variable") *
		(P("private") * spc * Cg( Cc(true), "private")) ^ -1 *
		P(_capture_type) * name *
		(P("(") * spc * Ct( V("ZERO_OR_MORE_EXPRS") ) * P(")") * spc + Cc(nil)) *
		(P("[") * spc * V("EXPR") * P("]") * spc + Cc(nil)) *
		(P("<") * spc * Ct( V("VAR_ATTR") * (comma * V("VAR_ATTR")) ^ 0 ) * P(">") * spc) ^ -1 *
		P(";") * spc ),
	
	--  {
	--      "file.bt", <line>,
	--      "local-variable",
	--      <variable type>,
	--      <variable name>,
	--      { <struct parameters> } OR nil,
	--      <array size expr> OR nil,
	--      <initial value expr> OR nil,
	--  }
	LOCAL_VAR_DEFN = Ct(
		P(_capture_position) * Cc("local-variable") *
		P("local") * spc * P(_capture_type) * name *
		(P("(") * spc * Ct( V("ZERO_OR_MORE_EXPRS") ) * P(")") * spc + Cc(nil)) *
		(P("[") * spc * V("EXPR") * P("]") * spc + Cc(nil)) *
		(P("=") * spc * V("EXPR") * spc + Cc(nil)) * P(";") * spc ),
	
	RETURN = Ct( P(_capture_position) * Cc("return") * P("return") * spc * (V("EXPR") + Cc(nil)) * P(";") * spc),
	
	ARG = Ct( P(_capture_type) * name ),
	
	--  {
	--      "struct",
	--      "name" or nil
	--      { <arguments> },
	--      { <statements> },
	--      "typedef name" or nil,
	--      { <variable name>, { <parameters> }, <array size expr> OR nil } OR nil,
	--  }
	STRUCT_ARG_LIST = Ct( (S("(") * spc * (V("ARG") * (comma * V("ARG")) ^ 0) ^ -1 * S(")")) ^ -1 ),
	STRUCT_VAR_DECL = Ct( name * Ct( (P("(") * spc * V("ZERO_OR_MORE_EXPRS") * P(")") * spc) ^ -1) * (P("[") * spc * V("EXPR") * P("]") * spc + Cc(nil)) ),
	STRUCT_DEFN =
		Ct( P(_capture_position) * Cc("struct") *                      P("struct") * spc * name    * V("STRUCT_ARG_LIST") * spc * Ct( V("STATEMENT_BODY") ) * Cc(nil) * (V("STRUCT_VAR_DECL") + Cc(nil)) * P(";") * spc ) +
		Ct( P(_capture_position) * Cc("struct") *                      P("struct") * spc * Cc(nil) * V("STRUCT_ARG_LIST") * spc * Ct( V("STATEMENT_BODY") ) * Cc(nil) * (V("STRUCT_VAR_DECL") + Cc(nil)) * P(";") * spc ) +
		Ct( P(_capture_position) * Cc("struct") * P("typedef") * spc * P("struct") * spc * name    * V("STRUCT_ARG_LIST") * spc * Ct( V("STATEMENT_BODY") ) * name                                       * P(";") * spc ) +
		Ct( P(_capture_position) * Cc("struct") * P("typedef") * spc * P("struct") * spc * Cc(nil) * V("STRUCT_ARG_LIST") * spc * Ct( V("STATEMENT_BODY") ) * name                                       * P(";") * spc ),
	
	--  {
	--      "file.bt", <line>,
	--      "typedef",
	--      "struct foobar",
	--      "foobar_t",
	--      <array size expr> OR nil,
	--  }
	TYPEDEF = Ct( P(_capture_position) * Cc("typedef") * P("typedef") * spc * P(_capture_type) * name * (P("[") * spc * V("EXPR") * P("]") * spc + Cc(nil)) * P(";") * spc ),
	
	--  {
	--      "file.bt", <line>,
	--      "enum",
	--      "value type - int/word/etc",,
	--      "enum name" or nil,
	--      {
	--          { "member name" } or { "member name", <value expr> },
	--      },
	--      "typedef name" or nil,
	--      { <variable name>, nil, <array size expr> OR nil } OR nil,
	--  }
	ENUM_TYPE        = (P("<") * P(_capture_type) * P(">") * spc) + Cc("int"),
	ENUM_MEMBER      = Ct( name * (P("=") * spc * (V("EXPR") + _PARSE_ERROR())) ^ -1 ),
	ENUM_BODY = Ct(
		V("BRACE_BLOCK_OPEN") *
		(V("ENUM_MEMBER") + _PARSE_ERROR()) *
		(V("BRACE_BLOCK_CONTINUE") * comma * (-(#(P("}"))) * (V("ENUM_MEMBER") + _PARSE_ERROR()))) ^ 0 *
		(comma * #(P("}"))) ^ -1 *
		V("BRACE_BLOCK_CLOSE")
	),
	ENUM_VAR_DECL    = Ct( name * Cc(nil) * (P("[") * spc * V("EXPR") * P("]") * spc + Cc(nil)) ),
	ENUM_DEFN =
		Ct( P(_capture_position) * Cc("enum") *                      P("enum") * spc * V("ENUM_TYPE") * name    * V("ENUM_BODY") * Cc(nil) * Cc(nil)            * P(";") * spc ) +
		Ct( P(_capture_position) * Cc("enum") *                      P("enum") * spc * V("ENUM_TYPE") * name    * V("ENUM_BODY") * Cc(nil) * V("ENUM_VAR_DECL") * P(";") * spc ) +
		Ct( P(_capture_position) * Cc("enum") *                      P("enum") * spc * V("ENUM_TYPE") * Cc(nil) * V("ENUM_BODY") * Cc(nil) * V("ENUM_VAR_DECL") * P(";") * spc ) +
		Ct( P(_capture_position) * Cc("enum") * P("typedef") * spc * P("enum") * spc * V("ENUM_TYPE") * name    * V("ENUM_BODY") * name    * Cc(nil)            * P(";") * spc ) +
		Ct( P(_capture_position) * Cc("enum") * P("typedef") * spc * P("enum") * spc * V("ENUM_TYPE") * Cc(nil) * V("ENUM_BODY") * name    * Cc(nil)            * P(";") * spc ),
	
	--  {
	--      "function",
	--      "return type",
	--      "name",
	--      { <arguments> },
	--      { <statements> },
	--  }
	FUNC_ARG_LIST = Ct( S("(") * spc * (V("ARG") * (comma * V("ARG")) ^ 0) ^ -1 * S(")") ) * spc,
	FUNC_DEFN = Ct( P(_capture_position) * Cc("function") * name * name * V("FUNC_ARG_LIST") * Ct( V("STATEMENT_BODY") ) ),
	
	--  {
	--      "if",
	--      { <condition>, { <statements> } },  <-- if
	--      { <condition>, { <statements> } },  <-- else if
	--      { <condition>, { <statements> } },  <-- else if
	--      {              { <statements> } },  <-- else
	--  }
	IF_BODY = Ct( V("STATEMENT_BODY") + V("STMT") ),
	IF = Ct( P(_capture_position) * Cc("if") *
		Ct( P("if")      * spc * P("(") * spc * V("EXPR") * P(")") * spc * V("IF_BODY") )      * spc *
		Ct( P("else if") * spc * P("(") * spc * V("EXPR") * P(")") * spc * V("IF_BODY") ) ^ 0  * spc *
		Ct( P("else")                                              * spc * V("IF_BODY") ) ^ -1 * spc
	),
	
	--  {
	--      "file.bt", <line>,
	--      "for",
	--      { <init expr> } OR nil,
	--      { <cond expr> } OR nil,
	--      { <iter expr> } OR nil,
	--      { <statements> },
	--  }
	FOR = Ct( P(_capture_position) * Cc("for") *
		P("for") * spc * P("(") * spc *
			(V("LOCAL_VAR_DEFN") + (#V("VAR_DEFN") * _PARSE_ERROR("Cannot declare non-local variable in 'for' loop initialiser")) + (V("EXPR_OR_NIL") * P(";") * spc)) *
			V("EXPR_OR_NIL") * P(";") * spc *
			V("EXPR_OR_NIL") * P(")") * spc *
			V("IF_BODY") * spc
	),
	
	-- while gets compiled to be a for loop with just a condition (see above)
	WHILE = Ct( P(_capture_position) * Cc("for") *
		P("while") * spc * P("(") * spc * Cc(nil) * V("EXPR") * Cc(nil) * P(")") * spc * V("IF_BODY") * spc
	),
	
	--  {
	--      "file.bt", <line>,
	--      "switch",
	--      <expr>,
	--      {
	--          { <case expr> (nil for "default"), { <case statements> } },
	--          ...
	--      }
	--  }
	NOT_CASE_OR_DEFAULT = #P(1) * -(P("case") + P("default")),
	SWITCH = Ct(P(_capture_position) * Cc("switch") *
		P("switch") * spc * P("(") * spc * V("EXPR") * P(")") * spc * Ct(
			V("BRACE_BLOCK_OPEN") *
			V("BRACE_BLOCK_CONTINUE") * (
				(
					Ct( P("case") * spc_req * V("EXPR") * P(":") * spc *
						Ct( ( V("BRACE_BLOCK_CONTINUE") * V("NOT_CASE_OR_DEFAULT") * (V("STMT") + _PARSE_ERROR()) ) ^ 0 ) ) +
					
					Ct( P("default") * spc * Cc(nil) * P(":") * spc *
						Ct( ( V("BRACE_BLOCK_CONTINUE") * V("NOT_CASE_OR_DEFAULT") * (V("STMT") + _PARSE_ERROR()) ) ^ 0 ) )
				) ^ 1
			) *
			V("BRACE_BLOCK_CLOSE")
		)
	),
	
	--  {
	--      "file.bt", <line>,
	--      "break",
	--  }
	BREAK = Ct( P(_capture_position) * Cc("break") * P("break") * spc ),
	
	--  {
	--      "file.bt", <line>,
	--      "continue",
	--  }
	CONTINUE = Ct( P(_capture_position) * Cc("continue") * P("continue") * spc ),
}

local function _resolve_pos(s)
	if s[1] ~= "_input_stream_pos"
	then
		error("Internal error: _resolve_pos called with filename '" .. s[1] .. "'")
	end
	
	s[1], s[2] = input_pos_to_file_and_line_num(s[2])
end

local function _compile_expr(expr)
	_resolve_pos(expr)
	
	local expr_parts = expr[4]
	
	if expr[3] ~= "_expr"
	then
		error("Internal error - _compile_expr() called with an '" .. expr[3] .. "' node")
	end
	
	-- Placeholders for operands and file/line in ops below
	local _1 = {}
	local _2 = {}
	local _F = {}
	local _L = {}
	
	local expand_op
	expand_op = function(filename, line_num, op, operand_1, operand_2)
		local ret = {}
		
		for i,v in ipairs(op)
		do
			if v == _1
			then
				ret[i] = operand_1
			elseif v == _2
			then
				ret[i] = operand_2
			elseif v == _F
			then
				ret[i] = filename
			elseif v == _L
			then
				ret[i] = line_num
			elseif type(v) == "table"
			then
				ret[i] = expand_op(filename, line_num, v, operand_1, operand_2)
			else
				ret[i] = v
			end
		end
		
		return ret
	end
	
	local left_to_right = { start = function() return 1 end, step = 1 }
	local right_to_left = { start = function(e) return #expr_parts - e end, step = -1 }
	
	local expand_binops = function(dir, ops)
		local idx = dir.start(2)
		
		while idx >= 1 and (idx + 2) <= #expr_parts
		do
			local matched = false
			
			for op, ast_op in pairs(ops)
			do
				if
					expr_parts[idx + 1][3] == "_token" and expr_parts[idx + 1][4] == op and
					expr_parts[idx][3]:sub(1, 1) ~= "_" and
					expr_parts[idx + 2][3]:sub(1, 1) ~= "_"
				then
					expr_parts[idx] = expand_op(expr_parts[idx + 1][1], expr_parts[idx + 1][2], ast_op, expr_parts[idx], expr_parts[idx + 2])
					table.remove(expr_parts, idx + 1)
					table.remove(expr_parts, idx + 1)
					
					matched = true
					break
				end
			end
			
			if not matched
			then
				idx = idx + dir.step
			elseif idx == #expr_parts
			then
				idx = idx + 2 * dir.step
			end
		end
	end
	
	local expand_unary_ops = function(dir, ops)
		local idx = dir.start(1)
		
		while idx >= 1 and (idx + 1) <= #expr_parts
		do
			local matched = false
			
			for op, ast_op in pairs(ops)
			do
				if
					(idx == 1 or expr_parts[idx - 1][3] == "_token") and
					expr_parts[idx][3] == "_token" and expr_parts[idx][4] == op and
					expr_parts[idx + 1][3]:sub(1, 1) ~= "_"
				then
					expr_parts[idx] = expand_op(expr_parts[idx][1], expr_parts[idx][2], ast_op, expr_parts[idx + 1])
					table.remove(expr_parts, idx + 1)
					
					matched = true
					break
				end
			end
			
			if not matched
			then
				idx = idx + dir.step
			elseif idx == #expr_parts
			then
				idx = idx + 1 * dir.step
			end
		end
	end
	
	for i = 1, #expr_parts
	do
		if expr_parts[i][3] == "_expr"
		then
			_compile_expr(expr_parts[i])
		else
			_resolve_pos(expr_parts[i])
		end
		
		if expr_parts[i][3] == "ref"
		then
			local path = expr_parts[i][4]
			
			for i = 1, #path
			do
				if type(path[i]) == "table"
				then
					_compile_expr(path[i])
				end
			end
		elseif expr_parts[i][3] == "call"
		then
			local args = expr_parts[i][5]
			
			for i = 1, #args
			do
				_compile_expr(args[i])
			end
		elseif expr_parts[i][3] == "cast"
		then
			local sub_expr = expr_parts[i][5]
			_compile_expr(sub_expr)
		elseif expr_parts[i][3] == "postfix-increment"
		then
			local sub_expr = expr_parts[i][4]
			_resolve_pos(sub_expr)
		elseif expr_parts[i][3] == "postfix-decrement"
		then
			local sub_expr = expr_parts[i][4]
			_resolve_pos(sub_expr)
		elseif expr_parts[i][3] == "_ternary_t"
		then
			local truth_expr = expr_parts[i][4]
			_compile_expr(truth_expr)
		end
	end
	
	expand_unary_ops(right_to_left, {
		-- Convert prefix increment/decrement to assignment
		["++"] = { _F, _L, "assign", _1, { _F, _L, "add",      _1, { _F, _L, "num", 1 } } },
		["--"] = { _F, _L, "assign", _1, { _F, _L, "subtract", _1, { _F, _L, "num", 1 } } },
		
		["!"] = { _F, _L, "logical-not", _1 },
		["~"] = { _F, _L, "bitwise-not", _1 },
		
		["+"] = { _F, _L, "plus",  _1 },
		["-"] = { _F, _L, "minus", _1 },
	})
	
	expand_binops(left_to_right, {
		["*"] = { _F, _L, "multiply", _1, _2 },
		["/"] = { _F, _L, "divide", _1, _2 },
		["%"] = { _F, _L, "mod", _1, _2 },
	})
	
	expand_binops(left_to_right, {
		["+"] = { _F, _L, "add", _1, _2 },
		["-"] = { _F, _L, "subtract", _1, _2 },
	})
	
	expand_binops(left_to_right, {
		["<<"] = { _F, _L, "left-shift", _1, _2 },
		[">>"] = { _F, _L, "right-shift", _1, _2 },
	})
	
	expand_binops(left_to_right, {
		["<"]  = { _F, _L, "less-than", _1, _2 },
		["<="] = { _F, _L, "less-than-or-equal", _1, _2 },
		[">"]  = { _F, _L, "greater-than", _1, _2 },
		[">="] = { _F, _L, "greater-than-or-equal", _1, _2 },
	})
	
	expand_binops(left_to_right, {
		["=="] = { _F, _L, "equal", _1, _2 },
		["!="] = { _F, _L, "not-equal", _1, _2 },
	})
	
	expand_binops(left_to_right, { ["&"] = { _F, _L, "bitwise-and", _1, _2 } })
	expand_binops(left_to_right, { ["^"] = { _F, _L, "bitwise-xor", _1, _2 } })
	expand_binops(left_to_right, { ["|"] = { _F, _L, "bitwise-or", _1, _2 } })
	
	expand_binops(left_to_right, { ["&&"] = { _F, _L, "logical-and", _1, _2 } })
	expand_binops(left_to_right, { ["||"] = { _F, _L, "logical-or", _1, _2 } })
	
	for tn_idx = #expr_parts - 1, 1, -1
	do
		if expr_parts[tn_idx + 1][3] == "_ternary_t" and expr_parts[tn_idx][3]:sub(1, 1) ~= "_"
		then
			local ternary_op = { expr_parts[tn_idx + 1][1], expr_parts[tn_idx + 1][2], "ternary", expr_parts[tn_idx], expr_parts[tn_idx + 1][4] }
			
			expr_parts[tn_idx] = ternary_op
			expr_parts[tn_idx + 1] = { "x", -1, "_ternary_ref", ternary_op }
		end
	end
	
	expand_binops(right_to_left, {
		["="] = { _F, _L, "assign", _1, _2 },
		
		["+="]  = { _F, _L, "assign", _1, { _F, _L, "add",         _1, _2 } },
		["-="]  = { _F, _L, "assign", _1, { _F, _L, "subtract",    _1, _2 } },
		["*="]  = { _F, _L, "assign", _1, { _F, _L, "multiply",    _1, _2 } },
		["/="]  = { _F, _L, "assign", _1, { _F, _L, "divide",      _1, _2 } },
		["%="]  = { _F, _L, "assign", _1, { _F, _L, "mod",         _1, _2 } },
		["<<="] = { _F, _L, "assign", _1, { _F, _L, "left-shift",  _1, _2 } },
		[">>="] = { _F, _L, "assign", _1, { _F, _L, "right-shift", _1, _2 } },
		["&="]  = { _F, _L, "assign", _1, { _F, _L, "bitwise-and", _1, _2 } },
		["^="]  = { _F, _L, "assign", _1, { _F, _L, "bitwise-xor", _1, _2 } },
		["|="]  = { _F, _L, "assign", _1, { _F, _L, "bitwise-or",  _1, _2 } },
	})
	
	for tn_idx = #expr_parts - 2, 1, -1
	do
		if expr_parts[tn_idx + 1][3] == "_ternary_ref" and expr_parts[tn_idx + 2][3]:sub(1, 1) ~= "_"
		then
			table.insert(expr_parts[tn_idx + 1][4], expr_parts[tn_idx + 2]);
			table.remove(expr_parts, tn_idx + 1)
			table.remove(expr_parts, tn_idx + 1)
		end
	end
	
	if #expr_parts ~= 1
	then
		error("Unable to compile expression starting at " .. expr[1] .. ":" .. expr[2])
	end
	
	-- Replace expr's content with the compiled expression in expr_parts[1]
	
	while #expr > 0
	do
		table.remove(expr, #expr)
	end
	
	for _,v in ipairs(expr_parts[1])
	do
		table.insert(expr, v)
	end
end

local function _compile_statement(s)
	local op = s[3]
	
	if op == "_expr"
	then
		_compile_expr(s)
		return
	end
	
	_resolve_pos(s)
	
	if op == "function"
	then
		local body = s[7]
		
		for i = 1, #body
		do
			_compile_statement(body[i])
		end
	elseif op == "struct"
	then
		local body = s[6]
		local var_decl = s[8]
		
		for i = 1, #body
		do
			_compile_statement(body[i])
		end
		
		if var_decl ~= nil
		then
			local var_params = var_decl[2]
			local var_array_size = var_decl[3]
			
			for _, vp in ipairs(var_params)
			do
				_compile_expr(vp)
			end
			
			if var_array_size ~= nil then _compile_expr(var_array_size) end
		end
	elseif op == "enum"
	then
		local members = s[6]
		
		for i = 1, #members
		do
			local value_expr = members[i][2]
			
			if value_expr ~= nil
			then
				_compile_expr(value_expr)
			end
		end
	elseif op == "local-variable"
	then
		local arguments = s[6]
		local array_size = s[7]
		local init_val = s[8]
		
		if arguments ~= nil
		then
			for i = 1, #arguments
			do
				_compile_expr(arguments[i])
			end
		end
		
		if array_size then _compile_expr(array_size) end
		if init_val   then _compile_expr(init_val)   end
	elseif op == "variable"
	then
		local arguments = s[6]
		local array_size = s[7]
		local attributes = s[8]
		
		if arguments ~= nil
		then
			for i = 1, #arguments
			do
				_compile_expr(arguments[i])
			end
		end
		
		if array_size then _compile_expr(array_size) end
		
		if attributes ~= nil
		then
			for i = 1, #attributes
			do
				_resolve_pos(attributes[i])
				
				if attributes[i][4] ~= nil
				then
					_compile_expr(attributes[i][4])
				end
			end
		end
	elseif op == "typedef"
	then
		local array_size = s[6]
		if array_size then _compile_expr(array_size) end
	elseif op == "for"
	then
		local init_expr = s[4]
		local cond_expr = s[5]
		local iter_expr = s[6]
		local body      = s[7]
		
		if init_expr then _compile_statement(init_expr) end
		if cond_expr then _compile_expr(cond_expr) end
		if iter_expr then _compile_expr(iter_expr) end
		
		for i = 1, #body
		do
			_compile_statement(body[i])
		end
	elseif op == "switch"
	then
		local expr  = s[4]
		local cases = s[5]
		
		_compile_expr(expr)
		
		for _, case in ipairs(cases)
		do
			local case_expr = case[1]
			local case_body = case[2]
			
			if case_expr ~= nil then _compile_expr(case_expr) end
			
			for _, statement in ipairs(case_body)
			do
				_compile_statement(statement)
			end
		end
	elseif op == "if"
	then
		for i = 4, #s
		do
			local branch = s[i]
			
			local condition  = #branch > 1 and branch[1] or nil
			local statements = #branch > 1 and branch[2] or branch[1]
			
			if condition ~= nil
			then
				_compile_expr(condition)
			end
			
			for _, s in pairs(statements)
			do
				_compile_statement(s)
			end
		end
	elseif op == "return"
	then
		local result = s[4]
		if result ~= nil then _compile_expr(result) end
	elseif op == "block"
	then
		local body = s[4]
		
		for _, s in pairs(body)
		do
			_compile_statement(s)
		end
	end
end

local function parse_text(text)
	build_lines_table(text)
	_open_statements = {}
	
	local ast = _parser:match(text)
	
	for i,v in ipairs(ast)
	do
		_compile_statement(v)
	end
	
	return ast
end

M.parse_text = parse_text;

-- local inspect = require 'inspect'
-- print(inspect(M.parser:match(io.input():read("*all"))));

return M
