/* 
 * PackageCmd.java --
 *
 *	This class implements the built-in "package" command in Tcl.
 *
 * Copyright (c) 1997 by Sun Microsystems, Inc.
 *
 * See the file "license.terms" for information on usage and redistribution
 * of this file, and for a DISCLAIMER OF ALL WARRANTIES.
 *
 * RCS: @(#) $Id: PackageCmd.java,v 1.1.1.1 1998/10/14 21:09:18 cvsadmin Exp $
 */

package tcl.lang;

import java.util.*;

class PackageCmd implements Command {

private static final String[] validCmds = {
    "forget",
    "ifneeded",
    "names",
    "provide",
    "require",
    "unknown",
    "vcompare",
    "versions",
    "vsatisfies"
};
  
static final private int OPT_FORGET	= 0;
static final private int OPT_IFNEEDED	= 1;
static final private int OPT_NAMES	= 2;
static final private int OPT_PROVIDE	= 3;
static final private int OPT_REQUIRE	= 4;
static final private int OPT_UNKNOWN	= 5;
static final private int OPT_VCOMPARE	= 6;
static final private int OPT_VERSIONS	= 7;
static final private int OPT_VSATISFIES	= 8;

/*
 *----------------------------------------------------------------------
 *
 * pkgProvide --
 *
 *	This procedure is invoked to declare that a particular version
 *	of a particular package is now present in an interpreter.  There
 *	must not be any other version of this package already
 *	provided in the interpreter.
 *
 * Results:
 *	Normally does nothing; if there is already another version
 *	of the package loaded then an error is raised.
 *
 * Side effects:
 *	The interpreter remembers that this package is available,
 *	so that no other version of the package may be provided for
 *	the interpreter.
 *
 *----------------------------------------------------------------------
 */

static void 
pkgProvide(
    Interp interp,		// Interpreter in which package is now
				// available. 
    String pkgName,		// Name of package. 
    String version)		// Version string for package.
throws 
    TclException
{
    Package pkg;

    /* 
     * Validate the version string that was passed in.
     */

    checkVersion(interp, version);
    pkg = findPackage(interp, pkgName);
    if (pkg.version == null) {
	pkg.version = version;
	return;
    }
    if (compareVersions(pkg.version, version, null) != 0) {
	throw new TclException(interp,
		"conflicting versions provided for package \"" + pkgName
		+ "\": " + pkg.version + ", then " + version);
    }
}

/*
 *----------------------------------------------------------------------
 *
 * pkgRequire --
 *
 *	This procedure is called by code that depends on a particular
 *	version of a particular package.  If the package is not already
 *	provided in the interpreter, this procedure invokes a Tcl script
 *	to provide it.  If the package is already provided, this
 *	procedure makes sure that the caller's needs don't conflict with
 *	the version that is present.
 *
 * Results:
 *	If successful, returns the version string for the currently
 *	provided version of the package, which may be different from
 *	the "version" argument.  If the caller's requirements
 *	cannot be met (e.g. the version requested conflicts with
 *	a currently provided version, or the required version cannot
 *	be found, or the script to provide the required version
 *	generates an error), a TclException is raised.
 *
 * Side effects:
 *	The script from some previous "package ifneeded" command may
 *	be invoked to provide the package.
 *
 *----------------------------------------------------------------------
 */

static String 
pkgRequire(
    Interp interp,		// Interpreter in which package is now
				// available.
    String pkgName,		// Name of desired package.
    String version,		// Version string for desired version;
				// null means use the latest version
				// available.
    boolean exact)		// true means that only the particular
				// version given is acceptable. false means
				// use the latest compatible version.
throws
    TclException
{
    VersionSatisfiesResult vsres;
    Package pkg;
    PkgAvail avail, best;
    String script;
    StringBuffer sbuf;
    int pass, result;
  
    /*
     * Do extra check to make sure that version is not
     * null when the exact flag is set to true.
     */

    if (version == null && exact) {
	throw new TclException(interp,
		"conflicting arguments : version == null and exact == true");
    }

    /*
     * Before we can compare versions the version string
     * must be verified but if it is null we are just looking
     * for the latest version so skip the check in this case.
     */

    if (version != null) {
	checkVersion(interp, version);
    }

    /*
     * It can take up to three passes to find the package:  one pass to
     * run the "package unknown" script, one to run the "package ifneeded"
     * script for a specific version, and a final pass to lookup the
     * package loaded by the "package ifneeded" script.
     */
  
    vsres = new VersionSatisfiesResult();
    for (pass = 1; ; pass++) {
	pkg = findPackage(interp, pkgName);
	if (pkg.version != null) {
	    break;
	}

	/*
	 * The package isn't yet present.  Search the list of available
	 * versions and invoke the script for the best available version.
	 */
    
	best = null;
	for (avail = pkg.avail; avail != null; avail = avail.next) {
	    if ((best != null) && (compareVersions(avail.version,
		    best.version, null) <= 0)) {
		continue;
	    }
	    if (version != null) {
		result = compareVersions(avail.version, version, vsres); 
		if ((result != 0) && exact) {
		    continue;
		}
		if (!vsres.satisfies) {
		    continue;
		}
	    }
	    best = avail;
	}
	if (best != null) {
	    /*
	     * We found an ifneeded script for the package.  Be careful while
	     * executing it:  this could cause reentrancy, so (a) protect the
	     * script itself from deletion and (b) don't assume that best
	     * will still exist when the script completes.
	     */
      
	    script = best.script;
	    try {
		interp.eval(script, TCL.GLOBAL_ONLY);
	    } catch (TclException e) {
		interp.addErrorInfo("\n    (\"package ifneeded\" script)");
	
		/* 
		 * Throw the error with new info added to errorInfo.
		 */
		
		throw e;
	    }
	    interp.resetResult();
	    pkg = findPackage(interp, pkgName);
	    break;
	}
    
	/*
	 * Package not in the database.  If there is a "package unknown"
	 * command, invoke it (but only on the first pass;  after that,
	 * we should not get here in the first place).
	 */
    
	if (pass > 1) {
	    break;
	}
	script = interp.packageUnknown;
	if (script != null) {
	    sbuf = new StringBuffer();
	    try {
		Util.appendElement(interp, sbuf, script);
		Util.appendElement(interp, sbuf, pkgName);
		if (version == null) {
		    Util.appendElement(interp, sbuf, "");
		} else {
		    Util.appendElement(interp, sbuf, version);
		}
		if (exact) {
		    Util.appendElement(interp, sbuf, "-exact");
		}
	    } catch (TclException e) {
		throw new TclRuntimeError("unexpected TclException: " + e);
	    }
	    try {
		interp.eval(sbuf.toString(), TCL.GLOBAL_ONLY);
	    } catch (TclException e) {
		interp.addErrorInfo("\n    (\"package unknown\" script)");
	
		/* 
		 * Throw the first exception.
		 */

		throw e;
	    }
	    interp.resetResult();
	}
    }
    if (pkg.version == null) {
	sbuf = new StringBuffer();
	sbuf.append("can't find package " + pkgName);
	if (version != null) {
	    sbuf.append(" " + version);
	}
	throw new TclException(interp, sbuf.toString());
    }
  
    /*
     * At this point we know that the package is present.  Make sure that the
     * provided version meets the current requirement.
     */
  
    if (version == null) {
	return pkg.version;
    }

    result = compareVersions(pkg.version, version, vsres);
    if ((vsres.satisfies && !exact) || (result == 0)) {
	return pkg.version;
    }

    /* 
     * If we have a version conflict we throw a TclException.
     */
    
    throw new TclException(interp, "version conflict for package \"" + pkgName
	    + "\": have " + pkg.version + ", need " + version);
}

/*
 *----------------------------------------------------------------------
 *
 * cmdProc --
 *
 *	This procedure is invoked to process the "package" Tcl command.
 *	See the user documentation for details on what it does.
 *
 * Side effects:
 *	|>None.<|
 *
 *----------------------------------------------------------------------
 */

public void cmdProc(
    Interp interp, 	// The current interpreter.
    TclObject argv[])   // Command arguments.
throws 
    TclException 	// Thrown if an error occurs.
{
    VersionSatisfiesResult vsres;
    Package pkg;
    PkgAvail avail;
    PkgAvail prev;
    String version;
    String pkgName;
    String key;
    String cmd;
    String ver1, ver2;
    StringBuffer sbuf;
    Enumeration enum;
    int i, opt, exact;
    boolean once;

    if (argv.length < 2) {
	throw new TclNumArgsException(interp, 1, argv,
		"option ?arg arg ...?");
    }
    opt = TclIndex.get(interp, argv[1], validCmds, "option", 0);
    switch (opt) {
    case OPT_FORGET: {
	/*
	 * Forget takes 0 or more arguments.
	 */
	
	for (i = 2; i < argv.length ; i++) {
	    /*
	     * We do not need to check to make sure
	     * package name is "" because it would not
	     * be in the hash table so name will be ignored.
	     */

	    pkgName = argv[i].toString();
	    pkg = (Package)interp.packageTable.get(pkgName);
	  
	    /*
	     * If this package does not exist, go to next one.
	     */

	    if (pkg == null) {
		continue;
	    }
	    interp.packageTable.remove(pkgName);
	    while (pkg.avail != null) {
		avail = pkg.avail;
		pkg.avail = avail.next;
		avail = null;
	    }
	    pkg = null;
	}
	return;
    }
    case OPT_IFNEEDED: {
	if ((argv.length < 4) || (argv.length > 5)) {
	    throw new TclNumArgsException(interp, 1, argv, 
		    "ifneeded package version ?script?");
	}	
	pkgName = argv[2].toString();
	version = argv[3].toString(); 
	
	/*
	 * Verify that this version string is valid.
	 */

	checkVersion(interp, version);
	if (argv.length == 4) {
	    pkg = (Package) interp.packageTable.get(pkgName);	  
	    if (pkg == null)
		return;
	  
	} else {
	    pkg = findPackage(interp, pkgName);
	}
	for (avail = pkg.avail, prev = null; avail != null;
	         prev = avail, avail = avail.next) { 	  
	    if (compareVersions(avail.version, version, null) == 0) {
		if (argv.length == 4) {
		    /*
		     * If doing a query return current script.
		     */

		    interp.setResult(avail.script);
		    return;
		}
	    
		/* 
		 * We matched so we must be setting the script.
		 */

		break;
	    }
	}
	
	/*
	 * When we do not match on a query return nothing.
	 */

	if (argv.length == 4) {
	    return;
	}
	if (avail == null) {
	    avail = new PkgAvail();
	    avail.version = version;
	    if (prev == null) {
		avail.next = pkg.avail;
		pkg.avail = avail;
	    } else {
		avail.next = prev.next;
		prev.next = avail;
	    }
	}
	avail.script = argv[4].toString();
	return;
    }
    case OPT_NAMES: {
	if (argv.length != 2) {
	    throw new TclNumArgsException(interp, 1, argv, 
		    "names");
	}

	try {
	    sbuf = new StringBuffer();
	    enum = interp.packageTable.keys();
	    once = false;
	    while (enum.hasMoreElements()) {
		once = true;
		key = (String) enum.nextElement();
		pkg = (Package) interp.packageTable.get(key); 
		if ((pkg.version != null) || (pkg.avail != null)) {
		    Util.appendElement(interp, sbuf, key);
		}
	    }
	    if (once) {
		interp.setResult(sbuf.toString());
	    }
	} catch (TclException e) {
	    throw new TclRuntimeError("unexpected TclException: " + e);
	}
	return;
    }
    case OPT_PROVIDE: {
	if ((argv.length < 3) || (argv.length > 4)) {
	    throw new TclNumArgsException(interp, 1, argv, 
		    "provide package ?version?");
	}		
	if (argv.length == 3) {
	    pkg = (Package)interp.packageTable.get( argv[2].toString() );
	    if (pkg != null) {
		if (pkg.version != null) {
		    interp.setResult(pkg.version);
		}
	    }
	    return;
	}
	pkgProvide(interp, argv[2].toString(), argv[3].toString());
	return;
    }
    case OPT_REQUIRE: {
	if ((argv.length < 3) || (argv.length > 5)) {
	    throw new TclNumArgsException(interp, 1, argv, 
		    "require ?-exact? package ?version?");
	}	
	if (argv[2].toString().equals("-exact")) {
	    exact = 1;
	} else {
	    exact = 0;
	}
	version = null;
	if (argv.length == (4 + exact)) {
	    version = argv[3 + exact].toString();
	} else if ((argv.length != 3) || (exact == 1)) {
	    throw new TclNumArgsException(interp, 1, argv, 
		    "require ?-exact? package ?version?");
	}
	if (exact == 1) {
	    version = pkgRequire(interp, argv[3].toString(), version, true);
	} else {
	    version = pkgRequire(interp, argv[2].toString(), version, false);
	}
	interp.setResult(version);
	return;
    }
    case OPT_UNKNOWN: {
	if (argv.length > 3) {
	    throw new TclNumArgsException(interp, 1, argv, 
		    "unknown ?command?");
	}
	if (argv.length == 2) {
	    if (interp.packageUnknown != null) {
		interp.setResult(interp.packageUnknown);
	    }
	} else if (argv.length == 3) {
	    interp.packageUnknown = null;
	    cmd = argv[2].toString();
	    if (cmd.length() > 0) {
		interp.packageUnknown = cmd;
	    }
	}
	return;
    }
    case OPT_VCOMPARE: {
	if (argv.length != 4) {
	    throw new TclNumArgsException(interp, 1, argv, 
		    "vcompare version1 version2");
	}	
	ver1 = argv[2].toString();
	ver2 = argv[3].toString();
	checkVersion(interp, ver1);
	checkVersion(interp, ver2);
	interp.setResult(compareVersions(ver1, ver2, null));
	return;
    }
    case OPT_VERSIONS: {
	if (argv.length != 3) {
	    throw new TclNumArgsException(interp, 1, argv, 
		    "versions package");
	}
	pkg = (Package)interp.packageTable.get(argv[2].toString());
	if (pkg != null) {
	    try {	
		sbuf = new StringBuffer();
		once = false;
		for (avail = pkg.avail; avail != null; avail = avail.next) {
		    once = true;
		    Util.appendElement(interp, sbuf, avail.version);
		}
		if (once) {
		    interp.setResult(sbuf.toString());
		}
	    } catch (TclException e) {
		throw new TclRuntimeError("unexpected TclException: " + e);
	    }
	}
	return;
    }
    case OPT_VSATISFIES: {
	if (argv.length != 4) {
	    throw new TclNumArgsException(interp, 1, argv, 
		    "vsatisfies version1 version2");
	}
	
	ver1 = argv[2].toString();
	ver2 = argv[3].toString();
	checkVersion(interp, ver1);
	checkVersion(interp, ver2);
	vsres = new VersionSatisfiesResult();
	compareVersions(ver1,ver2,vsres);
	interp.setResult(vsres.satisfies);
	return;
    }
    default: { 
	throw new TclRuntimeError("TclIndex.get() error");
    }
    } // end switch(opt)
}

/*
 *----------------------------------------------------------------------
 *
 * findPackage --
 *
 *	This procedure finds the Package record for a particular package
 *	in a particular interpreter, creating a record if one doesn't
 *	already exist.
 *
 * Results:
 *	The return value is a ref to the Package record for the
 *	package.
 *
 * Side effects:
 *	A new Package record may be created.
 *
 *----------------------------------------------------------------------
 */

private static Package 
findPackage(
    Interp interp,		// Interpreter to use for package lookup.
    String pkgName) 		// Name of package to find. 
throws 
    TclException
{
    Package pkg;

    /*
     * check package name to make sure it is not null or "".
     */

    if (pkgName == null || pkgName.length() == 0) {
	throw new TclException(interp, 
		"expected package name but got \"\"");
    }

    pkg = (Package) interp.packageTable.get(pkgName);
    if (pkg == null) {
	/*
	 * We should add a package with this name.
	 */
    
	pkg = new Package();
	interp.packageTable.put(pkgName, pkg);
    }
    return pkg;
}

/*
 *----------------------------------------------------------------------
 *
 * checkVersion --
 *
 *	This procedure checks to see whether a version number has
 *	valid syntax.
 *
 * Results:
 *	If string is not properly formed version number then a TclException
 *      is raised.
 *
 * Side effects:
 *	None.
 *
 *----------------------------------------------------------------------
 */

private static void 
checkVersion(
    Interp interp,		// Used for error reporting.
    String version)		// Supposedly a version number, which is
				// groups of decimal digits separated
				// by dots.
throws 
    TclException 
{    
    int i, len;
    char c;
    boolean error = true;
    
    try {
	if ((version == null) || (version.length() == 0)) {
	    version = "";
	    return;
	}
	if (!Character.isDigit(version.charAt(0))) {
	    return;
	}
	len = version.length();
	for (i = 1; i < len; i++) {
	    c = version.charAt(i);
	    if (!Character.isDigit(c) && (c != '.')) {
		return;
	    }
	}
	if (version.charAt(len - 1) == '.') {
	    return;
	}
	error = false;
    } finally {
	if (error) {
	    throw new TclException(interp, 
		    "expected version number but got \"" + version + "\"");
	}
    }
}

/*
 *----------------------------------------------------------------------
 *
 * compareVersions --
 *
 *	This procedure compares two version numbers.
 *
 * Results:
 *      This function will return a -1 if v1 is less than v2, 0
 *      if the two version numbers are the same, and 1 if v1 is
 *      greater than v2. If the sat argument is not null then
 *      then its VersionSatisfiesResult.satisifes field will be
 *      true if v2 >= v1 and both numbers have the same major number
 *	or false otherwise.
 *
 * Side effects:
 *	None.
 *
 *----------------------------------------------------------------------
 */

private static int 
compareVersions(
    String v1, 		// Versions strings. (e.g.  2.1.3)
    String v2,
    VersionSatisfiesResult vsres)

{
    int i;
    int max;
    int n1 = 0;
    int n2 = 0;
    boolean thisIsMajor = true;
    String[] v1ns;
    String[] v2ns;

    /*
     * Each iteration of the following loop processes one number from
     * each string, terminated by a ".".  If those numbers don't match
     * then the comparison is over;  otherwise, we loop back for the
     * next number.
     */

    /*
     * These should never happen because null strings would not
     * have gotten past the version verify.
     */

    if ((v1 == null) || (v2 == null)) {
	throw new TclRuntimeError("null version in package version compare");
    }
    v1ns = split(v1, '.');
    v2ns = split(v2, '.');

    /*
     * We are sure there is at least one string in each array so 
     * this should never happen.
     */

    if (v1ns.length == 0 || v2ns.length == 0) {
	throw new TclRuntimeError("version length is 0");
    }
    if (v1ns.length > v2ns.length) {
	max = v1ns.length;
    } else {
	max = v2ns.length;
    }
    
    for (i=0; i < max ; i++) {
	n1 = n2 = 0;

	/* 
	 * Grab number from each version ident if version spec
	 * ends the use a 0 as value.
	 */ 

	try {
	    if (i < v1ns.length) {
		n1 = Integer.parseInt( v1ns[i] );  
	    }
	    if (i < v2ns.length) {
		n2 = Integer.parseInt( v2ns[i] );  
	    }
	} catch (NumberFormatException ex) {
	    throw new TclRuntimeError(
		    "NumberFormatException for package versions \"" + v1
		    + "\" or \"" + v2 + "\"");
	}

	/*
	 * Compare and go on to the next version number if the
	 * current numbers match.
	 */

	if (n1 != n2) {
	    break;
	}
	thisIsMajor = false;
    }
    if (vsres != null) {
	vsres.satisfies = ((n1 == n2) || ((n1 > n2) && !thisIsMajor));
    }
    if (n1 > n2) {
	return 1;
    } else if (n1 == n2) {
	return 0;
    } else {
	return -1;
    }
}

/*
 *----------------------------------------------------------------------
 *
 * split --
 *
 *	Util function used in version compare to split a string on a
 *	single char it is only used in the version compare function.
 *
 * Results:
 *	|>None.<|
 *
 * Side effects:
 *	|>None.<|
 *
 *----------------------------------------------------------------------
 */

static String[] 
split(
    String in, 
    char splitchar) 
{  
    Vector words;
    String[] ret;
    int i;
    int len;
    char[] str;
    int wordstart = 0;
    
    /*
     * Create an array that is as big as the input
     * str plus one for an extra split char.
     */
  
    len = in.length();
    str = new char[len + 1];
    in.getChars(0,len,str,0);
    str[len++] = splitchar;
    words = new Vector(5);
    
    for (i=0; i < len; i++) {
			
	/*
	 * Compare this char to the split char
	 * if they are the same the we need to
	 * add the last word to the array.
	 */
    
	if (str[i] == splitchar) {
	    if (wordstart <= (i - 1)) {
		words.addElement( new String(str, wordstart, i - wordstart) );
	    }
	    wordstart = (i + 1);
	}
    }

    /*
     * Create an array that is as big as the number
     * of elements in the vector, copy over and return.
     */

    ret = new String[words.size()];
    words.copyInto(ret);
    return ret;
}








/*
 * if compare versions is called with a third argument then one of
 * these structures needs to be created and passed in
 */

static class VersionSatisfiesResult {
boolean satisfies = false;
}

/*
 * Each invocation of the "package ifneeded" command creates a class
 * of the following type, which is used to load the package into the
 * interpreter if it is requested with a "package require" command.
 */

static class PkgAvail {
String version = null;	/* Version string. */
String script = null;	/* Script to invoke to provide this version
			 * of the package. */
PkgAvail next = null;       /* Next in list of available versions of
			     * the same package. */
}



/*
 * For each package that is known in any way to an interpreter, there
 * is one record of the following type.  These records are stored in
 * the "packageTable" hash table in the interpreter, keyed by
 * package name such as "Tk" (no version number).
 */

static class Package {
String version = null;	 /* Version that has been supplied in this
			  * interpreter via "package provide"
			  * null means the package doesn't
			  * exist in this interpreter yet. */

PkgAvail avail = null;	/* First in list of all available versions
			 * of this package. */
}


} //end of class PackageCmd
