/**
 *      Provide interface for the connection to Avarice application.
 *  It's parameters are defined in Acedia's config.
 *      Class provides methods to obtain its configuration information
 *  (name, address, port), methods to check and change the status of connection,
 *  signals to handle arriving messages and ways to send messages back.
 *      Copyright 2021 Anton Tarasenko
 *------------------------------------------------------------------------------
 * This file is part of Acedia.
 *
 * Acedia is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3 of the License, or
 * (at your option) any later version.
 *
 * Acedia is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Acedia.  If not, see <https://www.gnu.org/licenses/>.
 */
class AvariceLink extends AcediaObject;

/**
 *      Objects of this class are supposed to be obtained via the
 *  `AvariceAPI.GetLink()` method. Available links are automatically initialized
 *  based on the configs and their parameters cannot be changed.
 *      It is also possible to spawn a link of your own by creating an object of
 *  this class (`AvariceLink`) and calling `Initialize()` method with
 *  appropriate parameters. To start the link then simply call `StartUp()`.
 *  But such links will not appear in the list of available links in
 *  `AvariceAPI`.
 */

//  Actual work of dealing with network input/output is done in
//  the `AvariceTcpStream` `Actor` class that is stored inside this reference
var private NativeActorRef tcpStream;

//  `tcpStream` communicates with this class by informing it about specific
//  events. This enum describes all of their types.
enum AvariceNetworkMessage
{
    //  Connection with Avarice established - can happen several times in case
    //  connection is interrupted
    ANM_Connected,
    //  We have lost connection with Avarice, but normally  will attempt to
    //  reconnect back
    ANM_Disconnected,
    //  JSON message received
    ANM_Message,
    //  Connection died: either was manually closed, host address could not
    //  be resolved or invalid data was received from Avarice
    ANM_Death
};

//  Name of this link, specified in the config
var private Text    linkName;
//  Host of the Avarice instance we are connecting to
var private Text    linkHost;
//  Port used by the Avarice instance we are connecting to
var private int     linkPort;

var private SimpleSignal onConnectedSignal;
var private SimpleSignal onDisconnectedSignal;
var private SimpleSignal onDeathSignal;
//      We want to have a separate signal for each message "service", since most
//  users of `AvariceLink` would only care about one particular service.
//      To achieve that we use this array as a "service name" <-> "signal" map.
var private HashTable serviceSignalMap;

var private const int TSERVICE_PREFIX, TTYPE_PREFIX;
var private const int TPARAMS_PREFIX, TMESSAGE_SUFFIX;

var private LoggerAPI.Definition fatalCannotSpawn;

protected function Constructor()
{
    onConnectedSignal = SimpleSignal(_.memory.Allocate(class'SimpleSignal'));
    onDisconnectedSignal = SimpleSignal(_.memory.Allocate(class'SimpleSignal'));
    onDeathSignal = SimpleSignal(_.memory.Allocate(class'SimpleSignal'));
    serviceSignalMap = _.collections.EmptyHashTable();
}

protected function Finalizer()
{
    local Actor storedStream;
    _.memory.Free(onConnectedSignal);
    _.memory.Free(onDisconnectedSignal);
    _.memory.Free(onDeathSignal);
    _.memory.Free(serviceSignalMap);
    _.memory.Free(linkName);
    _.memory.Free(linkHost);
    onConnectedSignal       = none;
    onDisconnectedSignal    = none;
    onDeathSignal           = none;
    serviceSignalMap        = none;
    linkName                = none;
    linkHost                = none;
    linkPort                = 0;
    if (tcpStream == none) {
        return;
    }
    storedStream = tcpStream.Get();
    if (storedStream != none) {
        storedStream.Destroy();
    }
    tcpStream.FreeSelf();
    tcpStream = none;
}

/**
 *  Initializes this caller `AvariceLink` with config data.
 *
 *  Can only successfully (for that `name` and `host` must not be `none`)
 *  be called once.
 *
 *  @param  name    Alias (case-insensitive) of caller `AvariceLink`.
 *      Must not be `none`.
 *  @param  host    Host of the Avarice instance that caller `AvariceLink` is
 *      connecting to. Must not be `none`.
 *  @param  name    Port used by the Avarice instance that caller `AvariceLink`
 *      is connecting to.
 */
public final function Initialize(BaseText name, BaseText host, int port)
{
    if (tcpStream != none)  return;
    if (name == none)       return;
    if (host == none)       return;

    linkName = name.Copy();
    linkHost = host.Copy();
    linkPort = port;
    tcpStream = _server.unreal.ActorRef(none);
}

/**
 *  Signal that will be emitted whenever caller `AvariceLink` connects to
 *  Avarice. This event can be emitted multiple times if case link temporarily
 *  loses it's TCP connection or if connection is killed off due to errors
 *  (or manually).
 *
 *  [Signature]
 *  void <slot>()
 */
