/*
 * 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.maven.shared.release.versions;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.maven.artifact.Artifact;
import org.apache.maven.artifact.ArtifactUtils;
import org.apache.maven.artifact.versioning.DefaultArtifactVersion;
import org.codehaus.plexus.util.StringUtils;

/**
 * This compares and increments versions for a common java versioning scheme.
 * <p>
 * The supported version scheme has the following parts.<br>
 * <code><i>component-digits-annotation-annotationRevision-buildSpecifier</i></code><br>
 * Example:<br>
 * <code>my-component-1.0.1-alpha-2-SNAPSHOT</code>
 * <b>Terms:</b>
 * <ul>
 * <li><i>component</i> - name of the versioned component (log4j, commons-lang, etc)
 * <li><i>digits</i> - Numeric digits with at least one "." period. (1.0, 1.1, 1.01, 1.2.3, etc)
 * <li><i>annotationRevision</i> - Integer qualifier for the annotation. (4 as in RC-4)
 * <li><i>buildSpecifier</i> - Additional specifier for build. (SNAPSHOT, or build number like "20041114.081234-2")
 * </ul>
 * <b>Digits is the only required piece of the version string, and must contain at lease one "." period.</b>
 * Implementation details:<br>
 * The separators "_" and "-" between components are also optional (though they are usually recommended).<br>
 * Example:<br>
 * <code>log4j-1.2.9-beta-9-SNAPSHOT == log4j1.2.9beta9SNAPSHOT == log4j_1.2.9_beta_9_SNAPSHOT</code>
 * Leading zeros are significant when performing comparisons.
 * TODO: this parser is better than DefaultArtifactVersion - replace it with this (but align naming) and then remove
 * this from here.
 */
public class DefaultVersionInfo implements VersionInfo {
    private final String strVersion;

    private final List<String> digits;

    private String annotation;

    private String annotationRevision;

    private final String buildSpecifier;

    private String annotationSeparator;

    private String annotationRevSeparator;

    private final String buildSeparator;

    private static final int DIGITS_INDEX = 1;

    private static final int ANNOTATION_SEPARATOR_INDEX = 2;

    private static final int ANNOTATION_INDEX = 3;

    private static final int ANNOTATION_REV_SEPARATOR_INDEX = 4;

    private static final int ANNOTATION_REVISION_INDEX = 5;

    private static final int BUILD_SEPARATOR_INDEX = 6;

    private static final int BUILD_SPECIFIER_INDEX = 7;

    private static final String SNAPSHOT_IDENTIFIER = "SNAPSHOT";

    private static final String DIGIT_SEPARATOR_STRING = ".";

    /** Constant <code>STANDARD_PATTERN</code> */
    public static final Pattern STANDARD_PATTERN = Pattern.compile(
            "^((?:\\d+\\.)*\\d+)" // digit(s) and '.' repeated - followed by digit (version digits 1.22.0, etc)
                    + "([-_])?" // optional - or _  (annotation separator)
                    + "([a-zA-Z]*)" // alpha characters (looking for annotation - alpha, beta, RC, etc.)
                    + "([-_])?" // optional - or _  (annotation revision separator)
                    + "(\\d*)" // digits  (any digits after rc or beta is an annotation revision)
                    + "(?:([-_])?(.*?))?$"); // - or _ followed everything else (build specifier)

    /* *
     * cmaki 02242009
     * FIX for non-digit release numbers, e.g. trunk-SNAPSHOT or just SNAPSHOT
     * This alternate pattern supports version numbers like:
     * trunk-SNAPSHOT
     * branchName-SNAPSHOT
     * SNAPSHOT
     */
    // for SNAPSHOT releases only (possible versions include: trunk-SNAPSHOT or SNAPSHOT)
    /** Constant <code>ALTERNATE_PATTERN</code> */
    public static final Pattern ALTERNATE_PATTERN = Pattern.compile("^(SNAPSHOT|[a-zA-Z]+[_-]SNAPSHOT)");

