/*
 * 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 org.gradle.api.GradleException;
import org.gradle.internal.UncheckedException;

import java.io.InputStream;
import java.net.URL;
import java.net.URLConnection;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Properties;
import java.util.TimeZone;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static java.lang.String.format;
import static org.gradle.internal.IoActions.uncheckedClose;

public class GradleVersion implements Comparable<GradleVersion> {
    public static final String URL = "http://www.gradle.org";
    private static final Pattern VERSION_PATTERN = Pattern.compile("((\\d+)(\\.\\d+)+)(-(\\p{Alpha}+)-(\\d+[a-z]?))?(-(SNAPSHOT|\\d{14}([-+]\\d{4})?))?");
    private static final int STAGE_MILESTONE = 0;

    private final String version;
    private final int majorPart;
    private final String buildTime;
    private final String commitId;
    private final Long snapshot;
    private final String versionPart;
    private final Stage stage;
    private static final GradleVersion CURRENT;

    public static final String RESOURCE_NAME = "/org/gradle/build-receipt.properties";
    public static final String VERSION_OVERRIDE_VAR = "GRADLE_VERSION_OVERRIDE";
    public static final String VERSION_NUMBER_PROPERTY = "versionNumber";

    static {
        URL resource = GradleVersion.class.getResource(RESOURCE_NAME);
        if (resource == null) {
            throw new GradleException(format("Resource '%s' not found.", RESOURCE_NAME));
        }

        InputStream inputStream = null;
        try {
            URLConnection connection = resource.openConnection();
            connection.setUseCaches(false);
            inputStream = connection.getInputStream();
            Properties properties = new Properties();
            properties.load(inputStream);

            String version = properties.get(VERSION_NUMBER_PROPERTY).toString();

            // We allow the gradle version to be overridden for tests that are sensitive
            // to the version and need to test with various different version patterns.
            // We use an env variable because these are easy to set on daemon startup,
            // whereas system properties are scrubbed at daemon startup.
            String overrideVersion = System.getenv(VERSION_OVERRIDE_VAR);
            if (overrideVersion != null) {
                version = overrideVersion;
            }

            String buildTimestamp = properties.get("buildTimestampIso").toString();
            String commitId = properties.get("commitId").toString();

            CURRENT = new GradleVersion(version, "unknown".equals(buildTimestamp) ? null : buildTimestamp, commitId);
        } catch (Exception e) {
            throw new GradleException(format("Could not load version details from resource '%s'.", resource), e);
        } finally {
            if (inputStream != null) {
                uncheckedClose(inputStream);
            }
        }
    }

    public static GradleVersion current() {
        return CURRENT;
    }

    /**
     * Parses the given string into a GradleVersion.
     *
     * @throws IllegalArgumentException On unrecognized version string.
     */
    public static GradleVersion version(String version) throws IllegalArgumentException {
        return new GradleVersion(version, null, null);
    }

    private GradleVersion(String version, String buildTime, String commitId) {
        this.version = version;
        this.commitId = commitId;
        this.buildTime = buildTime;
        Matcher matcher = VERSION_PATTERN.matcher(version);
        if (!matcher.matches()) {
            throw new IllegalArgumentException(format("'%s' is not a valid Gradle version string (examples: '1.0', '1.0-rc-1')", version));
        }

        versionPart = matcher.group(1);
        majorPart = Integer.parseInt(matcher.group(2), 10);

        if (matcher.group(4) != null) {
            int stageNumber;
            if (matcher.group(5).equals("milestone")) {
                stageNumber = STAGE_MILESTONE;
            } else if (matcher.group(5).equals("preview")) {
                stageNumber = 2;
            } else if (matcher.group(5).equals("rc")) {
                stageNumber = 3;
            } else {
                stageNumber = 1;
            }
            String stageString = matcher.group(6);
            stage = new Stage(stageNumber, stageString);
        } else {
            stage = null;
        }

        if ("snapshot".equals(matcher.group(5))) {
            snapshot = 0L;
        } else if (matcher.group(8) == null) {
            snapshot = null;
        } else if ("SNAPSHOT".equals(matcher.group(8))) {
            snapshot = 0L;
        } else {
            try {
                if (matcher.group(9) != null) {
                    snapshot = new SimpleDateFormat("yyyyMMddHHmmssZ").parse(matcher.group(8)).getTime();
                } else {
                    SimpleDateFormat format = new SimpleDateFormat("yyyyMMddHHmmss");
                    format.setTimeZone(TimeZone.getTimeZone("UTC"));
                    snapshot = format.parse(matcher.group(8)).getTime();
                }
            } catch (ParseException e) {
                throw UncheckedException.throwAsUncheckedException(e);
            }
        }
    }

    @Override
    public String toString() {
        return "Gradle " + version;
    }

    public String getVersion() {
        return version;
    }

    public String getBuildTime() {
        return buildTime;
    }

    public String getRevision() {
        return commitId;
    }

    public boolean isSnapshot() {
        return snapshot != null;
    }

    /**
     * The base version of this version. For pre-release versions, this is the target version.
     *
     * For example, the version base of '1.2-rc-1' is '1.2'.
     *
     * @return The version base
     */
    public GradleVersion getBaseVersion() {
        if (stage == null && snapshot == null) {
            return this;
        }
        return version(versionPart);
    }

    public GradleVersion getNextMajor() {
        if (stage != null && stage.stage == STAGE_MILESTONE) {
            return version(majorPart + ".0");
        }
        return version((majorPart + 1) + ".0");
    }

    public int compareTo(GradleVersion gradleVersion) {
        String[] majorVersionParts = versionPart.split("\\.");
        String[] otherMajorVersionParts = gradleVersion.versionPart.split("\\.");

        for (int i = 0; i < majorVersionParts.length && i < otherMajorVersionParts.length; i++) {
            int part = Integer.parseInt(majorVersionParts[i]);
            int otherPart = Integer.parseInt(otherMajorVersionParts[i]);

            if (part > otherPart) {
                return 1;
            }
            if (otherPart > part) {
                return -1;
            }
        }
        if (majorVersionParts.length > otherMajorVersionParts.length) {
            return 1;
        }
        if (majorVersionParts.length < otherMajorVersionParts.length) {
            return -1;
        }

        if (stage != null && gradleVersion.stage != null) {
            int diff = stage.compareTo(gradleVersion.stage);
            if (diff != 0) {
                return diff;
            }
        }
        if (stage == null && gradleVersion.stage != null) {
            return 1;
        }
        if (stage != null && gradleVersion.stage == null) {
            return -1;
        }

        if (snapshot != null && gradleVersion.snapshot != null) {
            return snapshot.compareTo(gradleVersion.snapshot);
        }
        if (snapshot == null && gradleVersion.snapshot != null) {
            return 1;
        }
        if (snapshot != null && gradleVersion.snapshot == null) {
            return -1;
        }

        return 0;
    }

    @Override
    public boolean equals(Object o) {
        if (o == this) {
            return true;
        }
        if (o == null || o.getClass() != getClass()) {
            return false;
        }
        GradleVersion other = (GradleVersion) o;
        return version.equals(other.version);
    }

    @Override
    public int hashCode() {
        return version.hashCode();
    }

    public boolean isValid() {
        return versionPart != null;
    }

    static final class Stage implements Comparable<Stage> {
        final int stage;
        final int number;
        final Character patchNo;

        Stage(int stage, String number) {
            this.stage = stage;
            Matcher m = Pattern.compile("(\\d+)([a-z])?").matcher(number);
            try {
                m.matches();
                this.number = Integer.parseInt(m.group(1));
            } catch (Exception e) {
                throw new RuntimeException("Invalid stage small number: " + number, e);
            }

            if (m.groupCount() == 2 && m.group(2) != null) {
                this.patchNo = m.group(2).charAt(0);
            } else {
                this.patchNo = '_';
            }
        }

        public int compareTo(Stage other) {
            if (stage > other.stage) {
                return 1;
            }
            if (stage < other.stage) {
                return -1;
            }
            if (number > other.number) {
                return 1;
            }
            if (number < other.number) {
                return -1;
            }
            if (patchNo > other.patchNo) {
                return 1;
            }
            if (patchNo < other.patchNo) {
                return -1;
            }
            return 0;
        }
    }
}
