/* This code is part of Freenet. It is distributed under the GNU General
 * Public License, version 2 (or at your option any later version). See
 * http://www.gnu.org/ for further details of the GPL. */
package freenet.node;

import java.util.Calendar;
import java.util.TimeZone;

import freenet.support.Fields;
import freenet.support.LogThresholdCallback;
import freenet.support.Logger;
import freenet.support.Logger.LogLevel;

/**
 * Central spot for stuff related to the versioning of the codebase.
 *
 * Note: when using the fields of this class, you must note that the Java
 * compiler compiles <b>constant final static</b> fields into the class
 * definitions of classes that use them. This might not be the most appropriate
 * behaviour when, eg. creating a class that reports statistics.
 *
 * A final static field can be made "non-constant" in the eyes of the compiler
 * by initialising it with the results of a method, eg <code>T identity(T o)
 * { T o; }</code>; however the "constant" behaviour might be required in some
 * cases. A more flexible solution is to add a method that returns the field,
 * eg {@link #publicVersion()}, and choose between the method and the field
 * as necessary.
 *
 * <pre>$ grep -R "\WVersion\.\w*[^(a-zA-Z]" src</pre>
 *
 * can be used to find the places in the source code whose logic needs to be
 * checked for this.
 *
 */
public class Version {

	/** FReenet Reference Daemon */
	public static final String nodeName = "Fred";

	/** The current tree version.
	 * FIXME: This is part of the node compatibility computations, so cannot be
	 * safely changed!!! Hence publicVersion ... */
	public static final String nodeVersion = "0.7";

	/** The version for publicity purposes i.e. the version of the node that has
	 * been released. */
	public static final String publicVersion = "0.7.5";

	/** The protocol version supported */
	public static final String protocolVersion = "1.0";

	/** The build number of the current revision */
	private static final int buildNumber = 1500;

	/** Oldest build of fred we will talk to *before* _cal */
	private static final int oldLastGoodBuild = 1474;
	/** Oldest build of fred we will talk to *after* _cal */
	private static final int newLastGoodBuild = 1475;
	static final long transitionTime;

	static {
		final Calendar _cal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
		// year, month - 1 (or constant), day, hour, minute, second
		_cal.set( 2016, Calendar.JULY, 15, 0, 0, 0 );
		transitionTime = _cal.getTimeInMillis();
	}

        private static volatile boolean logMINOR;
        private static volatile boolean logDEBUG;
	static {
		Logger.registerLogThresholdCallback(new LogThresholdCallback(){
			@Override
			public void shouldUpdate(){
				logMINOR = Logger.shouldLog(LogLevel.MINOR, this);
                                logDEBUG = Logger.shouldLog(LogLevel.DEBUG, this);
			}
		});
	}

	/**
	 * Use this method when you want the value returned to be the run-time
	 * version, not the build-time version.
	 *
	 * For example, if you compile your class, then modify this class and
	 * compile it (the build script does this automatically), but don't
	 * re-compile your original class, then that will still have the old
	 * version compiled into it, since it is a static constant.
	 *
	 * @return The build number (not SVN revision number) of this node.
	 */
	public static int buildNumber() {
		return buildNumber;
	}

	/**
	 * Analogous to {@link #buildNumber()} but for {@link #publicVersion}.
	 */
	public static String publicVersion() {
		return publicVersion;
	}

	/**
	 * Analogous to {@link #buildNumber()} but for {@link #transitionTime}.
	 */
	public static long transitionTime() {
		return transitionTime;
	}



	/**
	 * @return The lowest build number with which the node will connect and exchange
	 * data normally.
	 */
	public static int lastGoodBuild() {
		if(System.currentTimeMillis() >= transitionTime)
			return newLastGoodBuild;
		else
			return oldLastGoodBuild;
	}

	/** The highest reported build of fred */
	private static int highestSeenBuild = buildNumber;

	/** The current stable tree version */
	public static final String stableNodeVersion = "0.7";

	/** The stable protocol version supported */
	public static final String stableProtocolVersion = "STABLE-0.7";

	/** Oldest stable build of Fred we will talk to */
	public static final int lastGoodStableBuild = 1;

	/** Revision number of Version.java as read from CVS */
	public static final String cvsRevision = "@custom@";

	/**
	 * Analogous to {@link #buildNumber()} but for {@link #cvsRevision}.
	 */
	public static String cvsRevision() {
		return cvsRevision;
	}

