/* This code is part of Freenet. It is distributed under the GNU General
 * Public License, version 2 (or at your option any later version). See
 * http://www.gnu.org/ for further details of the GPL. */
package freenet.clients.fcp;

import java.util.UUID;

import freenet.pluginmanager.FredPluginFCPMessageHandler;
import freenet.support.SimpleFieldSet;
import freenet.support.StringValidityChecker;
import freenet.support.api.Bucket;

/**
 * Container class for both incoming and outgoing FCP messages.
 */
public final class FCPPluginMessage {
    public static enum ClientPermissions {
        /** The client is connected by network and the owner of the node has configured
         *  restricted access for the client's IP */
        ACCESS_FCP_RESTRICTED,
        /** The client is connected by network and the owner of the node has configured full
         *  access for the client's IP */
        ACCESS_FCP_FULL,
        /** The client plugin is running within the same node as the server plugin.<br>
         *  This probably should be interpreted as {@link #ACCESS_FCP_FULL}: If the client
         *  plugin is running inside the node, it can probably do whatever it wants. We're
         *  nevertheless shipping this information to you as it is available anyway. */
        ACCESS_DIRECT
    }

    /**
     * The permissions of the client which sent the messages. Null for server-to-client and
     * outgoing messages.<br>
     * Will be set by the {@link FCPPluginConnection} before delivery of the message. Thus, you
     * can pass null for this in all constructors.
     */
    public final ClientPermissions permissions;
    
    /**
     * The unique identifier of the message.<br>
     * Can be used by server and client to track the progress of messages.<br>
     * This especially applies to {@link FCPPluginConnection#sendSynchronous(SendDirection,
     * FCPPluginMessage, long)} which will wait for a reply with the same identifier as the
     * original message until it returns.<br><br>
     * 
     * For reply messages, this shall be the same as the identifier of the message to which this
     * is a reply.<br>
     * For non-reply message, this shall be a sufficiently random {@link String} to prevent 
     * collisions with any previous message identifiers. The default is a random {@link UUID}, and
     * alternate implementations are recommended to use a random {@link UUID} as well.<br><br>
     * 
     * <b>Notice:</b> Custom client implementations can chose the identifier freely when sending
     * messages, and thus violate these rules. This is highly discouraged though, as non-unique
     * identifiers make tracking messages impossible. But if a client does violate the rules and
     * thereby breaks its message tracking, thats not the server's problem, and thus should not
     * cause complexification of the server code.<br>
     * So server implementations <b>should</b> assume that the client chooses the identifier in
     * a sane manner which follows the rules.<br>
     * This class does follow the rules, and thus client and server implementations using it
     * will do so as well.
     */
    public final String identifier;
    
    /**
     * Part 1 of the actual message: Human-readable parameters. Shall be small amount of data.<br>
     * Can be null for data-only or success-indicator messages.
     */
    public final SimpleFieldSet params;
    
    /**
     * Part 2 of the actual message: Non-human readable, large size bulk data.<br>
     * Can be null if no large amount of data is to be transfered.
     */
    public final Bucket data;
    
    /**
     * For messages which are a reply to another message, this is always non-null. It then
     * is true if the operation to which this is a reply succeeded, false if it failed.<br>
     * For non-reply messages, this is always null.<br><br>
     * 
     * Notice: Whether this is null or non-null is used to determine the return value of
     * {@link #isReplyMessage()} - a reply message has success != null, a non-reply message has
     * success == null.
     */
    public final Boolean success;

    /**
     * For reply messages with {@link #success} == false, may contain an alpha-numeric String
     * which identifies a reason for the failure in a standardized representation which software
     * can parse easily. May also be null in that case, but please try to not do that.<br>
     * For {@link #success} == null or true, this must be null.<br><br>
     * 
     * The String shall be for programming purposes and thus <b>must</b> be alpha-numeric.<br>
     * For unclassified errors, such as Exceptions which you do not expect, use "InternalError".
     */
    public final String errorCode;

