/*
 * Copyright 2010 the original author or authors.
 *
 * 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.gradle.util;

import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.lang.StringUtils;

/**
 * Selects a single item from a list of candidates based on a camel-case pattern.
 */
public class NameMatcher {
    private final SortedSet<String> matches = new TreeSet<String>();
    private final Set<String> candidates = new TreeSet<String>();
    private String pattern;

    /**
     * Locates the best match for the given pattern in the given set of candidate items.
     *
     * @return The matching item if exactly 1 match found, null if no matches or multiple matches.
     */
    public <T> T find(String pattern, Map<String, ? extends T> items) {
        String name = find(pattern, items.keySet());
        if (name != null) {
            return items.get(name);
        }
        return null;
    }
    
    /**
     * Locates the best match for the given pattern in the given set of candidate items.
     *
     * @return The match if exactly 1 match found, null if no matches or multiple matches.
     */
    public String find(String pattern, Collection<String> items) {
        this.pattern = pattern;
        matches.clear();
        candidates.clear();

        if (items.contains(pattern)) {
            matches.add(pattern);
            return pattern;
        }

        if (pattern.length() == 0) {
            return null;
        }

        Pattern camelCasePattern = getPatternForName(pattern);
        Pattern normalisedCamelCasePattern = Pattern.compile(camelCasePattern.pattern(), Pattern.CASE_INSENSITIVE);
        String normalisedPattern = pattern.toUpperCase();

        Set<String> matches1 = new TreeSet<String>();
        Set<String> matches2 = new TreeSet<String>();

        for (String candidate : items) {
            if (camelCasePattern.matcher(candidate).matches()) {
                matches1.add(candidate);
                continue;
            }
            if (normalisedCamelCasePattern.matcher(candidate).lookingAt()) {
                matches2.add(candidate);
                continue;
            }
            if (StringUtils.getLevenshteinDistance(normalisedPattern, candidate.toUpperCase()) <= Math.min(3,
                    pattern.length() / 2)) {
                candidates.add(candidate);
            }
        }

        if (!matches1.isEmpty()) {
            matches.addAll(matches1);
        }
        else {
            matches.addAll(matches2);
        }

        if (matches.size() == 1) {
            return matches.first();
        }

        return null;
    }

    private static Pattern getPatternForName(String name) {
        Pattern boundaryPattern = Pattern.compile("((^|\\p{Punct})\\p{javaLowerCase}+)|(\\p{javaUpperCase}\\p{javaLowerCase}*)");
        Matcher matcher = boundaryPattern.matcher(name);
        int pos = 0;
        StringBuilder builder = new StringBuilder();
        while (matcher.find()) {
            String prefix = name.substring(pos, matcher.start());
            if (prefix.length() > 0) {
                builder.append(Pattern.quote(prefix));
            }
            builder.append(Pattern.quote(matcher.group()));
            builder.append("[\\p{javaLowerCase}\\p{Digit}]*");
            pos = matcher.end();
        }
        builder.append(Pattern.quote(name.substring(pos, name.length())));
        return Pattern.compile(builder.toString());
    }

    /**
     * Returns all matches, when there were more than 1.
     *
     * @return The matches. Returns an empty set when there are no matches.
     */
    public Set<String> getMatches() {
        return matches;
    }

    /**
     * Returns the potential matches, if any.
     *
     * @return The matches. Returns an empty set when there are no potential matches.
     */
    public Set<String> getCandidates() {
        return candidates;
    }

    public String formatErrorMessage(String singularItemDescription, Object container) {
        String capItem = StringUtils.capitalize(singularItemDescription);
        if (!matches.isEmpty()) {
            return String.format("%s '%s' is ambiguous in %s. Candidates are: %s.", capItem, pattern, container, GUtil.toString(matches));
        }
        if (!candidates.isEmpty()) {
            return String.format("%s '%s' not found in %s. Some candidates are: %s.", capItem, pattern, container, GUtil.toString(candidates));
        }
        return String.format("%s '%s' not found in %s.", capItem, pattern, container);
    }
}
