package de.ids_mannheim.korap.response;

import static de.ids_mannheim.korap.util.KrillString.quote;

import com.fasterxml.jackson.annotation.*;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.*;

import java.util.*;
import java.io.*;
import de.ids_mannheim.korap.response.Message;
import de.ids_mannheim.korap.response.Messages;
import de.ids_mannheim.korap.util.QueryException;
import de.ids_mannheim.korap.util.StatusCodes;

/**
 * A unified notification class for KorAP related errors,
 * warnings and messages.
 * 
 * <p>
 * The object contains lists of errors, warnings and messages
 * and new warnings, errors or messages are appended to these lists.
 * 
 * <p>
 * <blockquote><pre>
 * Notifications n = new Notifications();
 * n.addWarning(456, "Something went wrong");
 * if (n.hasWarnings()) {
 * for (Message msg : n.getWarnings())
 * System.err.out(msg.getCode() + ": " + msg.getMessage());
 * };
 * System.err.println(n.toJsonString());
 * </pre></blockquote>
 * 
 * @author Nils Diewald
 * @see de.ids_mannheim.korap.response.Messages
 */
/*
 * This will be inherited most of the time as Java does not support roles
 * and I have no idea how to do this more elegantly.
 */
@JsonInclude(Include.NON_NULL)
@JsonIgnoreProperties(ignoreUnknown = true)
public class Notifications {

    // Create object mapper for JSON generation
    protected ObjectMapper mapper = new ObjectMapper();

    private Messages warnings, errors, messages;


    /**
     * Check for warnings.
     * 
     * @return <tt>true</tt> in case there are warnings, otherwise
     *         <tt>false</tt>
     */
    public boolean hasWarnings () {
        if (this.warnings == null || this.warnings.size() == 0)
            return false;
        return true;
    };


    /**
     * Return all warnings.
     * 
     * @return {@link Messages} representing all warnings
     */
    @JsonIgnore
    public Messages getWarnings () {
        return this.warnings;
    };


    /**
     * Set warnings by means of a {@link JsonNode}.
     * 
     * @param msgs
     *            JSON array of warnings.
     * @return {@link Notifications} object for chaining.
     */
    public Notifications setWarnings (JsonNode msgs) {
        for (JsonNode msg : msgs)
            this.addWarning(msg);
        return this;
    };



    /**
     * Return a specific warning based on an index.
     * 
     * @param index
     *            The index of the warning in the list of warnings.
     * @return The message in case it exists, otherwise
     *         <code>null</code>
     */
    @JsonIgnore
    public Message getWarning (int index) {
        if (this.warnings != null)
            return this.warnings.get(index);
        return (Message) null;
    };


    /**
     * Appends a new warning.
     * 
     * @param code
     *            Integer code representation of the warning
     * @param msg
     *            String representation of the warning
     * @param terms
     *            Optional strings of additional information
     * @return Notification object for chaining
     */
    public Notifications addWarning (int code, String msg, String ... terms) {

        if (this.warnings == null) {
            this.warnings = new Messages();
        };

        this.warnings.add(code, msg, terms);

        return this;
    };


    /**
     * Appends a new warning.
     * 
     * @param node
     *            {@link JsonNode} representing a warning message
     * @return Notification object for chaining
     */
    public Notifications addWarning (JsonNode node) {
        if (this.warnings == null)
            this.warnings = new Messages();

        try {
            this.warnings.add(node);
        }
        catch (QueryException qe) {
            this.warnings.add(qe.getErrorCode(), qe.getMessage());
        };

        return this;
    };


    /**
     * Appends new warnings.
     * 
     * @param msgs
     *            {@link Messages} representing multiple warnings
     * @return Notification object for chaining
     */
    public Notifications addWarnings (Messages msgs) {
        if (this.warnings == null)
            this.warnings = msgs;
        else
            this.warnings.add(msgs);
        return this;
    };


    /**
     * Return all errors.
     * 
     * @return The {@link Messages} object representing all errors
     */
    @JsonIgnore
    public Messages getErrors () {
        return this.errors;
    };