    /**
     * For reply messages with {@link #errorCode} != null, may contain a String which describes
     * the problem in a human-readable, user-friendly manner. May also be null in that case, but
     * please try to not do that.<br>
     * For {@link #errorCode} == null, this must be null.<br><br>
     * 
     * You are encouraged to provide it translated to the configured language already.<br>
     * The String shall not be used for identifying problems in programming.<br>
     * There, use {@link #errorCode}.
     * For Exceptions which you do not expect, {@link Exception#toString()} will return a
     * sufficient errorMessage (containing the name of the Exception and the localized error
     * message, or non-localized if there is no translation).<br><br>
     * 
     * (Notice: This may only be non-null if {@link #errorCode} is non-null instead of just
     * if {@link #success} == false to ensure that a developer-friendly error signaling is
     * implemented: errorCode is designed to be easy to parse, errorMessage is designed
     * to be human readable and thus cannot be parsed. Therefore, the errorCode field should be
     * more mandatory than this field.)
     */
    public final String errorMessage;

    /**
     * @return
     *     True if the message is merely a reply to a previous message from your side.<br>
     *     In that case, you <b>must not</b> send another reply message back to prevent infinite
     *     bouncing of "success!" replies.
     */
    public boolean isReplyMessage() {
        return success != null;
    }
    
    /**
     * See the JavaDoc of the member variables with the same name as the parameters for an
     * explanation of the parameters.
     */
    private FCPPluginMessage(ClientPermissions permissions, String identifier,
            SimpleFieldSet params, Bucket data, Boolean success, String errorCode,
            String errorMessage) {
        
        // See JavaDoc of member variables with the same name for reasons of the requirements
        assert(permissions != null || permissions == null);
        assert(identifier != null);
        assert(params != null || params == null);
        assert(data != null || data == null);
        assert(success != null || success == null);
        
        assert(params != null || data != null || success != null)
            : "Messages should not be empty";
        
        assert(errorCode == null || (success != null && success == false))
            : "errorCode should only be provided for reply messages which indicate failure.";
        
        assert(errorCode == null ||
               StringValidityChecker.isLatinLettersAndNumbersOnly(errorCode))
            : "errorCode should be alpha-numeric";
        
        assert(errorMessage == null || errorCode != null)
            : "errorCode should always be provided if there is an errorMessage";
        
        this.permissions = permissions;
        this.identifier = identifier;
        this.params = params;
        this.data = data;
        this.success = success;
        this.errorCode = errorCode;
        this.errorMessage = errorMessage;
    }
    
    /**
     * For being used by server or client to construct outgoing messages.<br>
     * Those can then be passed to the send functions of {@link FCPPluginConnection} or returned in
     * the message handlers of {@link FredPluginFCPMessageHandler}.<br><br>
     * 
     * <b>ATTENTION</b>: Messages constructed with this constructor here are <b>not</b> reply 
     * messages.<br>
     * If you are replying to a message, notably when returning a message in the message handler
     * interface implementation, you must use {@link #constructReplyMessage(FCPPluginMessage,
     * SimpleFieldSet, Bucket, boolean, String, String)} (or one of its shortcuts) instead.<br>
     * <br>
     * 
     * See the JavaDoc of the member variables with the same name as the parameters for an
     * explanation of the parameters.<br><br>
     * 
     * There is a shortcut to this constructor for typical choice of parameters:<br>
     * {@link #construct()}.
     */
    public static FCPPluginMessage construct(SimpleFieldSet params, Bucket data) {
        // Notice: While the specification of FCP formally allows the client to freely chose the
        // ID, we hereby restrict it to be a random UUID instead of allowing the client
        // (or server) to chose it. This is to prevent accidental collisions with the IDs of
        // other messages. I cannot think of any usecase of free-choice identifiers. And
        // collisions are *bad*: They can break the "ACK" mechanism of the "success" variable.
        // This would in turn break things such as the sendSynchronous() functions of
        // FCPPluginConnection.
        return new FCPPluginMessage(null, UUID.randomUUID().toString(), params, data,
            // success, errorCode, errorMessage are null since non-reply messages must not
            // indicate errors
            null, null, null);
    }

    /**
     * Same as {@link #construct(SimpleFieldSet, Bucket)} with the missing parameters being:<br>
     * <code>SimpleFieldSet params = new SimpleFieldSet(shortLived = true);<br>
     * Bucket data = null;</code><br><br>
     * 
     * <b>ATTENTION</b>: Messages constructed with this constructor here are <b>not</b> reply
     * messages.<br>
     * If you are replying to a message, notably when returning a message in the message handler
     * interface implementation, you must use {@link #constructReplyMessage(FCPPluginMessage,
     * SimpleFieldSet, Bucket, boolean, String, String)} (or one of its shortcuts) instead.<br>
     */
    public static FCPPluginMessage construct() {
        return construct(new SimpleFieldSet(true), null);
    }

