package org.ovirt.engine.ui.frontend.server.gwt.plugin;

import java.util.Objects;

import org.codehaus.jackson.JsonNode;
import org.codehaus.jackson.node.JsonNodeFactory;
import org.codehaus.jackson.node.ObjectNode;

/**
 * Immutable structure that contains UI plugin descriptor/configuration data and associated logic.
 */
public class PluginData implements Comparable<PluginData> {

    public interface ValidationCallback {

        void descriptorError(String message);

        void configurationError(String message);

    }

    private static final String ATT_NAME = "name"; //$NON-NLS-1$
    private static final String ATT_URL = "url"; //$NON-NLS-1$
    private static final String ATT_CONFIG = "config"; //$NON-NLS-1$
    private static final String ATT_RESOURCEPATH = "resourcePath"; //$NON-NLS-1$
    private static final String ATT_LAZYLOAD = "lazyLoad"; //$NON-NLS-1$
    private static final String ATT_ENABLED = "enabled"; //$NON-NLS-1$
    private static final String ATT_ORDER = "order"; //$NON-NLS-1$

    private final JsonNode descriptorNode;
    private final long descriptorLastModified;

    private final JsonNode configurationNode;
    private final long configurationLastModified;

    private final JsonNodeFactory nodeFactory;

    public PluginData(JsonNode descriptorNode, long descriptorLastModified,
            JsonNode configurationNode, long configurationLastModified,
            JsonNodeFactory nodeFactory) {
        this.descriptorNode = descriptorNode;
        this.descriptorLastModified = descriptorLastModified;
        this.configurationNode = configurationNode;
        this.configurationLastModified = configurationLastModified;
        this.nodeFactory = nodeFactory;
    }

    public JsonNode getDescriptorNode() {
        // TODO should return a deep copy, see JACKSON-707
        return descriptorNode;
    }

    public JsonNode getConfigurationNode() {
        // TODO should return a deep copy, see JACKSON-707
        return configurationNode;
    }

    public long getDescriptorLastModified() {
        return descriptorLastModified;
    }

    public long getConfigurationLastModified() {
        return configurationLastModified;
    }

    public boolean validate(ValidationCallback callback) {
        boolean isValid = true;

        // Validate descriptor data
        if (!checkRequiredNonEmptyStringNode(descriptorNode, ATT_NAME)) {
            callback.descriptorError("Required non-empty string attribute: " + ATT_NAME); //$NON-NLS-1$
            isValid = false;
        }
        if (!checkRequiredNonEmptyStringNode(descriptorNode, ATT_URL)) {
            callback.descriptorError("Required non-empty string attribute: " + ATT_URL); //$NON-NLS-1$
            isValid = false;
        }
        if (!checkOptionalObjectNode(descriptorNode, ATT_CONFIG)) {
            callback.descriptorError("Optional attribute must be object: " + ATT_CONFIG); //$NON-NLS-1$
            isValid = false;
        }
        if (!checkOptionalNonEmptyStringNode(descriptorNode, ATT_RESOURCEPATH)) {
            callback.descriptorError("Optional attribute must be non-empty string: " + ATT_RESOURCEPATH); //$NON-NLS-1$
            isValid = false;
        }
        if (!checkOptionalBooleanNode(descriptorNode, ATT_LAZYLOAD)) {
            callback.descriptorError("Optional attribute must be boolean: " + ATT_LAZYLOAD); //$NON-NLS-1$
            isValid = false;
        }

        // Validate configuration data
        if (!checkOptionalObjectNode(configurationNode, ATT_CONFIG)) {
            callback.configurationError("Optional attribute must be object: " + ATT_CONFIG); //$NON-NLS-1$
            isValid = false;
        }
        if (!checkOptionalBooleanNode(configurationNode, ATT_ENABLED)) {
            callback.configurationError("Optional attribute must be boolean: " + ATT_ENABLED); //$NON-NLS-1$
            isValid = false;
        }
        if (!checkOptionalIntegerNode(configurationNode, ATT_ORDER)) {
            callback.configurationError("Optional attribute must be integer: " + ATT_ORDER); //$NON-NLS-1$
            isValid = false;
        }

        return isValid;
    }

