/*
 * Copyright © 2017 Ivar Grimstad ([email protected])
 *
 * 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.mvcspec.ozark.uri;

import org.mvcspec.ozark.util.AnnotationUtils;

import javax.enterprise.inject.Vetoed;
import javax.mvc.UriRef;
import javax.ws.rs.core.MultivaluedHashMap;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.UriBuilder;
import java.lang.reflect.Method;
import java.net.URI;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * <p>A cache for all parsed instances of {@link UriTemplate}
 * providing methods to generate URIs of current application.</p>
 *
 * @author Florian Hirsch
 */
@Vetoed // produced by UriTemplateParser
public class ApplicationUris {

    private MultivaluedMap<String, UriTemplate> uriTemplates = new MultivaluedHashMap<>();

    /**
     * @see javax.mvc.MvcContext#uri(String)
     */
    public URI get(String identifier) {
        return getUriBuilder(identifier).build();
    }

    /**
     * @see javax.mvc.MvcContext#uri(String, Map)
     */
    public URI get(String identifier, Map<String, Object> params) {
        UriTemplate uriTemplate = getUriTemplate(identifier);
        UriBuilder uriBuilder = UriBuilder.fromUri(uriTemplate.path());
        Map<String, Object> pathParams = new HashMap<>();
        // Everything which is not defined as query- or matrix-param should be a path-param
        params.forEach((key, value) -> {
            if (uriTemplate.queryParams().contains(key)) {
                uriBuilder.queryParam(key, value);
            } else if (uriTemplate.matrixParams().contains(key)) {
                uriBuilder.matrixParam(key, value);
            } else {
                pathParams.put(key, value);
            }
        });
        return uriBuilder.buildFromMap(pathParams);
    }

    /**
     * @see javax.mvc.MvcContext#uriBuilder(String)
     */
    public UriBuilder getUriBuilder(String identifier) {
        UriTemplate uriTemplate = getUriTemplate(identifier);
        return UriBuilder.fromUri(uriTemplate.path());
    }

    /**
     * <p>Registers given uriTemplate by the value of
     * the {@link UriRef} annotation if present on given method
     * <em>and</em> by the simple name of the controller class
     * and the method name separated by '#' (MyController#myMethod).</p>
     *
     * <p>We don't validate ambigous usage of e.g. MyController#myMethod as this
     * is valid if mvc#uri methods are not used.</p>
     */
    void register(UriTemplate uriTemplate, Method method) {
        UriRef uriRef = AnnotationUtils.getAnnotation(method, UriRef.class);
        if (uriRef != null) {
            merge(uriRef.value(), uriTemplate);
        }
        String identifier = String.format("%s#%s", method.getDeclaringClass().getSimpleName(), method.getName());
        merge(identifier, uriTemplate);
    }

    /**
     * <p>Merges given UriTemplate into the Cache.
     * If one or more templates for given identifier is found
     * we check if the exisiting templates have the same path.
     * If so we merge the optional query- and matrix params to the template.
     * It's valid to reuse an identifier for the same path for different HTTP methods.
     * If the pathes don't match the usage of the identifier is amigous and
     * we store both templates to inform the developer about all invalid usages.</p>
     */
    private void merge(String identifier, UriTemplate uriTemplate) {
        if (!uriTemplates.containsKey(identifier)) {
            uriTemplates.add(identifier, uriTemplate);
            return;
        }
        Optional<UriTemplate> existingTemplate = uriTemplates.get(identifier).stream().filter(template
            -> template.path().equals(uriTemplate.path())).findFirst();
        if (existingTemplate.isPresent()) {
            UriTemplate template = existingTemplate.get();
            template.queryParams().addAll(uriTemplate.queryParams());
            template.matrixParams().addAll(uriTemplate.matrixParams());
        } else {
            uriTemplates.add(identifier, uriTemplate);
        }
    }

    /**
     * @return the UriTemplate for given identifier from the cache.
     * @throws IllegalArgumentException if no UriTemplate
     * is found for given identifier or if ambiguously used
     * templates are found.
     */
    private UriTemplate getUriTemplate(String identifier) {
        Objects.requireNonNull(identifier, "identifier must not be null");
        if (!uriTemplates.containsKey(identifier)) {
            throw new IllegalArgumentException(
                String.format("No uriTemplate registered for identifier '%s'", identifier));
        }
        List<UriTemplate> registeredTemplats = uriTemplates.get(identifier);
        if (registeredTemplats.size() > 1) {
            throw new IllegalArgumentException(String.format(
                "Ambiguous usage of identifier '%s' for following URIs: %s", identifier,
                registeredTemplats.stream().map(UriTemplate::path).collect(Collectors.toList())));
        }
        return this.uriTemplates.getFirst(identifier);
    }

    /**
     * @return all registered UriTemplates with their identifier.
     */
    Set<Map.Entry<String, List<UriTemplate>>> list() {
        return uriTemplates.entrySet();
    }

    @Override
    public String toString() {
        return "ApplicationUris{" + "uriTemplates=" + uriTemplates + '}';
    }

}