--***********************************************************
--**                    THE INDIE STONE                    **
--**				  Author: turbotutone				   **
--***********************************************************

local function getLuaNetModule( _name, _debug )
    local self 			= {};
    local self_commands = {};
    local self_LuaNet 	= LuaNet:getInstance();
    local self_name 	= _name;
    local self_debug	= _debug or false;

    -- ***********************************************************************************************
    -- addCommandHandler
    -- string		_command	= name of the command
    -- function		_handler	= (optional) function to handle incoming packets for command
    -- ***********************************************************************************************
    function self.addCommandHandler( _command, _handler )
        -- see if command is string and not already added
        if type( _command ) == 'string' and not self_commands[ _command ] then
            self_commands[ _command ] = {};
            -- add the handler function if its set
            if _handler and type( _handler ) == "function" then
                if self_debug then print( "LuaNet: Adding command handler -> " .. tostring( _command ) ); end
                self_commands[ _command ].handler = _handler;
            else
                if self_debug then print( "LuaNet: Adding command -> " .. tostring( _command ) ); end
            end
        else
            if self_debug then print( "LuaNet: Command already exists or not string -> " .. tostring( _command ) ); end
        end
    end

    -- ***********************************************************************************************
    -- listen (used internally)
    -- table		_package	= a LuaNet package
    -- IsoPlayer	_player		= on server carries over IsoPlayer
    -- ***********************************************************************************************
    function self.listen( _package, _player )
        if self_LuaNet.isRunning() then
            -- read LuaNet package
            local com 		= _package.command;
            local recipient = _package.recipient;
            local source 	= _package.source;
            local payload 	= _package.payload;

            if self_debug then print( "LuaNet: Lookup handler for -> ", com ); end
            if com and self_commands[ com ] then
                if isServer() and recipient then
                    if self_debug then print( "LuaNet: Forwarding to player -> ", recipient ); end
                    -- if a recipient is set in the _package_ then forward it to that player
                    local players 		= getOnlinePlayers();
                    local array_size 	= players:size();
                    for i=0, array_size-1, 1 do
                        local player = players:get(i);
                        if player:getUsername() == recipient then
                            if self_debug then print( "LuaNet: IsoPlayer [OK] for player -> ", recipient ); end
                            sendServerCommand( player, self_LuaNet:getName(), self_name, _package );
                        end
                    end
                elseif self_commands[ com ].handler then
                    -- elseif handler function is set, handle package and unpack payload
                    if self_debug then print( "LuaNet: Passing payload to handler for -> ", com ); end
                    if not _player and source then
                        -- if on _player is not set and source is, then attempt to get IsoPlayer from source username
                        local players 		= getOnlinePlayers();
                        local array_size 	= players:size();
                        for i=0, array_size-1, 1 do
                            local player = players:get(i);
                            if player:getUsername() == source then
                                if self_debug then print( "LuaNet: Retrieved source IsoPlayer for player -> ", source ); end
                                _player = player;
                            end
                        end
                    end
                    -- unpack payload to handler, attempt to pass a IsoPlayer if available
                    self_commands[ com ].handler( _player or false, unpack( payload ) );
                else
                    -- else warn we have dead package
                    if self_debug then print( "LuaNet: Package journey ended unexpectedly for command -> ", com ); end
                end
            end
        end
    end

    -- ***********************************************************************************************
    -- send
    -- string		_command	= name of the command
    -- arg			...			= any number of arguments
    -- ***********************************************************************************************
    function self.send( _command, ... )
        self.sendPlayer( false, _command, ... )
    end

    -- ***********************************************************************************************
    -- sendPlayer (NOTE: has slightly different behaviour for server and client, read comments for info.)
    -- IsoPlayer	_player		= IsoPlayer instance of the recipient
    -- string		_command	= name of the command
    -- arg			...			= any number of arguments
    -- ***********************************************************************************************
    function self.sendPlayer( _player, _command, ... )
        if self_LuaNet.isRunning() then --and self_commands[ _command ] then
        if _player and not instanceof(_player, "IsoPlayer" ) then
            -- if player param set and not a player return and warn.
            if self_debug then print( "LuaNet: Not a instance of IsoPlayer for sendPlayer -> ", _command ); end
            return;
        end

        -- create the package
        local package 		= {};
        package.command 	= _command;
        package.recipient 	= false;
        package.source 		= false;
        package.payload 	= { ... };		-- payload will be unpacked in exact order to the handler function on the receiving end.
        if isServer() then
            if _player then
                -- if player set, send package directly to player, no need to add source or recipient info
                sendServerCommand( _player, self_LuaNet:getName(), self_name, package );
            else
                -- otherwise send package to all clients
                sendServerCommand( self_LuaNet:getName(), self_name, package );
            end
        elseif isClient() then
            if _player then
                -- if player set, add recipient info so server can forward, and source info so target knows where it came from
                package.source = getPlayer():getUsername();
                package.recipient = _player:getUsername();
                sendClientCommand( self_LuaNet:getName(), self_name, package );
            else
                -- otherwise just send package to server.
                sendClientCommand( self_LuaNet:getName(), self_name, package );
            end
        end
        else
            if self_debug then print( "LuaNet: Not running or command not found -> ", self_LuaNet.isRunning(), _command ); end
        end
    end

    return self;
