/*
 * Copyright 2009-2013 Roland Huss
 *
 * 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.jolokia.server.core.request;

import java.util.ArrayList;
import java.util.Deque;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.jolokia.core.util.EscapeUtil;
import org.jolokia.json.JSONArray;
import org.jolokia.json.JSONObject;
import org.jolokia.server.core.util.RequestType;

/**
 * Factory for creating {@link JolokiaRequest Jolokia requests}
 *
 * @author roland
 * @since Oct 29, 2009
 */
public final class JolokiaRequestFactory {

    // private constructor for static class
    private JolokiaRequestFactory() { }

    /**
     * <p>
     * Create a JMX request from a GET Request with a REST URL.
     * <p>
     * The REST URL which gets recognized has the following format:
     * <p>
     * {@code
     *    <base_url>/<type>/<param1>/<param2>/...
     * }
     * <p>
     * where <code>base_url<code> is the URL specifying the overall servlet (including
     * the servlet context, something like "http://localhost:8080/j4p-agent"),
     * <code>type</code> the operational mode and <code>param1 .. paramN<code>
     * the provided parameters which are dependent on the <code>type<code>
     * <p>
     * The following types are recognized so far, along with there parameters:
     *
     * <ul>
     *   <li>Type: <b>read</b> ({@link RequestType#READ})<br/>
     *       Parameters: <code>param1<code> = MBean name, <code>param2</code> = Attribute name,
     *       <code>param3 ... paramN</code> = Inner Path.
     *       The inner path is optional and specifies a path into complex MBean attributes
     *       like collections or maps. If within collections/arrays/tabular data,
     *       <code>paramX</code> should specify
     *       a numeric index, in maps/composite data <code>paramX</code> is a used as a string
     *       key.</li>. If the attribute name contains "," it is interpreted as a list of attributes.
     *       which should be returned.
     *   <li>Type: <b>write</b> ({@link RequestType#WRITE})<br/>
     *       Parameters: <code>param1</code> = MBean name, <code>param2</code> = Attribute name,
     *       <code>param3</code> = value, <code>param4 ... paramN</code> = Inner Path.
     *       The value must be URL encoded (with UTF-8 as charset), and must be convertible into
     *       a data structure</li>
     *   <li>Type: <b>exec</b> ({@link RequestType#EXEC})<br/>
     *       Parameters: <code>param1</code> = MBean name, <code>param2</code> = operation name,
     *       <code>param4 ... paramN</code> = arguments for the operation.
     *       The arguments must be URL encoded (with UTF-8 as charset), and must be convertable into
     *       a data structure</li>
     *    <li>Type: <b>version</b> ({@link RequestType#VERSION})<br/>
     *        Parameters: none
     *    <li>Type: <b>search</b> ({@link RequestType#SEARCH})<br/>
     *        Parameters: <code>param1</code> = MBean name pattern
     *    <li>Type: <b>list</b> ({@link RequestType#LIST})<br/>
     *        Parameters: <code>param1 ... paramN</code> = Inner Path.
     *    <li>Type: <b>notification</b> ({@link RequestType#NOTIFICATION})<br/>
     *        Parameters: <code>param1</code> = Client ID, <code>param2 ... paramN</code> = Notification command
     *        specific parameters.
     * </ul>
     *
     * @param pPathInfo path info of HTTP request
     * @param pProcessingParameters processing parameters. Must not be null/
     * @return a newly created {@link JolokiaRequest}
     */
    public static <R extends JolokiaRequest> R createGetRequest(String pPathInfo, ProcessingParameters pProcessingParameters)
            throws BadRequestException {
        String pathInfo = extractPathInfo(pPathInfo, pProcessingParameters);

        // Get all path elements as a reverse stack
        Deque<String> elements = EscapeUtil.extractElementsFromPath(pathInfo);

        // Use version by default if no type is given - consumes (pop) one element from the path
        RequestType type;
        try {
            type = !elements.isEmpty() ? RequestType.getTypeByName(elements.pop()) : RequestType.VERSION;
        } catch (IllegalArgumentException e) {
            throw new BadRequestException(e.getMessage());
        }

        // Parse request
        RequestCreator<R> creator = getCreator(type);
        return creator.create(elements, pProcessingParameters);
    }

