--[[
	A module for building complex HTML from Lua using a
	fluent interface.

	Originally written on the English Wikipedia by
	Toohool and Mr. Stradivarius.

	Code released under the GPL v2+ as per:
	https://en.wikipedia.org/w/index.php?diff=next&oldid=581399786
	https://en.wikipedia.org/w/index.php?diff=next&oldid=581403025

	@license GNU GPL v2+
	@author Marius Hoch < hoo@online.de >
]]

local HtmlBuilder = {}

local util = require 'libraryUtil'
local checkType = util.checkType
local checkTypeMulti = util.checkTypeMulti

local metatable = {}
local methodtable = {}

local selfClosingTags = {
	area = true,
	base = true,
	br = true,
	col = true,
	command = true,
	embed = true,
	hr = true,
	img = true,
	input = true,
	keygen = true,
	link = true,
	meta = true,
	param = true,
	source = true,
	track = true,
	wbr = true,
}

local htmlencodeMap = {
	['>'] = '&gt;',
	['<'] = '&lt;',
	['&'] = '&amp;',
	['"'] = '&quot;',
}

metatable.__index = methodtable

metatable.__tostring = function( t )
	local ret = {}
	t:_build( ret )
	return table.concat( ret )
end

-- Get an attribute table (name, value) and its index
--
-- @param name
local function getAttr( t, name )
	for i, attr in ipairs( t.attributes ) do
		if attr.name == name then
			return attr, i
		end
	end
end

-- Is this a valid attribute name?
--
-- @param s
local function isValidAttributeName( s )
	-- Good estimate: http://www.w3.org/TR/2000/REC-xml-20001006#NT-Name
	return s:match( '^[a-zA-Z_:][a-zA-Z0-9_.:-]*$' )
end

-- Is this a valid tag name?
--
-- @param s
local function isValidTag( s )
	return s:match( '^[a-zA-Z0-9]+$' )
end

-- Escape a value, for use in HTML
--
-- @param s
local function htmlEncode( s )
	-- The parentheses ensure that there is only one return value
	return ( string.gsub( s, '[<>&"]', htmlencodeMap ) )
end

local function cssEncode( s )
	-- XXX: I'm not sure this character set is complete.
	-- bug #68011: allow delete character (\127)
	return mw.ustring.gsub( s, '[^\32-\57\60-\127]', function ( m )
		return string.format( '\\%X ', mw.ustring.codepoint( m ) )
	end )
end

-- Create a builder object. This is a separate function so that we can show the
-- correct error levels in both HtmlBuilder.create and metatable.tag.
--
-- @param tagName
-- @param args
local function createBuilder( tagName, args )
	if tagName ~= nil and tagName ~= '' and not isValidTag( tagName ) then
		error( string.format( "invalid tag name '%s'", tagName ), 3 )
	end

	args = args or {}
	local builder = {}
	setmetatable( builder, metatable )
	builder.nodes = {}
	builder.attributes = {}
	builder.styles = {}

	if tagName ~= '' then
		builder.tagName = tagName
	end

	builder.parent = args.parent
	builder.selfClosing = selfClosingTags[tagName] or args.selfClosing or false
	return builder
end

-- Append a builder to the current node. This is separate from methodtable.node
-- so that we can show the correct error level in both methodtable.node and
-- methodtable.wikitext.
--
-- @param builder
local function appendBuilder( t, builder )
	if t.selfClosing then
		error( "self-closing tags can't have child nodes", 3 )
	end

	if builder then
		table.insert( t.nodes, builder )
	end
	return t
end

methodtable._build = function( t, ret )
	if t.tagName then
		table.insert( ret, '<' .. t.tagName )
		for i, attr in ipairs( t.attributes ) do
			table.insert(
				ret,
				-- Note: Attribute names have already been validated
				' ' .. attr.name .. '="' .. htmlEncode( attr.val ) .. '"'
			)
		end
		if #t.styles > 0 then
			table.insert( ret, ' style="' )
			local css = {}
			for i, prop in ipairs( t.styles ) do
				if type( prop ) ~= 'table' then -- added with cssText()
					table.insert( css, htmlEncode( prop ) )
				else -- added with css()
					table.insert(
						css,
						htmlEncode( cssEncode( prop.name ) .. ':' .. cssEncode( prop.val ) )
					)
				end
			end
			table.insert( ret, table.concat( css, ';' ) )
			table.insert( ret, '"' )
		end
		if t.selfClosing then
			table.insert( ret, ' />' )
			return
		end
		table.insert( ret, '>' )
	end
	for i, node in ipairs( t.nodes ) do
		if node then
			if type( node ) == 'table' then
				node:_build( ret )
			else
				table.insert( ret, tostring( node ) )
			end
		end
	end
	if t.tagName then
		table.insert( ret, '</' .. t.tagName .. '>' )
	end
end

-- Append a builder to the current node
--
-- @param builder
methodtable.node = function( t, builder )
	return appendBuilder( t, builder )
end

-- Appends some markup to the node. This will be treated as wikitext.
methodtable.wikitext = function( t, ... )
	local vals = {...}
	for i = 1, #vals do
		checkTypeMulti( 'wikitext', i, vals[i], { 'string', 'number' } )
		appendBuilder( t, vals[i] )
	end
	return t