/* SIGNAL */
public final function SimpleSlot OnConnected(AcediaObject receiver)
{
    return SimpleSlot(onConnectedSignal.NewSlot(receiver));
}

/**
 *  Signal that will be emitted whenever caller `AvariceLink` disconnects from
 *  Avarice. Disconnects can temporarily be cause by network issue and
 *  `AvariceLink` will attempt to restore it's connection. To detect when
 *  connection was permanently severed use `OnDeath` signal instead.
 *
 *  [Signature]
 *  void <slot>()
 */
/* SIGNAL */
public final function SimpleSlot OnDisconnected(AcediaObject receiver)
{
    return SimpleSlot(onDisconnectedSignal.NewSlot(receiver));
}

/**
 *  Signal that will be emitted whenever connection is closed and dropped:
 *  either due to bein unable to resolve host's address, receiving incorrect
 *  input from Avarice or someone manually closing it.
 *
 *  [Signature]
 *  void <slot>()
 */
/* SIGNAL */
public final function SimpleSlot OnDeath(AcediaObject receiver)
{
    return SimpleSlot(onDeathSignal.NewSlot(receiver));
}

/**
 *  Signal that will be emitted whenever caller `AvariceLink` disconnects from
 *  Avarice. Disconnects can temporarily be cause by network issue and
 *  `AvariceLink` will attempt to restore it's connection. To detect when
 *  connection was permanently severed use `OnDeath` signal instead.
 *
 *  @param  service Name of the service, whos messages one wants to receive.
 *      `none` will be treated as an empty `Text`.
 *
 *  [Signature]
 *  void <slot>(AvariceLink link, AvariceMessage message)
 *  @param  link    Link that has received message.
 *  @param  message Received message.
 *      Can be any JSON-compatible value (see `JSONAPI.IsCompatible()`
 *      for more information).
 */
/* SIGNAL */
public final function Avarice_OnMessage_Slot OnMessage(
    AcediaObject    receiver,
    BaseText        service)
{
    return Avarice_OnMessage_Slot(GetServiceSignal(service).NewSlot(receiver));
}

private final function Avarice_OnMessage_Signal GetServiceSignal(
    BaseText service)
{
    local Avarice_OnMessage_Signal result;
    if (service != none) {
        service = service.Copy();
    }
    else {
        service = Text(_.memory.Allocate(class'Text'));
    }
    result = Avarice_OnMessage_Signal(serviceSignalMap.GetItem(service));
    if (result == none)
    {
        result = Avarice_OnMessage_Signal(
            _.memory.Allocate(class'Avarice_OnMessage_Signal'));
        serviceSignalMap.SetItem(service, result);
        _.memory.Free(result);
    }
    else {
        service.FreeSelf();
    }
    return result;
}

/**
 *  Starts caller `AvariceLink`, making it attempt to connect to the Avarice
 *  with parameters that should be first specified by the `Initialize()` call.
 *
 *  Does nothing if the caller `AvariceLink` is either not initialized or
 *  is already active (`IsActive() == true`).
 */
public final function StartUp()
{
    local AvariceTcpStream newStream;
    local LevelCore core;

    if (tcpStream == none)          return;
    if (tcpStream.Get() != none)    return;
    core = __level().GetLevelCore();
    if (core == none)               return;

    newStream = AvariceTcpStream(core.Allocate(class'AvariceTcpStream'));
    if (newStream == none)
    {
        //  `linkName` has to be defined if `tcpStream` is defined
        _.logger.Auto(fatalCannotSpawn).Arg(linkName.Copy());
        return;
    }
    tcpStream.Set(newStream);
    newStream.StartUp(self, class'Avarice_Feature'.static.GetReconnectTime());
}

/**
 *  Shuts down any connections related to the caller `AvariceLink`.
 *
 *  Does nothing if the caller `AvariceLink` is either not initialized or
 *  is already inactive (`IsActive() == false`).
 */
public final function ShutDown()
{
    local Actor storedStream;
    if (tcpStream == none)      return;
    storedStream = tcpStream.Get();
    if (storedStream == none)   return;

    storedStream.Destroy();
    tcpStream.Set(none);
}

/**
 *  Checks whether caller `AvariceLink` is currently active: either connected or
 *  currently attempts to connect to Avarice.
 *
 *  See also `IsConnected()`.
 *
 *  @return `true` if caller `AvariceLink` is either connected or currently
 *      attempting to connect to Avarice. `false` otherwise.
 */
public final function bool IsActive()
{
    if (tcpStream == none) {
        return false;
    }
    return tcpStream.Get() != none;
}

/**
 *  Checks whether caller `AvariceLink` is currently connected or to Avarice.
 *
 *  See also `IsActive()`.
 *
 *  @return `true` iff caller `AvariceLink` is currently connected to Avarice.
 */
