--- original: https://github.com/tionis/lua-yaml/blob/7a982f56b54170d81bbe44f8c0a0515af6bca74a/yaml.lua

---yaml.lua
--LUA YAML parser, based on js-lua, fast and tiny.
--Although this implementation does not currently support the entire YAML specification,
--feel free to fork the project and submit a patch :)
-- https://github.com/exosite/lua-yaml

local Generator = {}
function Generator.new(self)
	return self
end

Generator.tablePrintValue = function(self, value, indent, done)
	indent = indent or 0
	done = done or {}
	if type(value) == "table" and not done[value] then
		done[value] = true

		local list = {}
		for key in pairs(value) do
			list[#list + 1] = key
		end
		table.sort(list, function(a, b)
			return tostring(a) < tostring(b)
		end)

		local rep = "\n"
		for _, key in ipairs(list) do
			local keyRep
			if type(key) == "number" then
				keyRep = "- "
			else
				keyRep = tostring(key) .. ": "
			end
			rep = rep
				.. string.format(
					"%s%s%s\n",
					string.rep(" ", indent),
					keyRep,
					self:tablePrintValue(value[key], indent + 2, done)
				)
		end

		rep = rep .. string.rep(" ", indent)

		done[value] = false
		return rep
	elseif type(value) == "string" then
		return string.format("%q", value)
	else
		return tostring(value)
	end
end

local function trimBlankLine(str)
	local lines = {}
	for s in str:gmatch("[^\r\n]+") do
		s = string.gsub(s, "%s+$", "")
		if #s > 0 then
			table.insert(lines, s)
		end
	end
	return table.concat(lines, "\n")
end

local table_clone = function(t)
	local clone = {}
	for k, v in pairs(t) do
		clone[k] = v
	end
	return clone
end

local string_trim = function(s, what)
	what = what or " "
	return s:gsub("^[" .. what .. "]*(.-)[" .. what .. "]*$", "%1")
end

local push = function(stack, item)
	stack[#stack + 1] = item
end

local pop = function(stack)
	local item = stack[#stack]
	stack[#stack] = nil
	return item
end

local context = function(str)
	if type(str) ~= "string" then
		return ""
	end

	str = str:sub(0, 25):gsub("\n", "\\n"):gsub('"', '\\"')
	return ', near "' .. str .. '"'
end

local Parser = {}
function Parser.new(self, tokens)
	self.tokens = tokens
	self.parse_stack = {}
	self.refs = {}
	self.current = 0
	return self
end

local exports = { version = "1.3" }

local word = function(w)
	return "^(" .. w .. ")([%s$%c])"
end

local tokens = {
	{ "yaml-version", "^%%YAML ([0-9%.]*)" },
	{ "comment", "^[\n ]+#[^\n]*" },
	{ "indent", "^[\n]+( *)" },
	{ "space", "^ +" },
	{ "true", word("enabled"), const = true, value = true },
	{ "true", word("true"), const = true, value = true },
	{ "true", word("yes"), const = true, value = true },
	{ "true", word("on"), const = true, value = true },
	{ "false", word("disabled"), const = true, value = false },
	{ "false", word("false"), const = true, value = false },
	{ "false", word("no"), const = true, value = false },
	{ "false", word("off"), const = true, value = false },
	{ "null", word("null"), const = true, value = nil },
	{ "null", word("Null"), const = true, value = nil },
	{ "null", word("NULL"), const = true, value = nil },
	{ "null", word("~"), const = true, value = nil },
	{ "id", '^"([^"]-)" *(:[%s%c])' },
	{ "id", "^'([^']-)' *(:[%s%c])" },
	{ "string", '^"([^"]-)"', force_text = true },
	{ "string", "^'([^']-)'", force_text = true },
	{ "timestamp", "^(%d%d%d%d)-(%d%d?)-(%d%d?)%s+(%d%d?):(%d%d):(%d%d)%s+(%-?%d%d?):(%d%d)" },
	{ "timestamp", "^(%d%d%d%d)-(%d%d?)-(%d%d?)%s+(%d%d?):(%d%d):(%d%d)%s+(%-?%d%d?)" },
	{ "timestamp", "^(%d%d%d%d)-(%d%d?)-(%d%d?)%s+(%d%d?):(%d%d):(%d%d)" },
	{ "timestamp", "^(%d%d%d%d)-(%d%d?)-(%d%d?)%s+(%d%d?):(%d%d)" },
	{ "timestamp", "^(%d%d%d%d)-(%d%d?)-(%d%d?)%s+(%d%d?)" },
	{ "timestamp", "^(%d%d%d%d)-(%d%d?)-(%d%d?)" },
	{ "doc", "^%-%-%-[^%c]*" },
	{ ",", "^," },
	{ "string", "^%b{} *[^,%c]+", noinline = true },
	{ "{", "^{" },
	{ "}", "^}" },
	{ "string", "^(%b[] *[^%c #]+[^%c]-) #[^\n]*", noinline = true }, -- Any letter/punctuation after '[...]', before '#' is string (after '#' comment)
	{ "string", "^(%b[] *[^%c #]+[^%c]+)", noinline = true }, -- Any letter/punctuation after '[...]' is string
	{ "[", "^%[" },
	{ "]", "^%]" },
	{ "string", "^-[^%s]+", noinline = true },
	{ "-", "^%-", noinline = true },
	{ ":", "^:" },
	{ "pipe", "^(|)(%d*[+%-]?)", sep = "\n" },
	{ "pipe", "^(>)(%d*[+%-]?)", sep = " " },
	{ "id", "^([%w/$][%w %-_\\.]*)(:[%s%c])" },
	{ "string", "^([^%c]-)( #)[^\n]+", noinline = true }, --String with " #comment"
	{ "string", "^([^%c]+)", noinline = true },
	{ "string", "^[^,%]}%c ]+" },
}

exports.tokenize = function(str)
	local token
	local row = 0
	local ignore
	local indents = 0
	local lastIndents
	local stack = {}
	local indentAmount = 0
	local inline = false
	str = str:gsub("\r\n", "\010")

	while #str > 0 do
		for i in ipairs(tokens) do
			local captures = {}
			if not inline or tokens[i].noinline == nil then
				captures = { str:match(tokens[i][2]) }
			end

			if #captures > 0 then
				captures.input = str:sub(0, 50)
				token = table_clone(tokens[i])
				token[2] = captures
				local str2 = str:gsub(tokens[i][2], "", 1)
				token.raw = str:sub(1, #str - #str2)
				str = str2
				if token[1] == "{" or token[1] == "[" then
					inline = true
				elseif token.const then
					-- Since word pattern contains last char we're re-adding it
					str = token[2][2] .. str
					token.raw = token.raw:sub(1, #token.raw - #token[2][2])
				elseif token[1] == "id" then
					-- Since id pattern contains last semi-colon we're re-adding it
					str = token[2][2] .. str
					token.raw = token.raw:sub(1, #token.raw - #token[2][2])
					-- Trim
					token[2][1] = string_trim(token[2][1])
				elseif token[1] == "string" then
					token[2][1] = string_trim(token[2][1])
					-- Finding numbers
					local snip = token[2][1]
					if not token.force_text then
						if snip:match("^(-?%d+%.%d+)$") or snip:match("^(-?%d+)$") then
							token[1] = "number"
						end
					end
				elseif token[1] == "comment" then
					ignore = true
				elseif token[1] == "indent" then
					row = row + 1
					inline = false
					lastIndents = indents
					if indentAmount == 0 then
						indentAmount = #token[2][1]
					end
					if indentAmount ~= 0 then
						indents = (#token[2][1] / indentAmount)
					else
						indents = 0
					end

					if indents == lastIndents then
						ignore = true
					elseif indents > lastIndents + 2 then
						error(
							"SyntaxError: invalid indentation, got "
								.. tostring(indents)
								.. " instead of "
								.. tostring(lastIndents)
								.. context(token[2].input)
						)
					elseif indents > lastIndents + 1 then
						push(stack, token)
					elseif indents < lastIndents then
						local input = token[2].input
						token = { "dedent", { "", input = "" } }
						token.input = input
						while lastIndents > indents + 1 do
							lastIndents = lastIndents - 1
							push(stack, token)
						end
					end
				end -- if token[1] == XXX
				token.row = row
				break
			end -- if #captures > 0
		end

		if not ignore then
			if token then
				push(stack, token)
				token = nil
			else
				error("SyntaxError " .. context(str))
			end
		end

		ignore = false
	end

	return stack
end

Parser.peek = function(self, offset)
	offset = offset or 1
	return self.tokens[offset + self.current]
end

Parser.advance = function(self)
	self.current = self.current + 1
	return self.tokens[self.current]
end

Parser.advanceValue = function(self)
	return self:advance()[2][1]
end

Parser.accept = function(self, type)
	if self:peekType(type) then
		return self:advance()
	end
end

Parser.expect = function(self, type, msg)
	return self:accept(type) or error(msg .. context(self:peek()[1].input))
end

Parser.expectDedent = function(self, msg)
	return self:accept("dedent") or (self:peek() == nil) or error(msg .. context(self:peek()[2].input))
end

Parser.peekType = function(self, val, offset)
	return self:peek(offset) and self:peek(offset)[1] == val
end

Parser.ignore = function(self, items)
	local advanced
	repeat
		advanced = false
		for _, v in pairs(items) do
			if self:peekType(v) then
				self:advance()
				advanced = true
			end
		end
	until advanced == false
end

Parser.ignoreSpace = function(self)
	self:ignore({ "space" })
end

Parser.ignoreWhitespace = function(self)
	self:ignore({ "space", "indent", "dedent" })
end

Parser.parse = function(self)
	local ref = nil
	if self:peekType("string") and not self:peek().force_text then
		local char = self:peek()[2][1]:sub(1, 1)
		if char == "&" then
			ref = self:peek()[2][1]:sub(2)
			self:advanceValue()
			self:ignoreSpace()
		elseif char == "*" then
			ref = self:peek()[2][1]:sub(2)
			return self.refs[ref]
		end
	end

	local result
	local c = {
		indent = self:accept("indent") and 1 or 0,
		token = self:peek(),
	}
	push(self.parse_stack, c)

	if c.token then
		if c.token[1] == "yaml-version" then
			result = self:parseYamlVersion()
		elseif c.token[1] == "doc" then
			result = self:parseDoc()
		elseif c.token[1] == "-" then
			result = self:parseList()
		elseif c.token[1] == "{" then
			result = self:parseInlineHash()
		elseif c.token[1] == "[" then
			result = self:parseInlineList()
		elseif c.token[1] == "id" then
			result = self:parseHash()
		elseif c.token[1] == "string" then
			result = self:parseString("\n")
		elseif c.token[1] == "timestamp" then
			result = self:parseTimestamp()
		elseif c.token[1] == "number" then
			result = tonumber(self:advanceValue())
		elseif c.token[1] == "pipe" then
			result = self:parsePipe()
		elseif c.token.const == true then
			self:advanceValue()
			result = c.token.value
		else
			error("ParseError: unexpected token '" .. c.token[1] .. "'" .. context(c.token.input))
		end
	end

	pop(self.parse_stack)
	while c.indent > 0 do
		c.indent = c.indent - 1
		local term = "term " .. c.token[1] .. ": '" .. c.token[2][1] .. "'"
		self:expectDedent("last " .. term .. " is not properly dedented")
	end

	if ref then
		self.refs[ref] = result
	end

	return result
end

Parser.parseYamlVersion = function(self)
	local version = tonumber(self:advanceValue())
	if version ~= 1.1 then
		error("Unsupported YAML protocol version, has: " .. version .. ", need: 1.1")
	end
	self:accept("yaml-version")
	return self:parse()
end

Parser.parseDoc = function(self)
	self:accept("doc")
	return self:parse()
end

Parser.inline = function(self)
	local current = self:peek(0)
	if not current then
		return {}, 0
	end

	local inline = {}
	local i = 0

	while
		self:peek(i)
		and not self:peekType("indent", i)
		and not self:peekType("doc", i)
		and current.row == self:peek(i).row
	do
		inline[self:peek(i)[1]] = true
		i = i - 1
	end

	return inline, -i
end

Parser.isInline = function(self)
	local _, i = self:inline()
	return i > 0
end

Parser.parent = function(self, level)
	level = level or 1
	return self.parse_stack[#self.parse_stack - level]
end

Parser.parentType = function(self, type, level)
	return self:parent(level) and self:parent(level).token[1] == type
end

Parser.parseString = function(self)
	if self:isInline() then
		local result = self:advanceValue()

		--[[
      - a: this looks
        flowing: but is
        no: string
    --]]
		local types = self:inline()
		if types["id"] and types["-"] then
			if not self:peekType("indent") or not self:peekType("indent", 2) then
				return result
			end
		end

		--[[
      a: 1
      b: this is
        a flowing string
        example
      c: 3
    --]]
		if self:peekType("indent") then
			self:expect("indent", "text block needs to start with indent")
			local addtl = self:accept("indent")

			result = result .. "\n" .. self:parseTextBlock("\n")

			self:expectDedent("text block ending dedent missing")
			if addtl then
				self:expectDedent("text block ending dedent missing")
			end
		end
		return result
	else
		--[[
      a: 1
      b:
        this is also
        a flowing string
        example
      c: 3
    --]]
		return self:parseTextBlock("\n")
	end
end

Parser.parsePipe = function(self)
	local pipe = self:expect("pipe")
	self:expect("indent", "text block needs to start with indent")
	local result = self:parseTextBlock(pipe.sep)
	self:expectDedent("text block ending dedent missing")
	return result
end

Parser.parseTextBlock = function(self, sep)
	local token = self:advance()
	local result = string_trim(token.raw, "\n")
	local indents = 0
	while self:peek() ~= nil and (indents > 0 or not self:peekType("dedent")) do
		local newtoken = self:advance()
		while token.row < newtoken.row do
			result = result .. sep
			token.row = token.row + 1
		end
		if newtoken[1] == "indent" then
			indents = indents + 1
		elseif newtoken[1] == "dedent" then
			indents = indents - 1
		else
			result = result .. string_trim(newtoken.raw, "\n")
		end
	end
	return result
end

Parser.parseHash = function(self, hash)
	hash = hash or {}
	local indents = 0
	if self:isInline() then
		local id = self:advanceValue()
		self:expect(":", "expected semi-colon after id")
		self:ignoreSpace()
		if self:accept("indent") then
			indents = indents + 1
			hash[id] = self:parse()
		else
			hash[id] = self:parse()
			if self:accept("indent") then
				indents = indents + 1
			end
		end
		self:ignoreSpace()
	end

	while self:peekType("id") do
		local id = self:advanceValue()
		self:expect(":", "expected semi-colon after id")
		self:ignoreSpace()
		hash[id] = self:parse()
		self:ignoreSpace()
	end

	while indents > 0 do
		self:expectDedent("expected dedent")
		indents = indents - 1
	end

	return hash
end

Parser.parseInlineHash = function(self)
	local id
	local hash = {}
	local i = 0

	self:accept("{")
	while not self:accept("}") do
		self:ignoreSpace()
		if i > 0 then
			self:expect(",", "expected comma")
		end

		self:ignoreWhitespace()
		if self:peekType("id") then
			id = self:advanceValue()
			if id then
				self:expect(":", "expected semi-colon after id")
				self:ignoreSpace()
				hash[id] = self:parse()
				self:ignoreWhitespace()
			end
		end

		i = i + 1
	end
	return hash
end

Parser.parseList = function(self)
	local list = {}
	while self:accept("-") do
		self:ignoreSpace()
		list[#list + 1] = self:parse()

		self:ignoreSpace()
	end
	return list
end

Parser.parseInlineList = function(self)
	local list = {}
	local i = 0
	self:accept("[")
	while not self:accept("]") do
		self:ignoreSpace()
		if i > 0 then
			self:expect(",", "expected comma")
		end

		self:ignoreSpace()
		list[#list + 1] = self:parse()
		self:ignoreSpace()
		i = i + 1
	end

	return list
end

Parser.parseTimestamp = function(self)
	local capture = self:advance()[2]
	return os.time({
		year = capture[1],
		month = capture[2],
		day = capture[3],
		hour = (capture[4] or 0) + (capture[7] or 0),
		min = (capture[5] or 0) + (capture[8] or 0),
		sec = capture[6] or 0,
		isdst = false,
	})
end

exports.eval = function(str)
	local parser = Parser:new(exports.tokenize(str))
	return parser:parse()
end

exports.dump = function(table)
	local generator = Generator:new()
	local result = generator:tablePrintValue(table)
	result = trimBlankLine(result)
	return result
end

return exports