	/**
	 * @return the node's version designators as an array
	 */
	public static String[] getVersion() {
		String[] ret =
			{ nodeName, nodeVersion, protocolVersion,  String.valueOf(buildNumber) };
		return ret;
	}

	public static String[] getLastGoodVersion() {
		String[] ret =
			{ nodeName, nodeVersion, protocolVersion,  String.valueOf(lastGoodBuild()) };
		return ret;
	}

	/**
	 * @return the version string that should be presented in the NodeReference
	 */
	public static String getVersionString() {
		return Fields.commaList(getVersion());
	}

	/**
	 * @return is needed for the freeviz
	 */
	public static String getLastGoodVersionString() {
		return Fields.commaList(getLastGoodVersion());
	}

	/**
	 * @return true if requests should be accepted from nodes brandishing this
	 *         protocol version string
	 */
	private static boolean goodProtocol(String prot) {
		if (prot.equals(protocolVersion)
// uncomment next line to accept stable, see also explainBadVersion() below
//			|| prot.equals(stableProtocolVersion)
			)
			return true;
		return false;
	}

	/**
	 * @return true if requests should be accepted from nodes brandishing this
	 *         version string
	 */
	public static boolean checkGoodVersion(
		String version) {
	    if(version == null) {
	        Logger.error(Version.class, "version == null!",
	                new Exception("error"));
	        return false;
	    }
		String[] v = Fields.commaList(version);

		if ((v.length < 3) || !goodProtocol(v[2])) {
			return false;
		}
		if (sameVersion(v)) {
			try {
				int build = Integer.parseInt(v[3]);
				int req = lastGoodBuild();
				if (build < req) {
					if(logDEBUG) Logger.debug(
						Version.class,
						"Not accepting unstable from version: "
							+ version
							+ "(lastGoodBuild="
							+ req
							+ ')');
					return false;
				}
			} catch (NumberFormatException e) {
				if(logMINOR)
					Logger.minor(Version.class,
							"Not accepting (" + e + ") from " + version);
				return false;
			}
		}
		if (stableVersion(v)) {
			try {
				int build = Integer.parseInt(v[3]);
				if(build < lastGoodStableBuild) {
					if(logDEBUG) Logger.debug(
						Version.class,
						"Not accepting stable from version"
							+ version
							+ "(lastGoodStableBuild="
							+ lastGoodStableBuild
							+ ')');
					return false;
				}
			} catch (NumberFormatException e) {
				Logger.minor(
					Version.class,
					"Not accepting (" + e + ") from " + version);
				return false;
			}
		}
		if(logDEBUG)
			Logger.minor(Version.class, "Accepting: " + version);
		return true;
	}

	/**
	 * @return true if requests should be accepted from nodes brandishing this
	 *         version string, given an arbitrary lastGoodVersion
	 */
	public static boolean checkArbitraryGoodVersion(
		String version, String lastGoodVersion) {
	    if(version == null) {
	        Logger.error(Version.class, "version == null!",
	                new Exception("error"));
	        return false;
	    }
	    if(lastGoodVersion == null) {
	        Logger.error(Version.class, "lastGoodVersion == null!",
	                new Exception("error"));
	        return false;
	    }
		String[] v = Fields.commaList(version);
		String[] lgv = Fields.commaList(lastGoodVersion);

		if ((v == null || v.length < 3) || !goodProtocol(v[2])) {
			return false;
		}
		if ((lgv == null || lgv.length < 3) || !goodProtocol(lgv[2])) {
			return false;
		}
		if (sameArbitraryVersion(v,lgv)) {
			try {
				int build = Integer.parseInt(v[3]);
				int min_build = Integer.parseInt(lgv[3]);
				if (build < min_build) {
					if(logDEBUG) Logger.debug(
						Version.class,
						"Not accepting unstable from version: "
							+ version
							+ "(lastGoodVersion="
							+ lastGoodVersion
							+ ')');
					return false;
				}
			} catch (NumberFormatException e) {
				if(logMINOR)
					Logger.minor(Version.class,
							"Not accepting (" + e + ") from " + version + " and/or " + lastGoodVersion);
				return false;
			}
		}
		if (stableVersion(v)) {
			try {
				int build = Integer.parseInt(v[3]);
				if(build < lastGoodStableBuild) {
					if(logDEBUG) Logger.debug(
						Version.class,
						"Not accepting stable from version"
							+ version
							+ "(lastGoodStableBuild="
							+ lastGoodStableBuild
							+ ')');
					return false;
				}
			} catch (NumberFormatException e) {
				Logger.minor(
					Version.class,
					"Not accepting (" + e + ") from " + version);
				return false;
			}
		}
		if(logDEBUG)
			Logger.minor(Version.class, "Accepting: " + version);
		return true;
	}