    /**
     * Constructs this object and parses the supplied version string.
     *
     * @param version the version string
     * @throws org.apache.maven.shared.release.versions.VersionParseException if an exception during parsing the input
     */
    public DefaultVersionInfo(String version) throws VersionParseException {
        strVersion = version;

        // FIX for non-digit release numbers, e.g. trunk-SNAPSHOT or just SNAPSHOT
        Matcher matcher = ALTERNATE_PATTERN.matcher(strVersion);
        // TODO: hack because it didn't support "SNAPSHOT"
        if (matcher.matches()) {
            annotation = null;
            digits = null;
            buildSpecifier = version;
            buildSeparator = null;
            return;
        }

        Matcher m = STANDARD_PATTERN.matcher(strVersion);
        if (m.matches()) {
            digits = parseDigits(m.group(DIGITS_INDEX));
            if (!SNAPSHOT_IDENTIFIER.equals(m.group(ANNOTATION_INDEX))) {
                annotationSeparator = m.group(ANNOTATION_SEPARATOR_INDEX);
                annotation = nullIfEmpty(m.group(ANNOTATION_INDEX));

                if (StringUtils.isNotEmpty(m.group(ANNOTATION_REV_SEPARATOR_INDEX))
                        && StringUtils.isEmpty(m.group(ANNOTATION_REVISION_INDEX))) {
                    // The build separator was picked up as the annotation revision separator
                    buildSeparator = m.group(ANNOTATION_REV_SEPARATOR_INDEX);
                    buildSpecifier = nullIfEmpty(m.group(BUILD_SPECIFIER_INDEX));
                } else {
                    annotationRevSeparator = m.group(ANNOTATION_REV_SEPARATOR_INDEX);
                    annotationRevision = nullIfEmpty(m.group(ANNOTATION_REVISION_INDEX));

                    buildSeparator = m.group(BUILD_SEPARATOR_INDEX);
                    buildSpecifier = nullIfEmpty(m.group(BUILD_SPECIFIER_INDEX));
                }
            } else {
                // Annotation was "SNAPSHOT" so populate the build specifier with that data
                buildSeparator = m.group(ANNOTATION_SEPARATOR_INDEX);
                buildSpecifier = nullIfEmpty(m.group(ANNOTATION_INDEX));
            }
        } else {
            throw new VersionParseException("Unable to parse the version string: \"" + version + "\"");
        }
    }

    /**
     * <p>Constructor for DefaultVersionInfo.</p>
     *
     * @param digits a {@link java.util.List} object
     * @param annotation a {@link java.lang.String} object
     * @param annotationRevision a {@link java.lang.String} object
     * @param buildSpecifier a {@link java.lang.String} object
     * @param annotationSeparator a {@link java.lang.String} object
     * @param annotationRevSeparator a {@link java.lang.String} object
     * @param buildSeparator a {@link java.lang.String} object
     */
    public DefaultVersionInfo(
            List<String> digits,
            String annotation,
            String annotationRevision,
            String buildSpecifier,
            String annotationSeparator,
            String annotationRevSeparator,
            String buildSeparator) {
        this.digits = digits;
        this.annotation = annotation;
        this.annotationRevision = annotationRevision;
        this.buildSpecifier = buildSpecifier;
        this.annotationSeparator = annotationSeparator;
        this.annotationRevSeparator = annotationRevSeparator;
        this.buildSeparator = buildSeparator;
        this.strVersion = getVersionString(this, buildSpecifier, buildSeparator);
    }

    @Override
    public boolean isSnapshot() {
        return ArtifactUtils.isSnapshot(strVersion);
    }

    @Override
    public VersionInfo getNextVersion() {
        DefaultVersionInfo version = null;
        if (digits != null) {
            List<String> digits = new ArrayList<>(this.digits);
            String annotationRevision = this.annotationRevision;
            if (StringUtils.isNumeric(annotationRevision)) {
                annotationRevision = incrementVersionString(annotationRevision);
            } else {
                digits.set(digits.size() - 1, incrementVersionString(digits.get(digits.size() - 1)));
            }

            version = new DefaultVersionInfo(
                    digits,
                    annotation,
                    annotationRevision,
                    buildSpecifier,
                    annotationSeparator,
                    annotationRevSeparator,
                    buildSeparator);
        }
        return version;
    }

    /**
     * {@inheritDoc}
     *
     * Compares this {@link DefaultVersionInfo} to the supplied {@link DefaultVersionInfo} to determine which version is
     * greater.
     */
    @Override
    public int compareTo(VersionInfo obj) {
        DefaultVersionInfo that = (DefaultVersionInfo) obj;

        int result;
        // TODO: this is a workaround for a bug in DefaultArtifactVersion - fix there - 1.01 < 1.01.01
        if (strVersion.startsWith(that.strVersion)
                && !strVersion.equals(that.strVersion)
                && strVersion.charAt(that.strVersion.length()) != '-') {
            result = 1;
        } else if (that.strVersion.startsWith(strVersion)
                && !strVersion.equals(that.strVersion)
                && that.strVersion.charAt(strVersion.length()) != '-') {
            result = -1;
        } else {
            // TODO: this is a workaround for a bug in DefaultArtifactVersion - fix there - it should not consider case
            // in comparing the qualifier
            // NOTE: The combination of upper-casing and lower-casing is an approximation of String.equalsIgnoreCase()
            String thisVersion = strVersion.toUpperCase(Locale.ENGLISH).toLowerCase(Locale.ENGLISH);
            String thatVersion = that.strVersion.toUpperCase(Locale.ENGLISH).toLowerCase(Locale.ENGLISH);

            result = new DefaultArtifactVersion(thisVersion).compareTo(new DefaultArtifactVersion(thatVersion));
        }
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (!(obj instanceof DefaultVersionInfo)) {
            return false;
        }

        return compareTo((VersionInfo) obj) == 0;
    }

