/*
 * 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 freemarker.template;

import java.io.Serializable;
import java.util.Date;

import freemarker.template.utility.StringUtil;

/**
 * Represents a version number plus the further qualifiers and build info. This is
 * mostly used for representing a FreeMarker version number, but should also be able
 * to parse the version strings of 3rd party libraries.
 * 
 * @see Configuration#getVersion()
 * 
 * @since 2.3.20
 */
public final class Version implements Serializable {
    
    private final int major;
    private final int minor;
    private final int micro;
    private final String extraInfo;
    private final String originalStringValue;
    
    private final Boolean gaeCompliant;
    private final Date buildDate;
    
    private final int intValue;
    private volatile String calculatedStringValue;  // not final because it's calculated on demand
    private int hashCode;  // not final because it's calculated on demand

    /**
     * @throws IllegalArgumentException if the version string is malformed
     */
    public Version(String stringValue) {
        this(stringValue, null, null);
    }
    
    /**
     * @throws IllegalArgumentException if the version string is malformed
     */
    public Version(String stringValue, Boolean gaeCompliant, Date buildDate) {
        stringValue = stringValue.trim();
        originalStringValue = stringValue; 
        
        int[] parts = new int[3];
        String extraInfoTmp = null;
        {
            int partIdx = 0;
            for (int i = 0; i < stringValue.length(); i++) {
                char c = stringValue.charAt(i);
                if (isNumber(c)) {
                    parts[partIdx] = parts[partIdx] * 10 + (c - '0');
                } else {
                    if (i == 0) {
                        throw new IllegalArgumentException(
                                "The version number string " + StringUtil.jQuote(stringValue)
                                + " doesn't start with a number.");
                    }
                    if (c == '.') {
                        char nextC = i + 1 >= stringValue.length() ? 0 : stringValue.charAt(i + 1);
                        if (nextC == '.') {
                            throw new IllegalArgumentException(
                                    "The version number string " + StringUtil.jQuote(stringValue)
                                    + " contains multiple dots after a number.");
                        }
                        if (partIdx == 2 || !isNumber(nextC)) {
                            extraInfoTmp = stringValue.substring(i);
                            break;
                        } else {
                            partIdx++;
                        }
                    } else {
                        extraInfoTmp = stringValue.substring(i);
                        break;
                    }
                }
            }
            
            if (extraInfoTmp != null) {
                char firstChar = extraInfoTmp.charAt(0); 
                if (firstChar == '.' || firstChar == '-' || firstChar == '_') {
                    extraInfoTmp = extraInfoTmp.substring(1);
                    if (extraInfoTmp.length() == 0) {
                        throw new IllegalArgumentException(
                            "The version number string " + StringUtil.jQuote(stringValue)
                            + " has an extra info section opened with \"" + firstChar + "\", but it's empty.");
                    }
                }
            }
        }
        extraInfo = extraInfoTmp;
        
        major = parts[0];
        minor = parts[1];
        micro = parts[2];
        intValue = calculateIntValue();
        
        this.gaeCompliant = gaeCompliant;
        this.buildDate = buildDate;
        
    }

    private boolean isNumber(char c) {
        return c >= '0' && c <= '9';
    }

    public Version(int major, int minor, int micro) {
        this(major, minor, micro, null, null, null);
    }

    /**
     * Creates an object based on the {@code int} value that uses the same kind of encoding as {@link #intValue()}.
     * 
     * @since 2.3.24
     */
    public Version(int intValue) {
        this.intValue = intValue;
        
        this.micro = intValue % 1000;
        this.minor = (intValue / 1000) % 1000;
        this.major = intValue / 1000000;
        
        this.extraInfo = null;
        this.gaeCompliant = null;
        this.buildDate = null;
        originalStringValue = null;
    }
    
    public Version(int major, int minor, int micro, String extraInfo, Boolean gaeCompatible, Date buildDate) {
        this.major = major;
        this.minor = minor;
        this.micro = micro;
        this.extraInfo = extraInfo;
        this.gaeCompliant = gaeCompatible;
        this.buildDate = buildDate;
        intValue = calculateIntValue();
        originalStringValue = null;
    }

    private int calculateIntValue() {
        return intValueFor(major, minor, micro);
    }
    
    static public int intValueFor(int major, int minor, int micro) {
        return major * 1000000 + minor * 1000 + micro;
    }
    
    private String getStringValue() {
        if (originalStringValue != null) return originalStringValue;
        
        String calculatedStringValue = this.calculatedStringValue;
        if (calculatedStringValue == null) {
            synchronized (this) {
                calculatedStringValue = this.calculatedStringValue;
                if (calculatedStringValue == null) {
                    calculatedStringValue = major + "." + minor + "." + micro;
                    if (extraInfo != null) calculatedStringValue += "-" + extraInfo;
                    this.calculatedStringValue = calculatedStringValue;
                }
            }
        }
        return calculatedStringValue;
    }
    
    /**
     * Contains the major.minor.micor numbers and the extraInfo part, not the other information.
     */
    @Override
    public String toString() {
        return getStringValue();
    }

    /**
     * The 1st version number, like 1 in "1.2.3".
     */
    public int getMajor() {
        return major;
    }

    /**
     * The 2nd version number, like 2 in "1.2.3".
     */
    public int getMinor() {
        return minor;
    }

    /**
     * The 3rd version number, like 3 in "1.2.3".
     */
    public int getMicro() {
        return micro;
    }

    /**
     * The arbitrary string after the micro version number without leading dot, dash or underscore,
     * like "RC03" in "2.4.0-RC03".
     * This is usually a qualifier (RC, SNAPHOST, nightly, beta, etc) and sometimes build info (like
     * date).
     */
    public String getExtraInfo() {
        return extraInfo;
    }
    
    /**
     * @return The Google App Engine compliance, or {@code null}.
     */
    public Boolean isGAECompliant() {
        return gaeCompliant;
    }

    /**
     * @return The build date if known, or {@code null}.
     */
    public Date getBuildDate() {
        return buildDate;
    }

    /**
     * @return major * 1000000 + minor * 1000 + micro.
     */
    public int intValue() {
        return intValue;
    }

    @Override
    public int hashCode() {
        int r = hashCode;
        if (r != 0) return r;
        synchronized (this) {
            if (hashCode == 0) {
                final int prime = 31;
                int result = 1;
                result = prime * result + (buildDate == null ? 0 : buildDate.hashCode());
                result = prime * result + (extraInfo == null ? 0 : extraInfo.hashCode());
                result = prime * result + (gaeCompliant == null ? 0 : gaeCompliant.hashCode());
                result = prime * result + intValue;
                if (result == 0) result = -1;  // 0 is reserved for "not set"
                hashCode = result;
            }
            return hashCode;
        }
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (obj == null) return false;
        if (getClass() != obj.getClass()) return false;

        Version other = (Version) obj;

        if (intValue != other.intValue) return false;
        
        if (other.hashCode() != hashCode()) return false;
        
        if (buildDate == null) {
            if (other.buildDate != null) return false;
        } else if (!buildDate.equals(other.buildDate)) {
            return false;
        }
        
        if (extraInfo == null) {
            if (other.extraInfo != null) return false;
        } else if (!extraInfo.equals(other.extraInfo)) {
            return false;
        }
        
        if (gaeCompliant == null) {
            if (other.gaeCompliant != null) return false;
        } else if (!gaeCompliant.equals(other.gaeCompliant)) {
            return false;
        }
        
        return true;
    }
    
}