end

-- Appends a newline character to the node.
methodtable.newline = function( t )
	t:wikitext( '\n' )
	return t
end

-- Appends a new child node to the builder, and returns an HtmlBuilder instance
-- representing that new node.
--
-- @param tagName
-- @param args
methodtable.tag = function( t, tagName, args )
	checkType( 'tag', 1, tagName, 'string' )
	checkType( 'tag', 2, args, 'table', true )
	args = args or {}

	args.parent = t
	local builder = createBuilder( tagName, args )
	t:node( builder )
	return builder
end

-- Get the value of an html attribute
--
-- @param name
methodtable.getAttr = function( t, name )
	checkType( 'getAttr', 1, name, 'string' )

	local attr = getAttr( t, name )
	if attr then
		return attr.val
	end
	return nil
end

-- Set an HTML attribute on the node.
--
-- @param name Attribute to set, alternative table of name-value pairs
-- @param val Value of the attribute. Nil causes the attribute to be unset
methodtable.attr = function( t, name, val )
	if type( name ) == 'table' then
		if val ~= nil then
			error(
				"bad argument #2 to 'attr' " ..
				'(if argument #1 is a table, argument #2 must be left empty)',
				2
			)
		end

		local callForTable = function()
			for attrName, attrValue in pairs( name ) do
				t:attr( attrName, attrValue )
			end
		end

		if not pcall( callForTable ) then
			error(
				"bad argument #1 to 'attr' " ..
				'(table keys must be strings, and values must be strings or numbers)',
				2
			)
		end

		return t
	end

	checkType( 'attr', 1, name, 'string' )
	checkTypeMulti( 'attr', 2, val, { 'string', 'number', 'nil' } )

	-- if caller sets the style attribute explicitly, then replace all styles
	-- previously added with css() and cssText()
	if name == 'style' then
		t.styles = { val }
		return t
	end

	if not isValidAttributeName( name ) then
		error( string.format(
			"bad argument #1 to 'attr' (invalid attribute name '%s')",
			name
		), 2 )
	end

	local attr, i = getAttr( t, name )
	if attr then
		if val ~= nil then
			attr.val = val
		else
			table.remove( t.attributes, i )
		end
	elseif val ~= nil then
		table.insert( t.attributes, { name = name, val = val } )
	end

	return t
end

-- Adds a class name to the node's class attribute. Spaces will be
-- automatically added to delimit each added class name.
--
-- @param class
methodtable.addClass = function( t, class )
	checkTypeMulti( 'addClass', 1, class, { 'string', 'number', 'nil' } )

	if class == nil then
		return t
	end

	local attr = getAttr( t, 'class' )
	if attr then
		attr.val = attr.val .. ' ' .. class
	else
		t:attr( 'class', class )
	end

	return t
end

-- Set a CSS property to be added to the node's style attribute.
--
-- @param name CSS attribute to set, alternative table of name-value pairs
-- @param val The value to set. Nil causes it to be unset
methodtable.css = function( t, name, val )
	if type( name ) == 'table' then
		if val ~= nil then
			error(
				"bad argument #2 to 'css' " ..
				'(if argument #1 is a table, argument #2 must be left empty)',
				2
			)
		end

		local callForTable = function()
			for attrName, attrValue in pairs( name ) do
				t:css( attrName, attrValue )
			end
		end

		if not pcall( callForTable ) then
			error(
				"bad argument #1 to 'css' " ..
				'(table keys and values must be strings or numbers)',
				2
			)
		end

		return t
	end

	checkTypeMulti( 'css', 1, name, { 'string', 'number' } )
	checkTypeMulti( 'css', 2, val, { 'string', 'number', 'nil' } )

	for i, prop in ipairs( t.styles ) do
		if prop.name == name then
			if val ~= nil then
				prop.val = val
			else
				table.remove( t.styles, i )
			end
			return t
		end
	end

	if val ~= nil then
		table.insert( t.styles, { name = name, val = val } )
	end

	return t
end

-- Add some raw CSS to the node's style attribute. This is typically used
-- when a template allows some CSS to be passed in as a parameter
--
-- @param css
methodtable.cssText = function( t, css )
	checkTypeMulti( 'cssText', 1, css, { 'string', 'number', 'nil' } )
	if css ~= nil then
		table.insert( t.styles, css )
	end
	return t
end

-- Returns the parent node under which the current node was created. Like
-- jQuery.end, this is a convenience function to allow the construction of
-- several child nodes to be chained together into a single statement.
methodtable.done = function( t )
	return t.parent or t
end

-- Like .done(), but traverses all the way to the root node of the tree and
-- returns it.
methodtable.allDone = function( t )
	while t.parent do
		t = t.parent
	end
	return t
end

-- Create a new instance
--
-- @param tagName
-- @param args
function HtmlBuilder.create( tagName, args )
	checkType( 'mw.html.create', 1, tagName, 'string', true )
	checkType( 'mw.html.create', 2, args, 'table', true )
	return createBuilder( tagName, args )
end

mw_interface = nil

-- Register this library in the "mw" global
mw = mw or {}
mw.html = HtmlBuilder

package.loaded['mw.html'] = HtmlBuilder

return HtmlBuilder