    /**
     * For being used by server or client to construct outgoing messages which are a reply to an
     * original message.<br>
     * Those then can be returned from the message handler
     * {@link FredPluginFCPMessageHandler#handlePluginFCPMessage(FCPPluginConnection,
     * FCPPluginMessage)}.<br><br>
     * 
     * See the JavaDoc of the member variables with the same name as the parameters for an
     * explanation of the parameters.<br><br>
     * 
     * There are shortcuts to this constructor for typical choice of parameters:<br>
     * {@link #constructSuccessReply(FCPPluginMessage)}.<br>
     * {@link #constructErrorReply(FCPPluginMessage, String, String)}.<br>
     * 
     * @throws IllegalStateException
     *     If the original message was a reply message already.<br>
     *     Replies often shall only indicate success / failure instead of triggering actual
     *     operations, so it could cause infinite bouncing if you reply to them again.<br>
     *     Consider the whole of this as a remote procedure call process: A non-reply message is the
     *     procedure call, a reply message is the procedure result. When receiving the result, the
     *     procedure call is finished, and thus shouldn't cause further replies to be sent.<br>
     *     <b>Notice</b>: The JavaDoc of the aforementioned message handling function explains how
     *     you can nevertheless send a reply to reply messages.
     */
    public static FCPPluginMessage constructReplyMessage(FCPPluginMessage originalMessage,
            SimpleFieldSet params, Bucket data, boolean success, String errorCode,
            String errorMessage) {
        
        if(originalMessage.isReplyMessage()) {
            throw new IllegalStateException("Constructing a reply message for a message which "
                + "was a reply message already not allowed.");
        }
        
        return new FCPPluginMessage(null, originalMessage.identifier,
            params, data, success, errorCode, errorMessage);
    }

    /**
     * Same as {@link #constructReplyMessage(FCPPluginMessage, SimpleFieldSet, Bucket, boolean,
     * String, String)} with the missing parameters being:<br>
     * <code>
     * SimpleFieldSet params = new SimpleFieldSet(shortLived = true);<br>
     * Bucket data = null;<br>
     * boolean success = true;<br>
     * errorCode = null;<br>
     * errorMessage = null;<br>
     * </code>
     */
    public static FCPPluginMessage constructSuccessReply(FCPPluginMessage originalMessage) {
        return constructReplyMessage(
            originalMessage, new SimpleFieldSet(true), null, true, null, null);
    }

    /**
     * Same as {@link #constructReplyMessage(FCPPluginMessage, SimpleFieldSet, Bucket, boolean,
     * String, String)} with the missing parameters being:<br>
     * <code>
     * SimpleFieldSet params = new SimpleFieldSet(shortLived = true);<br>
     * Bucket data = null;<br>
     * boolean success = false;<br>
     * </code>
     */
    public static FCPPluginMessage constructErrorReply(FCPPluginMessage originalMessage,
            String errorCode, String errorMessage) {
        
        return constructReplyMessage(
            originalMessage, new SimpleFieldSet(true), null, false, errorCode, errorMessage);
    }

    /**
     * ATTENTION: Only for being used by internal network code.<br><br>
     * 
     * You <b>must not</b> use this for constructing outgoing messages in server or client
     * implementations.<br>
     * 
     * This function is typically to construct incoming messages for passing them to the message
     * handling function
     * {@link FredPluginFCPMessageHandler#handlePluginFCPMessage(FCPPluginConnection,
     * FCPPluginMessage)}.<br><br>
     * 
     * See the JavaDoc of the member variables with the same name as the parameters for an
     * explanation of the parameters.<br>
     */
    static FCPPluginMessage constructRawMessage(ClientPermissions permissions,
            String identifier, SimpleFieldSet params, Bucket data, Boolean success,
            String errorCode, String errorMessage) {
        
        return new FCPPluginMessage(permissions, identifier, params, data, success,
            errorCode, errorMessage);
    }

    @Override
    public String toString() {
        return super.toString() +
            " (permissions: " + permissions +
            "; identifier: " + identifier +
            "; data: " + data +
            "; success: " + success +
            "; errorCode: " + errorCode +
            "; errorMessage: " + errorMessage + 
            // At the end because a SimpleFieldSet usually contains multiple line breaks.
            "; params: " + '\n' + (params != null ? params.toOrderedString() : null);
    }
}