--- Log Display for Lua Errors
-- @module 04_LogDisplay
-- This is a little fake actor class meant for displaying lines of a log.
-- It's placed inside Def, but it's actually just an ActorFrame with some
--   children and special commands.
-- LogDisplay listens for several messages that control its behavior.
-- The name given to the LogDisplay is added to the name of the message to
--   make the messages unique to this LogDisplay.  So to issue a command to
--   the LogDisplay, broadcast the message "<command><name>".
--   For a LogDisplay named "Foo", you would broadcase "Foo" to add to the
--   log, "ToggleFoo" to toggle whether it's shown or hidden, and so on.
-- Some of the control messages take tables of args.
-- Arg elements of the form "name= type" are named, and must be a value of
--   the given type.
-- Arg elements of the form "name" are unnamed.
-- The messages are:
--   <name>:  params= {message= string, dont_show= bool}
--     Adds message to the log to be shown when revealing.
--     If dont_show is false, then Show will be automatically executed with
--     auto_hide= true.
--     Messages with line breaks will take up multiple lines.
--     The newest message in the log has index 1.
--   Toggle:  No params
--     Executes either Show or Hide, depending on the current state.
--   Show:  params= {range= {last_message, lines}, auto_hide= bool}
--     Shows the messages currently in the log.
--     range[1] is the index of the last message to show.
--     range[2] is the max number of lines to show.
--     range[1] defaults to a cleavage shot.
--     range[2] defaults to MaxDisplayLines.
--     If auto_hide is non-nil, Hide will be execuated after a short time.
--     If auto_hide is a number, it will be used as the time to wait before
--       hiding.  Otherwise, the Times.show will be used.
--   Query:  No params
--     Broadcasts "<name>Response" message with a table containing:
--       {log_len= number, hidden= bool}
--       log_len is the number of messages currently in the log.
--       hidden is whether the LogDisplay is currently hidden.
--   Hide:  No params
--     Hides the LogDisplay.
--   Clear:  params= {messages}
--     Clears the messages in the log.  If messages is a number, only clears
--     that many messages, oldest first.
--
-- params to Def.LogDisplay():
-- {
--   Name= string
--     The name will be used as the name of the main ActorFrame and to
--     control what messages the LogDisplay listens for.
--   MaxLogLen= number
--     The maximum number of messages to store in the log.
--   MaxDisplayLines= number
--     The maximum number of lines that can be displayed at a time.
--   ReplaceLinesWhenHidden= bool
--     If this is true, any messages recieved while the LogDisplay is hidden
--     will replace the message currently at the front of the log.
--     If this is false, messages recieved while the LogDisplay is hidden
--     will push other messages back.  Messages pushed past MaxLines will be
--     removed.
--   IgnoreIdentical= bool
--     If true, then a message that is identical to one already in the log
--     will be ignored.
--   Font= font name
--     The name of the font to use.  This will be passed to THEME:GetPathF,
--     so it should not be a path.
--   LineHeight= number
--     The height in pixels to use between lines.
--   LineWidth= number
--     The width that lines should be limited to.
--   TextZoom= number
--     The zoom factor to apply to the text.
--   TextColor= color
--     The color to use for the text.
--   Times= {show= number, hide= number}
--     show is the amount of time to wait before automatically hiding when
--     Show is executed with AutoHide == true.  It's passed as a table, so
--     if you keep a copy of the table, you should be able to modify it to
--     change the time.
--     hide is used as the time for the default hide command to hide.
--   Indent= number
--     The amount in pixels to indent lines that were part of a multi-line
--     message.
--   Hide= function(self)
--     This command will be executed when the LogDisplay needs to be hidden.
--     Hide will also be executed during the InitCommand, so the
--     LogDisplay will start in the hidden state.
--   Show= function(self, Lines)
--     This command will be executed when the LogDisplay needs to be shown.
--     Lines is the number of lines to show.
-- }
-- Any actors placed in params will be used and drawn behind the text.
-- If there are no actors in params, a quad filling the area will be used.
-- Reasonable defaults are provided for everything except Name.  If Name is
--   blank, you get nothing.  Defaults assume the LogDisplay should fill the
--   screen when in use.

-- Below is the implementation of the above features.

