/**
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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.apache.camel.impl;

import java.util.List;
import java.util.Optional;

import org.apache.camel.NamedNode;
import org.apache.camel.model.FromDefinition;
import org.apache.camel.model.RouteDefinition;
import org.apache.camel.model.rest.RestDefinition;
import org.apache.camel.model.rest.VerbDefinition;
import org.apache.camel.spi.NodeIdFactory;

/**
 * Factory for generating route ids based on uris.
 * <p>
 * For direct/seda routes it returns route name (direct:start -> start).
 * For rest routes it returns its method and context path formatted as one string.
 * <p>
 * When id cannot be generated, falls back to other {@link NodeIdFactory} implementation.
 * If none is passed in the constructor, then {@link DefaultNodeIdFactory} is used.
 */
public class RouteIdFactory implements NodeIdFactory {

    private static final char SEPARATOR = '-';
    private NodeIdFactory defaultNodeIdFactory;

    public RouteIdFactory() {
        defaultNodeIdFactory = new DefaultNodeIdFactory();
    }

    public RouteIdFactory(NodeIdFactory defaultNodeIdFactory) {
        this.defaultNodeIdFactory = defaultNodeIdFactory;
    }

    @Override
    public String createId(NamedNode definition) {
        if (definition instanceof RouteDefinition) {
            Optional<String> id = extractId((RouteDefinition) definition);

            if (id.isPresent()) {
                return id.get();
            }

            id = extractIdFromRestDefinition((RouteDefinition) definition);

            if (id.isPresent()) {
                return id.get();
            }
        }

        if (definition instanceof VerbDefinition) {
            Optional<String> id = extractIdFromVerb((VerbDefinition) definition);

            if (id.isPresent()) {
                return id.get();
            }
        }

        return defaultNodeIdFactory.createId(definition);
    }

    /**
     * Extract id from routes
     */
    private Optional<String> extractId(RouteDefinition routeDefinition) {
        if (routeDefinition.getRestDefinition() != null) {
            return Optional.empty();
        }

        List<FromDefinition> inputs = routeDefinition.getInputs();

        if (inputs == null || inputs.isEmpty()) {
            return Optional.empty();
        }

        FromDefinition from = inputs.get(0);
        String uri = from.getUri();

        // we want to use the context-path of the route
        int colon = uri.indexOf(':');

        if (colon > 0) {
            String name = uri.substring(colon + 1);

            int questionMark = name.indexOf("?");

            if (questionMark > 0) {
                return Optional.of(name.substring(0, questionMark));
            } else {
                return Optional.of(name);
            }
        }

        return Optional.empty();
    }

    /**
     * Extract id from a rest route.
     */
    private Optional<String> extractIdFromRestDefinition(RouteDefinition route) {
        if (route.getRestDefinition() != null) {
            return extractIdFromInput(route);
        }

        return Optional.empty();
    }

    /**
     * Extract id from a rest verb definition.
     */
    private Optional<String> extractIdFromVerb(VerbDefinition verb) {
        RestDefinition restDefinition = verb.getRest();

        if (restDefinition != null) {
            StringBuilder routeId = new StringBuilder();
            routeId.append(verb.asVerb());
            appendWithSeparator(routeId, prepareUri(restDefinition.getPath()));

            if (verb.getUri() != null && verb.getUri().length() > 0) {
                appendWithSeparator(routeId, prepareUri(verb.getUri()));
            }

            verb.setUsedForGeneratingNodeId(true);

            return Optional.of(routeId.toString());
        }

        return Optional.empty();

    }

    /**
     * Extract id from rest input uri.
     */
    private Optional<String> extractIdFromInput(RouteDefinition route) {
        List<FromDefinition> inputs = route.getInputs();

        if (inputs == null || inputs.isEmpty()) {
            return Optional.empty();
        }

        FromDefinition from = inputs.get(0);
        String uri = from.getUri();

        String[] uriSplitted = uri.split(":");

        // needs to have at least 3 fields
        if (uriSplitted.length < 3) {
            return Optional.empty();
        }

        String verb = uriSplitted[1];
        String contextPath = uriSplitted[2];
        String additionalUri = "";

        if (uriSplitted.length > 3 && uriSplitted[3].startsWith("/")) {
            additionalUri = uriSplitted[3];
        }

        StringBuilder routeId = new StringBuilder(verb.length() + contextPath.length() + additionalUri.length());

        routeId.append(verb);
        appendWithSeparator(routeId, prepareUri(contextPath));

        if (additionalUri.length() > 0) {
            appendWithSeparator(routeId, prepareUri(additionalUri));
        }

        return Optional.of(routeId.toString());
    }

    /**
     * Prepares uri to be part of the id.
     */
    private String prepareUri(String uri) {
        if (uri == null) {
            return "";
        }

        if (uri.contains("?")) {
            uri = uri.substring(0, uri.indexOf('?'));
        }

        return uri.replaceAll("/", String.valueOf(SEPARATOR));
    }

    /**
     * Appends new element to the builder.
     */
    private void appendWithSeparator(StringBuilder builder, String str) {
        if (builder.charAt(builder.length() - 1) == SEPARATOR) {
            if (str.startsWith(String.valueOf(SEPARATOR))) {
                builder.append(str.replaceFirst(String.valueOf(SEPARATOR), ""));
            } else {
                builder.append(str);
            }
        } else {
            if (!str.startsWith(String.valueOf(SEPARATOR))) {
                builder.append(SEPARATOR);
            }

            builder.append(str);
        }
    }
}