    /**
     * Create a single {@link JolokiaRequest} from a JSON map representation of the request
     *
     * @param pRequestMap JSON representation of a {@link JolokiaRequest}
     * @param pProcessingParams additional map of operational parameters. Must not be null.
     * @return the created {@link JolokiaRequest}
     */
    public static <R extends JolokiaRequest> R createPostRequest(JSONObject pRequestMap, ProcessingParameters pProcessingParams)
            throws BadRequestException {
        // "config" should be a map - we don't support any other type for "config" field
        Object config = pRequestMap.get("config");
        ProcessingParameters paramsMerged = config instanceof JSONObject
            ? pProcessingParams.mergedParams((JSONObject) pRequestMap.get("config"))
            : pProcessingParams;
        RequestType type;
        try {
            type = RequestType.getTypeByName((String) pRequestMap.get("type"));
        } catch (IllegalArgumentException e) {
            throw new BadRequestException(e.getMessage());
        }
        RequestCreator<R> creator = getCreator(type);
        return creator.create(pRequestMap, paramsMerged);
    }

    /**
     * Create a list of {@link JolokiaRequest}s from a JSON list representing jmx requests
     *
     * @param pJsonRequests JSON representation of a list of {@link JolokiaRequest}
     * @param pProcessingParams processing options. Must not be null.
     * @return list with one or more {@link JolokiaRequest}
     */
    public static List<JolokiaRequest> createPostRequests(JSONArray pJsonRequests, ProcessingParameters pProcessingParams)
            throws BadRequestException {
        List<JolokiaRequest> ret = new ArrayList<>();
        for (Object o : pJsonRequests) {
            if (!(o instanceof JSONObject)) {
                throw new IllegalArgumentException("Not a request within the list of requests " + pJsonRequests +
                        ". Expected JSONObject, but found: " + o);
            }
            ret.add(createPostRequest((JSONObject) o,pProcessingParams));
        }
        return ret;
    }

    // ========================================================================================================

    // Extract path info either from the 'real' URL path, or from an request parameter
    private static String extractPathInfo(String pPathInfo, ProcessingParameters pProcessingParams) {
        String pathInfo = pPathInfo;

        // If no pathinfo is given directly, we look for a query parameter named 'p'.
        // This variant is helpful, if there are problems with the server mangling
        // up the pathinfo (e.g. for security concerns, often '/','\',';' and other are not
        // allowed in encoded form within the pathinfo)
        if (pProcessingParams != null && (pPathInfo == null || pPathInfo.isEmpty() || pathInfo.matches("^/+$"))) {
            pathInfo = pProcessingParams.getPathInfo();
        }
        return normalizePathInfo(pathInfo);
    }

    // Return always a non-null string and strip of leading slash
    private static String normalizePathInfo(String pPathInfo) {
        if (pPathInfo != null && !pPathInfo.isEmpty()) {
            return pPathInfo.startsWith("/") ? pPathInfo.substring(1) : pPathInfo;
        } else {
            return "";
        }
    }

    // ==================================================================================
    // Dedicated creator for the various operations. They are installed as static processors.

    // Get the request creator for a specific type
    @SuppressWarnings("unchecked")
    private static <R extends RequestCreator<T>, T extends JolokiaRequest> R getCreator(RequestType pType) {
        RequestCreator<?> creator = CREATOR_MAP.get(pType);
        if (creator == null) {
            throw new UnsupportedOperationException("Type " + pType + " is not supported (yet)");
        }
        return (R) creator;
    }

    private static final Map<RequestType,RequestCreator<?>> CREATOR_MAP;

    static {
        CREATOR_MAP = new HashMap<>();
        CREATOR_MAP.put(RequestType.READ, JolokiaReadRequest.newCreator());
        CREATOR_MAP.put(RequestType.WRITE, JolokiaWriteRequest.newCreator());
        CREATOR_MAP.put(RequestType.EXEC, JolokiaExecRequest.newCreator());
        CREATOR_MAP.put(RequestType.LIST, JolokiaListRequest.newCreator());
        CREATOR_MAP.put(RequestType.VERSION, JolokiaVersionRequest.newCreator());
        CREATOR_MAP.put(RequestType.SEARCH, JolokiaSearchRequest.newCreator());
        CREATOR_MAP.put(RequestType.NOTIFICATION, JolokiaNotificationRequest.newCreator());
        CREATOR_MAP.put(RequestType.CONFIG, JolokiaConfigRequest.newCreator());
    }

}