local log_display_mt = {
	__index = {
		create_actors = function(self, params)
			self.name = params.Name
			self.font = params.Font or "Common Normal"
			self.line_height = params.LineHeight or 12
			self.line_width = params.LineWidth or SCREEN_WIDTH
			self.text_zoom = params.TextZoom or .5
			self.text_color = params.TextColor or color("#93a1a1")
			local lines_on_screen = params.MaxLines or SCREEN_HEIGHT / self.line_height
			self.max_lines = math.floor(lines_on_screen)
			self.add_to_height = (lines_on_screen - self.max_lines) * self.line_height
			self.max_log = params.MaxLogLen or self.max_lines
			self.indent = params.Indent or 8
			self.times = params.Times
			self.param_hide = params.Hide or function(subself)
					subself:linear(params.Times.hide)
					subself:y(-SCREEN_HEIGHT)
				end
			self.param_show = params.Show or function(subself, lines)
					subself:visible(true)
					subself:linear(params.Times.hide)
					local cover = math.min(self.line_height * (lines + .5), SCREEN_HEIGHT)
					subself:y(-SCREEN_HEIGHT + cover + self.add_to_height)
				end

			self.text_actors = {}
			self.message_log = {}
			self.messes_since_update = 0

			local name_mess = self.name .. "MessageCommand"
			local args = {
				Name = self.name,
				InitCommand = function(subself)
					self.container = subself
					if params.InitCommand then params.InitCommand(subself) end
					self:hide()
				end,
				OffCommand = function(subself)
					subself:visible(false)
					for i, actor in ipairs(self.text_actors) do
						actor:visible(false)
					end
				end,
				[name_mess] = function(subself, mess)
					if not PREFSMAN:GetPreference("ShowThemeErrors") and self.name == "ScriptError" then
						subself:visible(false)
						return
					end
					if not mess.message then
						return
					end
					if self.messes_since_update > self.max_log then
						return
					end
					-- Long ago, someone decided that "::" should be an alias for "\n"
					-- and hardcoded it into BitmapText.
					local message = tostring(mess.message):gsub("::", ":")
					if params.IgnoreIdentical and not self.hidden then
						for i, prevmess in ipairs(self.message_log) do
							if message == prevmess then
								return
							end
						end
					end
					if params.ReplaceLinesWhenHidden and self.hidden then
						self:clear()
						self.message_log[1] = message
					else
						table.insert(self.message_log, 1, message)
						if #self.message_log > self.max_log then
							table.remove(self.message_log)
						end
					end
					if not mess.dont_show or not self.hidden then
						self.messes_since_update = self.messes_since_update + 1
						self.hidden = false
						subself:stoptweening()
						subself:queuecommand("Update")
					end
				end,
				["Toggle" .. name_mess] = function(subself)
					if self.hidden then
						self:show()
					else
						self:hide()
					end
				end,
				["Hide" .. name_mess] = function(subself)
					self:hide()
				end,
				["Show" .. name_mess] = function(subself, mess)
					self:show(mess.range, mess.auto_hide)
				end,
				["Clear" .. name_mess] = function(subself, mess)
					self:clear(mess[1])
				end,
				UpdateCommand = function(subself)
					self:show(nil, true)
					self.messes_since_update = 0
				end,
				LoadFont(self.font) ..
					{
						Name = "WidthTester",
						InitCommand = function(subself)
							self.width_tester = subself
							subself:zoom(self.text_zoom)
							subself:visible(false)
						end
					}
			}
			if #params == 0 then
				args[#args + 1] =
					Def.Quad {
					Name = "Logbg",
					InitCommand = function(subself)
						subself:setsize(self.line_width, self.line_height * self.max_lines)
						subself:horizalign(left)
						subself:vertalign(top)
						subself:diffuse(color("#002b36"))
						subself:diffusealpha(.85)
					end
				}
			else
				-- Add bg actors passed through params.
				for i, actor in ipairs(params) do
					args[#args + 1] = actor
				end
			end
			-- Add commands passed through params.
			for name, command in pairs(params) do
				if type(name) == "string" and name:find("Command") and not args[name] then
					args[name] = command
				end
			end
			for i = 1, self.max_lines do
				args[#args + 1] =
					LoadFont(self.font) ..
					{
						Name = "Text" .. i,
						InitCommand = function(subself)
							-- Put them in the list in reverse order so the ones at the
							-- bottom of the screen are used first.
							self.text_actors[self.max_lines - i + 1] = subself
							subself:horizalign(left)
							subself:vertalign(top)
							subself:y(self.line_height * (i - 1))
							subself:zoom(self.text_zoom)
							subself:diffuse(self.text_color)
							subself:visible(false)
						end,
						OffCommand = function(self)
							self:visible(false)
						end
					}
			end
			return Def.ActorFrame(args)
		end,
		hide = function(self)
			self.container:stoptweening()
			self.param_hide(self.container)
			self.container:queuecommand("Off")
			self.hidden = true
		end,
		show = function(self, range, auto_hide)
			if not range then
				range = {}
			end
			local start = range[1] or 1
			local lmax = range[2] or self.max_lines
			local indented_lines = {}
			local next_message = start
			local num_lines = 0
			while num_lines < lmax and self.message_log[next_message] do
				self.width_tester:settext(self.message_log[next_message])
				local lines_to_add = convert_text_to_indented_lines(self.width_tester, self.indent, self.line_width, self.text_zoom)
				indented_lines[#indented_lines + 1] = lines_to_add
				num_lines = num_lines + #lines_to_add
				next_message = next_message + 1
			end
			local use_next = 1
			local used = 0
			for i, mess in ipairs(indented_lines) do
				for l, line in ipairs(mess) do
					-- Start at the end of the mess because the text actors are in
					-- reverse order.
					local ind = use_next + (#mess - l)
					local actor = self.text_actors[ind]
					if actor and used <= lmax then
						actor:settext(line[2])
						local indent = 8 + self.indent * line[1]
						local lw = self.line_width - (indent + 8)
						width_limit_text(actor, lw, self.text_zoom)
						actor:x(indent)
						actor:visible(true)
						used = used + 1
					end
				end
				use_next = use_next + #mess
			end
			self.container:stoptweening()
			self.param_show(self.container, used)
			self.hidden = false
			if auto_hide then
				self.container:sleep(self.times.show)
				self.container:queuecommand("Hide" .. self.name)
			end
		end,
		clear = function(self, messes)
			if #self.message_log < 1 then
				return
			end
			if not messes then
				self.message_log = {}
			else
				for i = 1, messes do
					table.remove(self.message_log)
					if #self.message_log < 1 then
						break
					end
				end
			end
		end
	}
}

function Def.LogDisplay(params)
	if type(params.Name) ~= "string" or params.Name == "" then
		ReportScriptError("Cannot create a LogDisplay without a name.")
		return nil
	end

	if not params.Times then
		params.Times = {show = 4, hide = .125}
	end
	if not params.Times.show then
		params.Times.show = 4
	end
	if not params.Times.hide then
		params.Times.hide = .125
	end

	local new_log_display = setmetatable({}, log_display_mt)
	_G[params.Name .. "LogDisplay"] = new_log_display
	return new_log_display:create_actors(params)
end

--[[
Copyright © 2014 Eric Reese / Kyzentun
All rights reserved.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
]]