public final function bool IsConnected()
{
    local AvariceTcpStream storedStream;
    if (tcpStream == none) {
        return false;
    }
    storedStream = AvariceTcpStream(tcpStream.Get());
    return storedStream.linkState == STATE_Connected;
}

/**
 *  Returns name caller `AvariceLink` was initialized with.
 *  Defined through the config files.
 *
 *  @return Name of the caller `AvariceLink`.
 *      `none` iff caller link was not yet initialized.
 */
public final function Text GetName()
{
    if (linkName != none) {
        return linkName.Copy();
    }
    //  `linkName` cannot be `none` after `Initialize()` call
    return none;
}

/**
 *  Returns host name (without port number) caller `AvariceLink` was
 *  initialized with. Defined through the config files.
 *
 *  See `GetPort()` method for port number.
 *
 *  @return Host name of the caller `AvariceLink`.
 *      `none` iff caller link was not yet initialized.
 */
public final function Text GetHost()
{
    if (linkHost != none) {
        return linkHost.Copy();
    }
    //  `linkName` cannot be `none` after `Initialize()` call
    return none;
}

/**
 *  Returns port number caller `AvariceLink` was initialized with.
 *  Defined through the config files.
 *
 *  @return Host name of the caller `AvariceLink`.
 *      If caller link was not yet initialized, method makes no guarantees
 *      about returned number.
 */
public final function int GetPort()
{
    return linkPort;
}

/**
 *  Send a message to the Avarice that caller `AvariceLink` is connected to.
 *
 *  Message can only be set if caller `AvariceLink` was initialized and is
 *  currently connected (see `IsConnected()`) to Avarice.
 *
 *  @param  service     Name of the service this message is addressed.
 *      As an example, to address a database one would specify its name,
 *      like "db". Cannot be `none`.
 *  @param  type        Name of this message. As an example, to address
 *      a database, one would specify here WHAT that database must do,
 *      like "get" to fetch some data. Cannot be `none`.
 *  @param  parameters  Parameters of the command. Can be any value that is
 *      JSON-compatible (see `JSONAPI.IsCompatible()` for details).
 *  @return `true` if message was successfully sent and `false` otherwise.
 *      Note that this method returning `true` does not necessarily mean that
 *      message has arrived (which is impossible to know at this moment),
 *      instead simply saying that network call to send data was successful.
 *      Avarice does not provide any mechanism to verify message arrival, so if
 *      you need that confirmation - it is necessary that service you are
 *      addressing make a reply.
 */
public final function bool SendMessage(
    BaseText        service,
    BaseText        type,
    AcediaObject    parameters)
{
    local Mutabletext       parametesAsJSON;
    local MutableText       message;
    local AvariceTcpStream  storedStream;
    if (tcpStream == none)                          return false;
    if (service == none)                            return false;
    if (type == none)                               return false;
    storedStream = AvariceTcpStream(tcpStream.Get());
    if (storedStream == none)                       return false;
    if (storedStream.linkState != STATE_Connected)  return false;
    parametesAsJSON = _.json.Print(parameters);
    if (parametesAsJSON == none)                    return false;

    message = _.text.Empty();
    message.Append(T(TSERVICE_PREFIX))
        .Append(_.json.Print(service))
        .Append(T(TTYPE_PREFIX))
        .Append(_.json.Print(type))
        .Append(T(TPARAMS_PREFIX))
        .Append(parametesAsJSON)
        .Append(T(TMESSAGE_SUFFIX));
    storedStream.SendMessage(message);
    message.FreeSelf();
    parametesAsJSON.FreeSelf();
    return true;
}

//      This is a public method, but it is not a part of
//  `AvariceLink` interface.
//      It is used as a communication channel with `AvariceTcpStream` and
//  should not be called outside of that class.
public final function ReceiveNetworkMessage(
    AvariceNetworkMessage   message,
    optional AvariceMessage avariceMessage)
{
    if (message == ANM_Connected) {
        onConnectedSignal.Emit();
    }
    else if (message == ANM_Disconnected) {
        onDisconnectedSignal.Emit();
    }
    else if (message == ANM_Message && avariceMessage != none) {
        GetServiceSignal(avariceMessage.service).Emit(self, avariceMessage);
    }
    else if (message == ANM_Death) {
        onDeathSignal.Emit();
        tcpStream.Set(none);
    }
}

defaultproperties
{
    TSERVICE_PREFIX = 0
    stringConstants(0) = "&{\"s\":"
    TTYPE_PREFIX    = 1
    stringConstants(1) = ",\"t\":"
    TPARAMS_PREFIX  = 2
    stringConstants(2) = ",\"p\":"
    TMESSAGE_SUFFIX = 3
    stringConstants(3) = "&}"
    fatalCannotSpawn = (l=LOG_Error,m="Cannot spawn new actor of class `AvariceTcpStream`, avarice link \"%1\" will not be created")
}