/*
 * Copyright 2016 Martijn Brekhof. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.xbmc.kore.testutils.tcpserver.handlers.jsonrpc;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;

import org.xbmc.kore.utils.LogUtils;

import java.io.IOException;

public abstract class JsonResponse {
    private final String TAG = LogUtils.makeLogTag(JsonResponse.class);

    private final ObjectNode jsonResponse;

    private static final ObjectMapper objectMapper = new ObjectMapper();

    private static final String RESULT_NODE = "result";
    private static final String PARAMS_NODE = "params";
    private static final String METHOD_NODE = "method";
    private static final String DATA_NODE = "data";
    protected static final String ID_NODE = "id";
    private static final String JSONRPC_NODE = "jsonrpc";

    public enum TYPE {
        OBJECT,
        ARRAY
    };

    public JsonResponse() {
        jsonResponse = objectMapper.createObjectNode();
        jsonResponse.put(JSONRPC_NODE, "2.0");
    }

    public JsonResponse(int id) {
        this();
        jsonResponse.put(ID_NODE, id);
    }

    public JsonResponse(int id, String jsonString) throws IOException {
        jsonResponse = (ObjectNode) objectMapper.readTree(jsonString);
        jsonResponse.put(JSONRPC_NODE, "2.0");
        jsonResponse.put(ID_NODE, id);
    }

    protected ObjectNode createObjectNode() {
        return objectMapper.createObjectNode();
    }

    protected ArrayNode createArrayNode() {
        return objectMapper.createArrayNode();
    }

    /**
     * Returns the node used to hold the result. First call will create the
     * result node for the given type
     * @param type that result node should be when first created
     * @return result node
     */
    protected JsonNode getResultNode(TYPE type) {
        JsonNode result;
        if(jsonResponse.has(RESULT_NODE)) {
            result = jsonResponse.get(RESULT_NODE);
            if( result.isArray() && type != TYPE.ARRAY ) {
                LogUtils.LOGE(TAG, "requested result node of type Object but response contains result node of type Array");
                return null;
            }
        } else {
            switch (type) {
                case ARRAY:
                    result = objectMapper.createArrayNode();
                    break;
                case OBJECT:
                default:
                    result = objectMapper.createObjectNode();
                    break;
            }
            jsonResponse.set(RESULT_NODE, result);
        }

        return result;
    }

    /**
     * Returns the parameters node of the json request object
     * Creates one if necessary
     * @return Parameters node
     */
    private ObjectNode getParametersNode() {
        ObjectNode params;
        if (jsonResponse.has(PARAMS_NODE)) {
            params = (ObjectNode)jsonResponse.get(PARAMS_NODE);
        } else {
            params = objectMapper.createObjectNode();
            jsonResponse.set(PARAMS_NODE, params);
        }

        return params;
    }

    private ObjectNode getDataNode() {
        ObjectNode data = null;
        if (jsonResponse.has(PARAMS_NODE)) {
            ObjectNode params = (ObjectNode)jsonResponse.get(PARAMS_NODE);
            if(params.has(DATA_NODE)) {
                data = (ObjectNode) params.get(DATA_NODE);
            }
        }

        if ( data == null ) {
            data = objectMapper.createObjectNode();
            ObjectNode params = getParametersNode();
            params.set(DATA_NODE, data);
        }

        return data;
    }

    protected void setResultToResponse(JsonNode value) {
        jsonResponse.set(RESULT_NODE, value);
    }

    protected void setResultToResponse(boolean value) {
        jsonResponse.put(RESULT_NODE, value);
    }

    protected void setResultToResponse(int value) {
        jsonResponse.put(RESULT_NODE, value);
    }

    protected void setResultToResponse(String value) {
        jsonResponse.put(RESULT_NODE, value);
    }

    protected void setLimits(int start, int end, int total) {
        ObjectNode limits = createObjectNode();
        limits.put("start", start);
        limits.put("end", end);
        limits.put("total", total);

        ((ObjectNode) getResultNode(TYPE.OBJECT)).set("limits", limits);
    }

    /**
     * Adds the value to the array in node with the given key.
     * If the array does not exist it will be created
     * and added.
     * @param node ObjectNode that should contain an entry with key with an array as value
     * @param key the key of the item in ObjectNode that should hold the array
     * @param value the value to be added to the array
     */
    protected void addToArrayNode(ObjectNode node, String key, String value) {
        JsonNode jsonNode = node.get(key);
        if (jsonNode == null) {
            jsonNode = objectMapper.createArrayNode();
            node.set(key, jsonNode);
        }

        if (jsonNode.isArray()) {
            ((ArrayNode) jsonNode).add(value);
        } else {
            LogUtils.LOGE(TAG, "JsonNode at key: " + key + " not of type ArrayNode." );
        }
    }

    /**
     * Adds the value to the array in node with the given key.
     * If the array does not exist it will be created
     * and added.
     * @param node ObjectNode that should contain an entry with key with an array as value
     * @param key the key of the item in ObjectNode that should hold the array
     * @param value the value to be added to the array
     */
    protected void addToArrayNode(ObjectNode node, String key, ObjectNode value) {
        JsonNode jsonNode = node.get(key);
        if (jsonNode == null) {
            jsonNode = objectMapper.createArrayNode();
            node.set(key, jsonNode);
        }

        if (jsonNode.isArray()) {
            ((ArrayNode) jsonNode).add(value);
        } else {
            LogUtils.LOGE(TAG, "JsonNode at key: " + key + " not of type ArrayNode." );
        }
    }

    protected void addToArrayNode(ObjectNode node, String key, JsonNode value) {
        JsonNode jsonNode = node.get(key);
        if (jsonNode == null) {
            jsonNode = objectMapper.createArrayNode();
            node.set(key, jsonNode);
        }

        if (jsonNode.isArray()) {
            ((ArrayNode) jsonNode).add(value);
        } else {
            LogUtils.LOGE(TAG, "JsonNode at key: " + key + " not of type ArrayNode." );
        }
    }

    protected void addDataToResponse(String parameter, boolean value) {
        getDataNode().put(parameter, value);
    }

    protected void addDataToResponse(String parameter, int value) {
        getDataNode().put(parameter, value);
    }

    protected void addDataToResponse(String parameter, ObjectNode node) {
        getDataNode().set(parameter, node);
    }

    protected void addParameterToResponse(String parameter, String value) {
        getParametersNode().put(parameter, value);
    }

    protected void addMethodToResponse(String method) {
        jsonResponse.put(METHOD_NODE, method);
    }

    public ObjectNode getResponseNode() {
        return jsonResponse;
    }

    public JsonNode getResultNode() {
        return jsonResponse.get(RESULT_NODE);
    }

    public String getId() {
        return jsonResponse.get(ID_NODE).asText();
    }

    public String getMethod() {
        return jsonResponse.get(METHOD_NODE).asText();
    }

    public String toJsonString() {
        return jsonResponse.toString();
    }
}