    boolean checkRequiredNonEmptyStringNode(JsonNode root, String fieldName) {
        JsonNode target = root.path(fieldName);
        return !target.isMissingNode() && target.isTextual() && !target.getTextValue().trim().isEmpty();
    }

    boolean checkOptionalNonEmptyStringNode(JsonNode root, String fieldName) {
        JsonNode target = root.path(fieldName);
        return !target.isMissingNode() ? target.isTextual() && !target.getTextValue().trim().isEmpty() : true;
    }

    boolean checkOptionalObjectNode(JsonNode root, String fieldName) {
        JsonNode target = root.path(fieldName);
        return !target.isMissingNode() ? target.isObject() : true;
    }

    boolean checkOptionalBooleanNode(JsonNode root, String fieldName) {
        JsonNode target = root.path(fieldName);
        return !target.isMissingNode() ? target.isBoolean() : true;
    }

    boolean checkOptionalIntegerNode(JsonNode root, String fieldName) {
        JsonNode target = root.path(fieldName);
        return !target.isMissingNode() ? target.isIntegralNumber() : true;
    }

    /**
     * Applies custom (user-defined) configuration on top of default (descriptor-defined) configuration, and returns the
     * resulting JSON object.
     */
    public ObjectNode mergeConfiguration() {
        JsonNode descriptorConfigNode = descriptorNode.path(ATT_CONFIG);
        ObjectNode result = nodeFactory.objectNode();

        // Apply default configuration, if any
        if (!descriptorConfigNode.isMissingNode() && descriptorConfigNode.isObject()) {
            result.putAll((ObjectNode) descriptorConfigNode);
        }

        // Apply custom configuration, if any
        JsonNode customConfigNode = configurationNode.path(ATT_CONFIG);
        if (customConfigNode != null && !customConfigNode.isMissingNode() && customConfigNode.isObject()) {
            result.putAll((ObjectNode) customConfigNode);
        }

        return result;
    }

    public String getName() {
        return descriptorNode.path(ATT_NAME).getTextValue();
    }

    public String getUrl() {
        return descriptorNode.path(ATT_URL).getTextValue();
    }

    public String getResourcePath() {
        JsonNode target = descriptorNode.path(ATT_RESOURCEPATH);
        return !target.isMissingNode() ? target.getTextValue() : null;
    }

    public boolean isLazyLoad() {
        JsonNode target = descriptorNode.path(ATT_LAZYLOAD);
        return !target.isMissingNode() ? target.getBooleanValue() : true;
    }

    public boolean isEnabled() {
        JsonNode target = configurationNode.path(ATT_ENABLED);
        return !target.isMissingNode() ? target.getBooleanValue() : true;
    }

    public int getOrder() {
        JsonNode target = configurationNode.path(ATT_ORDER);
        return !target.isMissingNode() ? target.getIntValue() : Integer.MAX_VALUE;
    }

    /**
     * Natural comparison method based on {@linkplain #getOrder plugin load order}.
     */
    @Override
    public int compareTo(PluginData other) {
        if (this == other) {
            return 0;
        }

        int o1 = this.getOrder();
        int o2 = other.getOrder();

        if (o1 < o2) {
            return -1;
        } else if (o1 > o2) {
            return 1;
        }

        return 0;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (!(obj instanceof PluginData)) {
            return false;
        }

        PluginData other = (PluginData) obj;
        return Objects.equals(getOrder(), other.getOrder());
    }

    @Override
    public int hashCode() {
        return Objects.hashCode(getOrder());
    }

    @Override
    public String toString() {
        return "Plugin " + getName(); //$NON-NLS-1$
    }

}
