/*
 *  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.plugins.version;

import java.util.Comparator;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.ivy.core.module.id.ModuleRevisionId;
import org.apache.ivy.plugins.latest.ArtifactInfo;
import org.apache.ivy.plugins.latest.LatestStrategy;

/**
 * Matches version ranges: [1.0,2.0] matches all versions greater or equal to 1.0 and lower or equal
 * to 2.0 [1.0,2.0[ matches all versions greater or equal to 1.0 and lower than 2.0 ]1.0,2.0]
 * matches all versions greater than 1.0 and lower or equal to 2.0 ]1.0,2.0[ matches all versions
 * greater than 1.0 and lower than 2.0 [1.0,) matches all versions greater or equal to 1.0 ]1.0,)
 * matches all versions greater than 1.0 (,2.0] matches all versions lower or equal to 2.0 (,2.0[
 * matches all versions lower than 2.0 This class uses a latest strategy to compare revisions. If
 * none is set, it uses the default one of the ivy instance set through setIvy(). If neither a
 * latest strategy nor a ivy instance is set, an IllegalStateException will be thrown when calling
 * accept(). Note that it can't work with latest time strategy, cause no time is known for the
 * limits of the range. Therefore only purely revision based LatestStrategy can be used.
 */
public class VersionRangeMatcher extends AbstractVersionMatcher {
    // todo: check these constants
    private static final String OPEN_INC = "[";

    private static final String OPEN_EXC = "]";

    private static final String OPEN_EXC_MAVEN = "(";

    private static final String CLOSE_INC = "]";

    private static final String CLOSE_EXC = "[";

    private static final String CLOSE_EXC_MAVEN = ")";

    private static final String LOWER_INFINITE = "(";

    private static final String UPPER_INFINITE = ")";

    private static final String SEPARATOR = ",";

    // following patterns are built upon constants above and should not be modified
    private static final String OPEN_INC_PATTERN = "\\" + OPEN_INC;

    private static final String OPEN_EXC_PATTERN = "\\" + OPEN_EXC + "\\" + OPEN_EXC_MAVEN;

    private static final String CLOSE_INC_PATTERN = "\\" + CLOSE_INC;

    private static final String CLOSE_EXC_PATTERN = "\\" + CLOSE_EXC + "\\" + CLOSE_EXC_MAVEN;

    private static final String LI_PATTERN = "\\" + LOWER_INFINITE;

    private static final String UI_PATTERN = "\\" + UPPER_INFINITE;

    private static final String SEP_PATTERN = "\\s*\\" + SEPARATOR + "\\s*";

    private static final String OPEN_PATTERN = "[" + OPEN_INC_PATTERN + OPEN_EXC_PATTERN + "]";

    private static final String CLOSE_PATTERN = "[" + CLOSE_INC_PATTERN + CLOSE_EXC_PATTERN + "]";

    private static final String ANY_NON_SPECIAL_PATTERN = "[^\\s" + SEPARATOR + OPEN_INC_PATTERN
            + OPEN_EXC_PATTERN + CLOSE_INC_PATTERN + CLOSE_EXC_PATTERN + LI_PATTERN + UI_PATTERN
            + "]";

    private static final String FINITE_PATTERN = OPEN_PATTERN + "\\s*(" + ANY_NON_SPECIAL_PATTERN
            + "+)" + SEP_PATTERN + "(" + ANY_NON_SPECIAL_PATTERN + "+)\\s*" + CLOSE_PATTERN;

    private static final String LOWER_INFINITE_PATTERN = LI_PATTERN + SEP_PATTERN + "("
            + ANY_NON_SPECIAL_PATTERN + "+)\\s*" + CLOSE_PATTERN;

    private static final String UPPER_INFINITE_PATTERN = OPEN_PATTERN + "\\s*("
            + ANY_NON_SPECIAL_PATTERN + "+)" + SEP_PATTERN + UI_PATTERN;

    private static final Pattern FINITE_RANGE = Pattern.compile(FINITE_PATTERN);

    private static final Pattern LOWER_INFINITE_RANGE = Pattern.compile(LOWER_INFINITE_PATTERN);

    private static final Pattern UPPER_INFINITE_RANGE = Pattern.compile(UPPER_INFINITE_PATTERN);

    private static final Pattern ALL_RANGE = Pattern.compile(FINITE_PATTERN + "|"
            + LOWER_INFINITE_PATTERN + "|" + UPPER_INFINITE_PATTERN);

    private final class MRIDArtifactInfo implements ArtifactInfo {
        private ModuleRevisionId mrid;

        public MRIDArtifactInfo(ModuleRevisionId id) {
            mrid = id;
        }

        public long getLastModified() {
            return 0;
        }

        public String getRevision() {
            return mrid.getRevision();
        }
    }