end

--[[
	LUANET MAIN CLASS:
--]]

local instance 	= false;	-- stores instance
local validInit = false;	-- variable used to prevent initialization by users

LuaNet 			= {};

function LuaNet:getInstance()
    if instance then
        return instance;
    end

    local self 			= {};
    local self_name 	= "LuaNet";
    local self_modules 	= {};
    local self_running 	= false;
    local self_debug 	= false;

    -- ***********************************************************************************************
    -- self.setDebug
    -- bool 	debug 	set debugging true or false
    -- ***********************************************************************************************
    function self.setDebug( _debug )
        if type( _debug ) == "boolean" then
            self_debug = _debug;
            print( "LuaNet: Debugging set to -> ", self_debug );
        end
    end

    -- ***********************************************************************************************
    -- getDebug
    -- returns 	bool 	debugging true/false
    -- ***********************************************************************************************
    function self.getDebug()
        return self_debug;
    end

    -- ***********************************************************************************************
    -- isRunning
    -- returns 	bool 	true if LuaNet is initialised and running, else false
    -- ***********************************************************************************************
    function self.isRunning()
        return self_running;
    end

    -- ***********************************************************************************************
    -- getName
    -- returns 	string 	by default 'LuaNet', used as pz network code module
    -- ***********************************************************************************************
    function self.getName()
        return self_name;
    end

    -- ***********************************************************************************************
    -- getModule
    -- returns 	LuaNetModule 	returns a LuaNetModule if exists, else creates and returns
    -- ***********************************************************************************************
    function self.getModule( _name, _debug )
        if not self_modules[ _name ] then
            self_modules[ _name ] = getLuaNetModule( _name, _debug );
        end
        return self_modules[ _name ];
    end

    -- ***********************************************************************************************
    -- self_listen (private function)
    -- string			_module		= name of the module
    -- LuaNetPackage	_package	= table containing package info
    -- IsoPlayer		_player		= optional, on server passes it player on client not
    -- ***********************************************************************************************
    local function self_listen( _module, _package, _player )
        if self_modules[ _module ] then
            if self_debug then print( "LuaNet: Incomming module/command -> ", _module, _package.command ); end
            -- pass package and player to the module's listener
            self_modules[ _module ].listen( _package, _player );
        end
    end

    -- ***********************************************************************************************
    -- self_listen (private function)
    -- string			_mainModule		= name of the main module must match this self_name (LuaNet)
    -- string			_module			= name of the module
    -- LuaNetPackage	_package	    = table containing package info
    -- ***********************************************************************************************
    local function self_listenClient( _mainModule, _module, _package )
        if _mainModule == self_name then
            self_listen( _module, _package, nil );
        end
    end

    -- ***********************************************************************************************
    -- self_listen (private function)
    -- string			_mainModule		= name of the main module must match this self_name (LuaNet)
    -- string			_module			= name of the module
    -- IsoPlayer		_player			= optional, on server passes it player on client not
    -- LuaNetPackage	_package	    = table containing package info
    -- ***********************************************************************************************
    local function self_listenServer( _mainModule, _module, _player, _package )
        if _mainModule == self_name then
            self_listen( _module, _package, _player );
        end
    end

    -- ***********************************************************************************************
    -- InitList
    -- List of funcitons to call on Init event
    -- ***********************************************************************************************
    local self_initList = {};
    function self.onInitAdd( _func )
        table.insert( self_initList, _func );
    end

    -- ***********************************************************************************************
    -- Init
    -- Can only be called by OnServerStarted or OnGameTimeLoaded below, sets listners and triggers Init event
    -- ***********************************************************************************************
    function self.Init()
        if not validInit or self_running then return end

        if isServer() then
            print("LuaNet: Registering server listener..." );
            Events.OnClientCommand.Add( self_listenServer );
        elseif isClient() then
            print("LuaNet: Registering client listener..." );
            Events.OnServerCommand.Add( self_listenClient );
        end

        self_running = true;

        print("LuaNet: Initialization [DONE], triggering events for 'LuaNet.onInitAdd'.");
        --triggerEvent( "OnLuaNetInit", self );
        for i,func in ipairs( self_initList ) do
            func();
        end
        validInit = false;
    end

    function self.destroy()
        instance = false;
    end

    instance = self;
    return self;
end

-- ***********************************************************************************************
-- OnServerStart
-- called when the server udp engine is running
-- ***********************************************************************************************	
Events.OnServerStarted.Add( function()
    if isServer() then
        validInit = true;
        print("LuaNet: Initializing...");
        LuaNet:getInstance().Init();
    end
end );

-- ***********************************************************************************************
-- OnGameTimeLoaded
-- called when the client udp engine is running and player properly initialised
-- ***********************************************************************************************
Events.OnGameTimeLoaded.Add( function()
    if isClient() then
        validInit = true;
        print("LuaNet: Loading...");
        LuaNet:getInstance().Init();
    end
end );