local inspect = require('inspect')

-- Utility functions for working with Lua in Fungus
local M = {}

------------
-- Debugging
------------

-- Prints a summary of object v in a human readable format.
function M.inspect(v)
    print (inspect.inspect(v))
end

------------
-- Math
------------

-- rounds a number to the nearest decimal places
-- e.g. round(1.523, 100) -- 1.52
function M.round(val, decimal)
  if (decimal) then
    return math.floor( (val * 10^decimal) + 0.5) / (10^decimal)
  else
    return math.floor(val+0.5)
  end
end

-------------
-- Coroutines
-------------

-- Waits for a number of seconds
function M.wait(duration)
    local t = M.time.timeSinceLevelLoad
    while (M.time.timeSinceLevelLoad - t < duration) do
        coroutine.yield()
    end
end

-- Waits until the lamda function provided returns true, or the timeout expires.
-- Returns true if the function succeeded, or false if the timeout expired
function M.waitfor(fn, timeoutduration)
    local t = M.time.timeSinceLevelLoad
    while (not fn()) do
        coroutine.yield()
        if (M.time.timeSinceLevelLoad - t > timeoutduration) then
            return false
        end
    end
    return true
end

-- Starts a C# coroutine method
function M.run(enumerator)
    -- If the parameter isn't an enumerator then CreateTask will fail
    local status, err = pcall( function() 
        local task = M.luaenvironment.RunUnityCoroutine(enumerator)
    end)

    if (not status) then
        print(debug.traceback("Can't start a coroutine with a c# method that doesn't return IEnumerator", 2))
        error(err)
    end
end

-- Starts a C# coroutine method and waits until it's finished
function M.runwait(enumerator)
    -- If the parameter isn't an enumerator then CreateTask will fail
    local status, err = pcall( function() 
        local task = M.luaenvironment.RunUnityCoroutine(enumerator)
        while (task != nil and task.Running) do
            coroutine.yield();
        end
    end)

    if (not status) then
        print(debug.traceback("Can't start a coroutine with a c# method that doesn't return IEnumerator", 2))
        error(err)
    end
end

---------------
-- String table
---------------

-- Set active language for string table
function M.setlanguage(languagecode)
    M.luautils.ActiveLanguage = languagecode
end

-- Get a named string from the string table
function M.getstring(key)
    return M.luautils.GetString(key)
end

-- Substitutes variables and localisation strings into a piece of text
-- e.g. v = 10, "Subbed value is [$v]" => "Subbed value is 10"
function M.sub(text)
    return M.luautils.substitute(text)
end

--------------------------------------------------------------
-- Integration tests
-- These functions require the Unity Test Tools to work.
--------------------------------------------------------------

-- Checks if a condition is true (requires Unity Test Tools)
-- Lua has a built in assert function, so we called this check to avoid conflicting.
function M.check(c, reason)
    if (not c) then
        -- Output a traceback to help track down source
        error( debug.traceback("Assert failed", 2) )
    end
    M.test.assert(c, reason)
end

-- Pass an integration test (requires Unity Test Tools)
function M.pass()
    M.test.pass()
end

-- Fail an integration test (requires Unity Test Tools)
-- reason: Option string explaining why the test failed.
function M.fail(reason)
    error( debug.traceback("Test failed", 2) )
    M.test.fail(reason)
end

---------------------------------------------
-- All functions below require Fungus to work
---------------------------------------------

-------------
-- Say Dialog
-------------

-- Options for configuring Say Dialog behaviour
M.sayoptions = {}
M.sayoptions.saydialog = nil
M.sayoptions.clearprevious = true 
M.sayoptions.waitforinput = true
M.sayoptions.fadewhendone = true
M.sayoptions.stopvoiceover = true

-- Set the active saydialog to use with the say and conversation functions
function M.setsaydialog(saydialog)
    M.sayoptions.saydialog = saydialog
    luautils.SetSayDialog(saydialog)
end

-- Gets the active say dialog, or creates one if none exists yet
function M.getsaydialog()
    if (M.sayoptions.saydialog == nil) then
        local sd = M.luautils.spawn("Prefabs/SayDialog").GetComponent("SayDialog")
        M.setsaydialog(sd)
    end
    return M.sayoptions.saydialog
end

-- Set the active character on the Say Dialog
-- character: A Fungus.Character component
-- portrait: The name of a sprite in the character's portrait list
function M.setcharacter(character, portrait)
    assert(character, "character must not be nil")
    local sd = M.getsaydialog()
    sd.SetCharacter(character)

    -- Do substitution on character name
    local subbed = M.sub(character.nameText)
    M.sayoptions.saydialog.SetCharacterName(subbed, character.nameColor)

    -- Try to set the portrait sprite
    if (portrait) then
        if (portrait == "") then
            M.sayoptions.saydialog.SetCharacterImage(nill)
        else
            for i,v in ipairs(character.portraits) do
                -- Use a case insensitive comparison
                if (string.lower(v.name) == string.lower(portrait)) then
                    M.sayoptions.saydialog.SetCharacterImage(v)
                end
            end
        end
    end
end

-- Write text to the active Say Dialog
-- text: A string to write to the say dialog
-- voice: A voiceover audioclip to play
function M.say(text, voiceclip)
   local sd = M.getsaydialog()

   -- Do variable substitution before displaying text
   local subbed = M.sub(text)
   local e = sd.DoSay(subbed, M.sayoptions.clearprevious, M.sayoptions.waitforinput, M.sayoptions.fadewhendone, M.sayoptions.stopvoiceover, voiceclip)

   M.runwait(e)