    private final Comparator<ModuleRevisionId> comparator = new Comparator<ModuleRevisionId>() {
        public int compare(ModuleRevisionId o1, ModuleRevisionId o2) {
            if (o1.equals(o2)) {
                return 0;
            }
            ArtifactInfo art1 = new MRIDArtifactInfo(o1);
            ArtifactInfo art2 = new MRIDArtifactInfo(o2);
            ArtifactInfo art = getLatestStrategy().findLatest(new ArtifactInfo[] {art1, art2},
                null);
            return art == art1 ? -1 : 1;
        }
    };

    private LatestStrategy latestStrategy;

    private String latestStrategyName = "default";

    public VersionRangeMatcher() {
        super("version-range");
    }

    public VersionRangeMatcher(String name) {
        super(name);
    }

    public VersionRangeMatcher(String name, LatestStrategy strategy) {
        super(name);
        this.latestStrategy = strategy;
    }

    public boolean isDynamic(ModuleRevisionId askedMrid) {
        String revision = askedMrid.getRevision();
        return ALL_RANGE.matcher(revision).matches();
    }

    public boolean accept(ModuleRevisionId askedMrid, ModuleRevisionId foundMrid) {
        String revision = askedMrid.getRevision();
        Matcher m;
        m = FINITE_RANGE.matcher(revision);
        if (m.matches()) {
            String lower = m.group(1);
            String upper = m.group(2);
            return isUpper(askedMrid, lower, foundMrid, revision.startsWith(OPEN_INC))
                    && isLower(askedMrid, upper, foundMrid, revision.endsWith(CLOSE_INC));
        }
        m = LOWER_INFINITE_RANGE.matcher(revision);
        if (m.matches()) {
            String upper = m.group(1);
            return isLower(askedMrid, upper, foundMrid, revision.endsWith(CLOSE_INC));
        }
        m = UPPER_INFINITE_RANGE.matcher(revision);
        if (m.matches()) {
            String lower = m.group(1);
            return isUpper(askedMrid, lower, foundMrid, revision.startsWith(OPEN_INC));
        }
        return false;
    }

    private boolean isLower(ModuleRevisionId askedMrid, String revision,
            ModuleRevisionId foundMrid, boolean inclusive) {
        ModuleRevisionId mRevId = ModuleRevisionId.newInstance(askedMrid, revision);
        int result = comparator.compare(mRevId, foundMrid);
        return result <= (inclusive ? 0 : -1);
    }

    private boolean isUpper(ModuleRevisionId askedMrid, String revision,
            ModuleRevisionId foundMrid, boolean inclusive) {
        ModuleRevisionId mRevId = ModuleRevisionId.newInstance(askedMrid, revision);
        int result = comparator.compare(mRevId, foundMrid);
        return result >= (inclusive ? 0 : 1);
    }

    public int compare(ModuleRevisionId askedMrid, ModuleRevisionId foundMrid,
            Comparator<ModuleRevisionId> staticComparator) {
        String revision = askedMrid.getRevision();
        Matcher m;
        m = UPPER_INFINITE_RANGE.matcher(revision);
        if (m.matches()) {
            // no upper limit, the dynamic revision can always be considered greater
            return 1;
        }
        String upper;
        m = FINITE_RANGE.matcher(revision);
        if (m.matches()) {
            upper = m.group(2);
        } else {
            m = LOWER_INFINITE_RANGE.matcher(revision);
            if (m.matches()) {
                upper = m.group(1);
            } else {
                throw new IllegalArgumentException(
                        "impossible to compare: askedMrid is not a dynamic revision: " + askedMrid);
            }
        }
        int c = staticComparator.compare(ModuleRevisionId.newInstance(askedMrid, upper), foundMrid);
        // if the comparison consider them equal, we must return -1, because we can't consider the
        // dynamic revision to be greater. Otherwise we can safely return the result of the static
        // comparison
        return c == 0 ? -1 : c;
    }

    public LatestStrategy getLatestStrategy() {
        if (latestStrategy == null) {
            if (getSettings() == null) {
                throw new IllegalStateException(
                        "no ivy instance nor latest strategy configured in version range matcher "
                                + this);
            }
            if (latestStrategyName == null) {
                throw new IllegalStateException(
                        "null latest strategy defined in version range matcher " + this);
            }
            latestStrategy = getSettings().getLatestStrategy(latestStrategyName);
            if (latestStrategy == null) {
                throw new IllegalStateException("unknown latest strategy '" + latestStrategyName
                        + "' configured in version range matcher " + this);
            }
        }
        return latestStrategy;
    }

    public void setLatestStrategy(LatestStrategy latestStrategy) {
        this.latestStrategy = latestStrategy;
    }

    public void setLatest(String latestStrategyName) {
        this.latestStrategyName = latestStrategyName;
    }

}
