/*
 *  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
 *
 *      https://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.ivy.core.module.id;

import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import org.apache.ivy.plugins.matcher.MapMatcher;
import org.apache.ivy.util.Checks;
import org.apache.ivy.util.Message;
import org.apache.ivy.util.filter.Filter;
import org.apache.ivy.util.filter.NoFilter;

/**
 * A list of module specific rules.
 * <p>
 * This class defines a list of module specific rules. For each module only one rule apply,
 * sometimes none.
 * </p>
 * <p>
 * To know which rule to apply, they are configured using matchers. So you can define a rule
 * applying to all module from one particular organization, or to all modules with a revisions
 * matching a pattern, and so on.
 * </p>
 * <p>
 * Rules condition are evaluated in order, so the first matching rule is returned.
 * </p>
 * <p>
 * Rules themselves can be represented by any object, depending on the purpose of the rule (define
 * which resolver to use, which TTL in cache, ...)
 * </p>
 *
 * @param <T> a type parameter
 */
public class ModuleRules<T> {

    private Map<MapMatcher, T> rules = new LinkedHashMap<>();

    private MatcherLookup matcherLookup = new MatcherLookup();

    /**
     * Constructs an empty ModuleRules.
     */
    public ModuleRules() {
    }

    private ModuleRules(Map<MapMatcher, T> rules) {
        this.rules = new LinkedHashMap<>(rules);
        for (MapMatcher matcher : rules.keySet()) {
            matcherLookup.add(matcher);
        }
    }

    /**
     * Defines a new rule for the given condition.
     *
     * @param condition
     *            the condition for which the rule should be applied. Must not be <code>null</code>.
     * @param rule
     *            the rule to apply. Must not be <code>null</code>.
     */
    public void defineRule(MapMatcher condition, T rule) {
        Checks.checkNotNull(condition, "condition");
        Checks.checkNotNull(rule, "rule");

        rules.put(condition, rule);
        matcherLookup.add(condition);
    }

    /**
     * Returns the rule object matching the given {@link ModuleId}, or <code>null</code> if no rule
     * applies.
     *
     * @param mid
     *            the {@link ModuleId} to search the rule for. Must not be <code>null</code>.
     * @return the rule object matching the given {@link ModuleId}, or <code>null</code> if no rule
     *         applies.
     * @see #getRule(ModuleId, Filter)
     */
    public T getRule(ModuleId mid) {
        return getRule(mid, NoFilter.<T> instance());
    }

    /**
     * Returns the rules objects matching the given {@link ModuleId}, or an empty array if no rule
     * applies.
     *
     * @param mid
     *            the {@link ModuleId} to search the rule for. Must not be <code>null</code>.
     * @return an array of rule objects matching the given {@link ModuleId}.
     */
    public List<T> getRules(ModuleId mid) {
        return getRules(mid.getAttributes(), NoFilter.<T> instance());
    }

    /**
     * Returns the rule object matching the given {@link ModuleRevisionId}, or <code>null</code> if
     * no rule applies.
     *
     * @param mrid
     *            the {@link ModuleRevisionId} to search the rule for. Must not be <code>null</code>
     *            .
     * @return the rule object matching the given {@link ModuleRevisionId}, or <code>null</code> if
     *         no rule applies.
     * @see #getRule(ModuleRevisionId, Filter)
     */
    public T getRule(ModuleRevisionId mrid) {
        return getRule(mrid, NoFilter.<T> instance());
    }

    /**
     * Returns the rule object matching the given {@link ModuleId} and accepted by the given
     * {@link Filter}, or <code>null</code> if no rule applies.
     *
     * @param mid
     *            the {@link ModuleRevisionId} to search the rule for. Must not be <code>null</code>
     *            .
     * @param filter
     *            the filter to use to filter the rule to return. The {@link Filter#accept(Object)}
     *            method will be called only with rule objects matching the given {@link ModuleId},
     *            and the first rule object accepted by the filter will be returned. Must not be
     *            <code>null</code>.
     * @return the rule object matching the given {@link ModuleId}, or <code>null</code> if no rule
     *         applies.
     * @see #getRule(ModuleRevisionId, Filter)
     */
    public T getRule(ModuleId mid, Filter<T> filter) {
        Checks.checkNotNull(mid, "mid");
        return getRule(mid.getAttributes(), filter);
    }