	/**
	 * @return string explaining why a version string is rejected
	 */
	public static String explainBadVersion(String version) {
		String[] v = Fields.commaList(version);

		if ((v.length < 3) || !goodProtocol(v[2])) {
			return "Required protocol version is "
						+ protocolVersion
// uncomment next line if accepting stable, see also goodProtocol() above
//						+ " or " + stableProtocolVersion
						;
		}
		if (sameVersion(v)) {
			try {
				int build = Integer.parseInt(v[3]);
				int req = lastGoodBuild();
				if (build < req)
					return "Build older than last good build " + req;
			} catch (NumberFormatException e) {
				return "Build number not numeric.";
			}
		}
		if (stableVersion(v)) {
			try {
				int build = Integer.parseInt(v[3]);
				if (build < lastGoodStableBuild)
					return "Build older than last good stable build " + lastGoodStableBuild;
			} catch (NumberFormatException e) {
				return "Build number not numeric.";
			}
		}
		return null;
	}

	/**
	 * @return the build number of an arbitrary version string
	 */
	public static int getArbitraryBuildNumber(
		String version ) throws VersionParseException {
	    if(version == null) {
	        Logger.error(Version.class, "version == null!",
	                new Exception("error"));
	        throw new VersionParseException("version == null");
	    }
		String[] v = Fields.commaList(version);

		if ((v.length < 3) || !goodProtocol(v[2])) {
			throw new VersionParseException("not long enough or bad protocol: "+version);
		}
		try {
			return Integer.parseInt(v[3]);
		} catch (NumberFormatException e) {
			throw (VersionParseException)new VersionParseException("Got NumberFormatException on "+v[3]+" : "+e+" for "+version).initCause(e);
		}
	}

	public static int getArbitraryBuildNumber(
			String version, int defaultValue ) {
		try {
			return getArbitraryBuildNumber(version);
		} catch (VersionParseException e) {
			return defaultValue;
		}
	}
	/**
	 * Update static variable highestSeenBuild anytime we encounter
	 * a new node with a higher version than we've seen before
	 */
	public static void seenVersion(String version) {
		String[] v = Fields.commaList(version);

		if ((v == null) || (v.length < 3))
			return; // bad, but that will be discovered elsewhere

		if (sameVersion(v)) {

			int buildNo;
			try {
				buildNo = Integer.parseInt(v[3]);
			} catch (NumberFormatException e) {
				return;
			}
			if (buildNo > highestSeenBuild) {
				if (logMINOR) {
					Logger.minor(
						Version.class,
						"New highest seen build: " + buildNo);
				}
				highestSeenBuild = buildNo;
			}
		}
	}
	
	public static int getHighestSeenBuild() {
		return highestSeenBuild;
	}

	/**
	 * @return true if the string describes the same node version as ours.
	 * Note that the build number may be different, and is ignored.
	 */
	public static boolean sameVersion(String[] v) {
		return v[0].equals(nodeName)
			&& v[1].equals(nodeVersion)
			&& (v.length >= 4);
	}

	/**
	 * @return true if the string describes the same node version as an arbitrary one.
	 * Note that the build number may be different, and is ignored.
	 */
	public static boolean sameArbitraryVersion(String[] v, String[] lgv) {
		return v[0].equals(lgv[0])
			&& v[1].equals(lgv[1])
			&& (v.length >= 4)
			&& (lgv.length >= 4);
	}

	/**
	 * @return true if the string describes a stable node version
	 */
	private static boolean stableVersion(String[] v) {
		return v[0].equals(nodeName)
			&& v[1].equals(stableNodeVersion)
			&& (v.length >= 4);
	}

	public static void main(String[] args) throws Throwable {
		System.out.println(
			"Freenet: "
				+ nodeName
				+ ' '
                    + nodeVersion
				+ " (protocol "
				+ protocolVersion
				+ ") build "
				+ buildNumber
				+ " (last good build: "
				+ lastGoodBuild()
				+ ") "
				+ cvsRevision);
	}
}