    /**
     * Set errors by means of a {@link JsonNode}.
     * 
     * @param msgs
     *            JSON array of errors.
     * @return Notifications object for chaining.
     */
    public Notifications setErrors (JsonNode msgs) {
        for (JsonNode msg : msgs)
            this.addError(msg);
        return this;
    };


    /**
     * Return a specific error based on an index.
     * 
     * @param index
     *            The index of the error in the list of errors.
     * @return The message in case it exists, otherwise
     *         <code>null</code>
     */
    @JsonIgnore
    public Message getError (int index) {
        if (this.errors != null)
            return this.errors.get(index);
        return (Message) null;
    };


    /**
     * Check for errors.
     * 
     * @return <tt>true</tt> in case there are errors, otherwise
     *         <tt>false</tt>
     */
    public boolean hasErrors () {
        if (this.errors == null || this.errors.size() == 0)
            return false;
        return true;
    };


    /**
     * Appends a new error.
     * 
     * @param code
     *            Integer code representation of the error
     * @param msg
     *            String representation of the error
     * @param terms
     *            Optional strings of additional information
     * @return Notification object for chaining
     */
    public Notifications addError (int code, String msg, String ... terms) {
        if (this.errors == null)
            this.errors = new Messages();
        this.errors.add(code, msg, terms);
        return this;
    };
    
    public Notifications addError (int code, String[] terms) {
        if (this.errors == null)
            this.errors = new Messages();
        this.errors.add(code, terms[0], Arrays.copyOfRange(terms, 1, terms.length));
        return this;
    }
    
    /**
     * Appends a new error.
     * 
     * @param node
     *            {@link JsonNode} representing an error message
     * @return Notification object for chaining
     */
    public Notifications addError (JsonNode msg) {
        if (this.errors == null)
            this.errors = new Messages();
        try {
            this.errors.add(msg);
        }
        catch (QueryException qe) {
            this.errors.add(qe.getErrorCode(), qe.getMessage());
        };

        return this;
    };


    /**
     * Appends new errors.
     * 
     * @param msgs
     *            {@link Messages} representing multiple errors
     * @return Notification object for chaining
     */
    public Notifications addErrors (Messages msgs) {
        if (this.errors == null)
            this.errors = msgs;
        else
            this.errors.add(msgs);
        return this;
    };


    /**
     * Return all messages.
     * 
     * @return {@link Messages} representing all messages
     */
    @JsonIgnore
    public Messages getMessages () {
        return this.messages;
    };


    /**
     * Set messages by means of a {@link JsonNode}.
     * 
     * @param msgs
     *            JSON array of messages.
     * @return Notifications object for chaining.
     */
    public Notifications setMessages (JsonNode msgs) {
        for (JsonNode msg : msgs)
            this.addMessage(msg);
        return this;
    };


    /**
     * Return a specific message based on an index.
     * 
     * @param index
     *            The index of the message in the list of messages.
     * @return The message in case it exists, otherwise
     *         <code>null</code>
     */
    @JsonIgnore
    public Message getMessage (int index) {
        if (this.messages != null)
            return this.messages.get(index);
        return (Message) null;
    };


    /**
     * Check for messages.
     * 
     * @return <tt>true</tt> in case there are messages, otherwise
     *         <tt>false</tt>
     */
    public boolean hasMessages () {
        if (this.messages == null || this.messages.size() == 0)
            return false;
        return true;
    };


    /**
     * Appends a new message.
     * 
     * @param code
     *            Integer code representation of the message
     * @param msg
     *            String representation of the message
     * @param terms
     *            Optional strings of additional information
     * @return Notification object for chaining
     */
    public Notifications addMessage (int code, String msg, String ... terms) {
        if (this.messages == null)
            this.messages = new Messages();
        this.messages.add(code, msg, terms);
        return this;
    };


    /**
     * Appends a new message.
     * 
     * @param node
     *            {@link JsonNode} representing a message
     * @return Notification object for chaining
     */
    public Notifications addMessage (JsonNode msg) {
        if (this.messages == null)
            this.messages = new Messages();
        try {
            this.messages.add(msg);
        }
        catch (QueryException qe) {
            this.messages.add(qe.getErrorCode(), qe.getMessage());
        };
        return this;
    };