    @Override
    public int hashCode() {
        return strVersion.toLowerCase(Locale.ENGLISH).hashCode();
    }

    /**
     * Takes a string and increments it as an integer.
     * Preserves any lpad of "0" zeros.
     *
     * @param s the version number
     * @return {@code String} increments the input {@code String} as an integer
     * and Preserves any lpad of "0" zeros.
     */
    protected String incrementVersionString(String s) {
        int n = Integer.valueOf(s).intValue() + 1;
        String value = String.valueOf(n);
        if (value.length() < s.length()) {
            // String was left-padded with zeros
            value = StringUtils.leftPad(value, s.length(), "0");
        }
        return value;
    }

    @Override
    public String getSnapshotVersionString() {
        if (strVersion.equals(Artifact.SNAPSHOT_VERSION)) {
            return strVersion;
        }

        String baseVersion = getReleaseVersionString();

        if (baseVersion.length() > 0) {
            baseVersion += "-";
        }

        return baseVersion + Artifact.SNAPSHOT_VERSION;
    }

    @Override
    public String getReleaseVersionString() {
        String baseVersion = strVersion;

        Matcher m = Artifact.VERSION_FILE_PATTERN.matcher(baseVersion);
        if (m.matches()) {
            baseVersion = m.group(1);
        }
        // MRELEASE-623 SNAPSHOT is case-insensitive
        else if (StringUtils.right(baseVersion, 9).equalsIgnoreCase("-" + Artifact.SNAPSHOT_VERSION)) {
            baseVersion = baseVersion.substring(0, baseVersion.length() - Artifact.SNAPSHOT_VERSION.length() - 1);
        } else if (baseVersion.equals(Artifact.SNAPSHOT_VERSION)) {
            baseVersion = "1.0";
        }
        return baseVersion;
    }

    @Override
    public String toString() {
        return strVersion;
    }

    /**
     * <p>getVersionString.</p>
     *
     * @param info a {@link org.apache.maven.shared.release.versions.DefaultVersionInfo} object
     * @param buildSpecifier a {@link java.lang.String} object
     * @param buildSeparator a {@link java.lang.String} object
     * @return a {@link java.lang.String} object
     */
    protected static String getVersionString(DefaultVersionInfo info, String buildSpecifier, String buildSeparator) {
        StringBuilder sb = new StringBuilder();

        if (info.digits != null) {
            sb.append(joinDigitString(info.digits));
        }

        if (StringUtils.isNotEmpty(info.annotation)) {
            sb.append(StringUtils.defaultString(info.annotationSeparator));
            sb.append(info.annotation);
        }

        if (StringUtils.isNotEmpty(info.annotationRevision)) {
            if (StringUtils.isEmpty(info.annotation)) {
                sb.append(StringUtils.defaultString(info.annotationSeparator));
            } else {
                sb.append(StringUtils.defaultString(info.annotationRevSeparator));
            }
            sb.append(info.annotationRevision);
        }

        if (StringUtils.isNotEmpty(buildSpecifier)) {
            sb.append(StringUtils.defaultString(buildSeparator));
            sb.append(buildSpecifier);
        }

        return sb.toString();
    }

    /**
     * Simply joins the items in the list with "." period
     *
     * @return a single {@code String} of the items in the passed list, joined with a "."
     * @param digits {@code List<String>} of digits
     */
    protected static String joinDigitString(List<String> digits) {
        return digits != null ? StringUtils.join(digits.iterator(), DIGIT_SEPARATOR_STRING) : null;
    }

    /**
     * Splits the string on "." and returns a list
     * containing each digit.
     *
     * @param strDigits
     */
    private List<String> parseDigits(String strDigits) {
        return Arrays.asList(StringUtils.split(strDigits, DIGIT_SEPARATOR_STRING));
    }

    // --------------------------------------------------
    // Getters & Setters
    // --------------------------------------------------

    private static String nullIfEmpty(String s) {
        return StringUtils.isEmpty(s) ? null : s;
    }

    /**
     * <p>Getter for the field <code>digits</code>.</p>
     *
     * @return a {@link java.util.List} object
     */
    public List<String> getDigits() {
        return digits;
    }

    /**
     * <p>Getter for the field <code>annotation</code>.</p>
     *
     * @return a {@link java.lang.String} object
     */
    public String getAnnotation() {
        return annotation;
    }

    /**
     * <p>Getter for the field <code>annotationRevision</code>.</p>
     *
     * @return a {@link java.lang.String} object
     */
    public String getAnnotationRevision() {
        return annotationRevision;
    }

    /**
     * <p>Getter for the field <code>buildSpecifier</code>.</p>
     *
     * @return a {@link java.lang.String} object
     */
    public String getBuildSpecifier() {
        return buildSpecifier;
    }
}