end

-- Say a series of lines, setting speakign character, portrait or stage position on each line.
-- All parameters may be ommitted. The first three parameters can appear in any order.
-- The story text starts with a ':' and runs to the next newline character.
-- conv: A string of conversational lines using the following format.
-- [character] [portrait] [position] <: Story text>
-- e.g. john happy left: Hi, I'm happy.
function M.conversation(text)
    local subbed = M.sub(text)
    local e = luautils.DoConversation(subbed)
    M.runwait(e)
end

--------------
-- Menu Dialog
--------------

-- Options for configuring Menu Dialog behaviour
M.menuoptions = {}

-- Set the active menudialog to use with the menu function
function M.setmenudialog(menudialog)
    M.menuoptions.menudialog = menudialog
end

-- Gets the active menu dialog, or creates one if none exists yet
function M.getmenudialog()
    if (M.menuoptions.menudialog == nil) then
        local md = M.luautils.spawn("Prefabs/MenuDialog").GetComponent("MenuDialog")
        M.setmenudialog(md)
    end
    return M.menuoptions.menudialog
end

-- Display a menu button
-- text: text to display on the button
-- callback: function to call when this option is selected
-- interactive (optional): if false, displays the option as disabled
function M.menu(text, callback, interactive)
    local md = M.getmenudialog()

    -- Do variable substitution before displaying text
    local subbed = M.sub(text)

    -- Default bool parameters are tricksy
    if (interactive == nil) then
        interactive = true
    end

    md.AddOption(subbed, interactive, M.luaenvironment, callback)
end

-- Display a list of menu options and wait for user to choose one.
-- When an option starts with the ~ character it will be displayed but not be selectable.
-- Returns the index of the selected option.
-- Returns 0 if options list is empty. Note: Lua array indices start at 1, not 0).
-- options: an array of option strings. e.g. { "Option 1", "Option 2" }
function M.choose(options)
    return M.choosetimer(options, 0, 0)
end

-- Display a list of menu options and wait for user to choose one, or for a timer to expire.
-- When an option starts with the ~ character it will be displayed but not be selectable.
-- Returns the index of the selected option, or the defaultoption if the timer expires.
-- Returns 0 if options list is empty. Note: Lua array indices start at 1, not 0).
-- options: an array of option strings. e.g. { "Option 1", "Option 2" }
-- duration: Time player has to pick an option.
-- defaultoption: Option index to return if the timer expires.
function M.choosetimer(options, duration, defaultoption)
    if (options == nil or #options == 0) then
        return 0
    end

    local md = M.getmenudialog()

    local selection = 0

    for i, text in ipairs(options) do

        local callback = function ()
            selection = i;
        end

        -- Do variable substitution before displaying text
        local subbed = M.sub(text)

        -- Check if option has been flagged as not interactive
        local interactive = true

        if string.sub(subbed, 1, 1) == "~" then
            interactive = false
            subbed = string.sub(subbed, 2)
        end

        md.AddOption(subbed, interactive, M.luaenvironment, callback)
    end

    if (duration > 0) then
        local callback = function ()
            selection = defaultoption
        end

        local e = md.ShowTimer(duration, M.luaenvironment, callback)
        M.run(e)
    end

    -- Wait until one of the callbacks is called by the user selecting an option
    while (selection == 0) do
        coroutine.yield()
    end

    return selection
end

-- Display a timer during which the player has to choose an option.
-- duration: The length of time to display the timer.
-- callback: Function to call if the timer expires before an option is selected.
function M.menutimer(duration, callback)
    local md = M.getmenudialog()

    local e = md.ShowTimer(duration, M.luaenvironment, callback)
    M.runwait(e)
end

-- Clear all currently displayed menu options
function M.clearmenu()
    assert(M.menuoptions.menudialog, "menudialog must not be nil")
    M.menuoptions.menudialog.Clear()
end

------------
-- Flowchart
------------

-- Returns the specified Variable in a Flowchart.
-- To access the value of the variable, use its .value property. e.g.
--  v = getvar(flowchart, "FloatVar") 
--  v.value = 10    -- Sets the value of the variable to 10
--  f = v.value     -- f now contains 10
-- flowchart: The Fungus Flowchart containing the Block to run.
-- varname: The name of the Variable to get.
function M.getvar(flowchart, varname)
    assert(flowchart, "flowchart must not be nil")
    return flowchart.GetVariable(varname)
end

-- Runs the specified Block in a Flowchart
-- flowchart: The Fungus Flowchart containing the Block to run.
-- blockname: The name of the Block to run.
-- commandindex: Index of the command to start execution at
-- nowait: If false, will yield until the Block finishes execution. If true will continue immediately.
function M.runblock(flowchart, blockname, commandindex, nowait)
    assert(flowchart, "flowchart must not be nil")
    assert(blockname, "blockname must not be nil")
    local block = flowchart.FindBlock(blockname)
    if (not block) then
        error("Block " .. blockname .. " not found")
        return
    end

    local e = block.Execute(commandindex or 0);

    if (nowait) then
        M.run( e )
    else
        M.runwait( e )
    end
end

return M