    /**
     * Appends new messages.
     * 
     * @param msgs
     *            {@link Messages} representing multiple messages
     * @return Notification object for chaining
     */
    public Notifications addMessages (Messages msgs) {
        if (this.messages == null)
            this.messages = msgs;
        else
            this.messages.add(msgs);
        return this;
    };


    /**
     * Copy notifications from another notification object.
     * 
     * @param notes
     *            Notification object to copy notifications from.
     * @return Notification object for chaining
     */
    public Notifications copyNotificationsFrom (Notifications notes) {
        try {
            if (notes.hasErrors())
                this.addErrors((Messages) notes.getErrors().clone());
            if (notes.hasWarnings())
                this.addWarnings((Messages) notes.getWarnings().clone());
            if (notes.hasMessages())
                this.addMessages((Messages) notes.getMessages().clone());
        }
        catch (CloneNotSupportedException cnse) {};
        return this;
    };


    /**
     * Copy notifications from a {@link JsonNode} object.
     * 
     * @param request
     *            Notifications containing {@link JsonNode}.
     * @return Notification object for chaining
     */
    public Notifications copyNotificationsFrom (JsonNode request) {

        // Add warnings from JSON
        if (request.has("warnings") && request.get("warnings").isArray()) {
            JsonNode msgs = request.get("warnings");
            for (JsonNode msg : msgs)
                this.addWarning(msg);
        };

        // Add messages from JSON
        if (request.has("messages") && request.get("messages").isArray()) {
            JsonNode msgs = request.get("messages");
            if (msgs.isArray())
                for (JsonNode msg : msgs)
                    this.addMessage(msg);
        };

        // Add errors from JSON
        if (request.has("errors") && request.get("errors").isArray()) {
            JsonNode msgs = request.get("errors");
            if (msgs.isArray())
                for (JsonNode msg : msgs)
                    this.addError(msg);
        };

        return this;
    };


    /**
     * Move notifications from a passed {@link Notification} object
     * to the invocant.
     * 
     * @param notes
     *            Notification object.
     * @return The invocant object for chaining
     */
    public Notifications moveNotificationsFrom (Notifications notes) {
        this.copyNotificationsFrom(notes);
        notes.clearNotifications();
        return this;
    };


    /**
     * Clear all notifications.
     * 
     * @return Notification object for chaining
     */
    public Notifications clearNotifications () {
        if (this.warnings != null)
            this.warnings.clear();
        if (this.messages != null)
            this.messages.clear();
        if (this.errors != null)
            this.errors.clear();
        return this;
    };



    /**
     * Serialize Notifications as a {@link JsonNode}.
     * 
     * @return {@link JsonNode} representation of all warnings,
     *         errors, and messages.
     */
    public JsonNode toJsonNode () {
        ObjectNode json = mapper.createObjectNode();

        // Add messages
        if (this.hasWarnings())
            json.put("warnings", this.getWarnings().toJsonNode());
        if (this.hasErrors())
            json.put("errors", this.getErrors().toJsonNode());
        if (this.hasMessages())
            json.put("messages", this.getMessages().toJsonNode());

        return (JsonNode) json;
    };


    /**
     * Serialize Notifications as a JSON string.
     * <p>
     * <blockquote><pre>
     * {
     * "errors": [
     * [123, "You are not allowed to serialize these messages"],
     * [124, "Your request was invalid"]
     * ],
     * "messages" : [
     * [125, "Class is deprecated", "Notifications"]
     * ]
     * }
     * </pre></blockquote>
     * 
     * @return String representation of all warnings, errors, and
     *         messages
     */
    public String toJsonString () {
        String msg = "";
        try {
            JsonNode node = this.toJsonNode();
            if (node == null)
                return "{}";
            return mapper.writeValueAsString(node);
        }
        catch (Exception e) {
            // Bad in case the message contains quotes!
            msg = ", " + quote(e.getLocalizedMessage());
        };

        return "{\"errors\" : [" + "[620, " + "\"Unable to generate JSON\""
                + msg + "]" + "]}";
    };
};