    /**
     * Returns the rule object matching the given {@link ModuleRevisionId} and accepted by the given
     * {@link Filter}, or <code>null</code> if no rule applies.
     *
     * @param mrid
     *            the {@link ModuleRevisionId} to search the rule for. Must not be <code>null</code>
     *            .
     * @param filter
     *            the filter to use to filter the rule to return. The {@link Filter#accept(Object)}
     *            method will be called only with rule objects matching the given
     *            {@link ModuleRevisionId}, and the first rule object accepted by the filter will be
     *            returned. Must not be <code>null</code>.
     * @return the rule object matching the given {@link ModuleRevisionId}, or <code>null</code> if
     *         no rule applies.
     * @see #getRule(ModuleRevisionId)
     */
    public T getRule(ModuleRevisionId mrid, Filter<T> filter) {
        Checks.checkNotNull(mrid, "mrid");
        Checks.checkNotNull(filter, "filter");
        Map<String, String> moduleAttributes = mrid.getAttributes();
        return getRule(moduleAttributes, filter);
    }

    private T getRule(Map<String, String> moduleAttributes, Filter<T> filter) {
        for (MapMatcher midm : matcherLookup.get(moduleAttributes)) {
            T rule = rules.get(midm);
            if (filter.accept(rule)) {
                return rule;
            }
        }
        return null;
    }

    /**
     * Returns the rules object matching the given {@link ModuleRevisionId} and accepted by the
     * given {@link Filter}, or an empty array if no rule applies.
     *
     * @param mrid
     *            the {@link ModuleRevisionId} to search the rule for. Must not be <code>null</code>
     *            .
     * @param filter
     *            the filter to use to filter the rule to return. The {@link Filter#accept(Object)}
     *            method will be called only with rule objects matching the given
     *            {@link ModuleRevisionId}. Must not be <code>null</code>.
     * @return an array of rule objects matching the given {@link ModuleRevisionId}.
     */
    public List<T> getRules(ModuleRevisionId mrid, Filter<T> filter) {
        Checks.checkNotNull(mrid, "mrid");
        Checks.checkNotNull(filter, "filter");
        Map<String, String> moduleAttributes = mrid.getAttributes();
        return getRules(moduleAttributes, filter);
    }

    private List<T> getRules(Map<String, String> moduleAttributes, Filter<T> filter) {
        List<T> matchingRules = new ArrayList<>();
        for (MapMatcher midm : matcherLookup.get(moduleAttributes)) {
            T rule = rules.get(midm);
            if (filter.accept(rule)) {
                matchingRules.add(rule);
            }
        }
        return matchingRules;
    }

    /**
     * Dump the list of rules to {@link Message#debug(String)}
     *
     * @param prefix
     *            the prefix to use for each line dumped
     */
    public void dump(String prefix) {
        if (rules.isEmpty()) {
            Message.debug(prefix + "NONE");
            return;
        }
        for (Map.Entry<MapMatcher, T> entry : rules.entrySet()) {
            MapMatcher midm = entry.getKey();
            T rule = entry.getValue();
            Message.debug(prefix + midm + " -> " + rule);
        }
    }

    /**
     * Returns an unmodifiable view of all the rules defined on this ModuleRules.
     * <p>
     * The rules are returned in a Map where they keys are the MapMatchers matching the rules
     * object, and the values are the rules object themselves.
     * </p>
     *
     * @return an unmodifiable view of all the rules defined on this ModuleRules.
     */
    public Map<MapMatcher, T> getAllRules() {
        return Collections.unmodifiableMap(rules);
    }

    public ModuleRules<T> clone() {
        return new ModuleRules<>(rules);
    }
}
