package code.loader;

import code.loader.except.ModFileDuplicatedException;
import code.loader.except.ModFileNotFoundException;
import code.stuff.Logger;
import code.stuff.Tracer;

import java.io.File;
import java.io.IOException;
import java.io.PrintStream;
import java.io.Writer;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.StringTokenizer;
import java.util.Vector;

/**
 * This class encapsulates PATHS where source files can be located.
 * one initializes CurryModulePath by appending "paths" to it.
 * then one can "ask" the CurryModulePath object to provide a complete path
 * to some fileName.
 * <p/>
 * Currently all members of this class will be static.
 * This implies that there is exactly one CurryModulePath.
 * Primary user of this class LoadManager.
 * Other user is the Command class in loop package.
 * <p/>
 *
 * @author Pravin Damle
 * @since Aug 7, 2004
 * Modified by Sunita Marathe on Jul 15, 2005
 */
public class CurryModulePath {
    /**
     * A Static data member that holds all the normalized paths
     * that should be searched when loading a module.  A
     * normalized path is independent of the "current working
     * directory" of the FLVM.
     */
    private static Vector<String> curryPaths = new Vector<String>();
    private static Vector<String> nativePaths = new Vector<String>();
    private static Vector<String> nativePkgs = new Vector<String>();

    // Prevent constructing an object of this class
    private CurryModulePath() {}

    public static final int CURRYPATH = 0;
    public static final int NATIVEPATH = 1;
    public static final int NATIVEPKG = 2;

    /**
     * Add the specified path(s) to the CurryModulePath.
     * The paths are colon (:) separated.
     * Any legal path (from the Operating System perspective) can be specified.
     * Requesting to add a nonexisting or null path to CurryModulePath,
     * prints an error message on stderr.
     * Requesting to add a path that is not a readable directory
     * to CurryModulePath, prints an error message on stderr.
     * All specified paths are converted to their Normalized
     * representation before adding them.
     * Normalized representation is independent of the 
     * "current working directory".
     * If a path has already been added, then a warning is printed
     * on the stderr.
     * <p/>
     * This method will be called by Main and Read-Eval-Print loop.
     *
     * @param path A Colon separated list of paths.
     */
    public static void append(String path, int pathType) throws IOException {
        if (path == null) {
            System.err.println("Warning: null " 
				+ (pathType==CURRYPATH ? "CurryPath" : "NativePath") 
				+ ". Nothing to append");
            return;
        }
        String[] paths = splitPath(path, pathType);
        for (int i = 0; i < paths.length; i++) {
            try {
                String thePath = getNormalizedPath(paths[i]);

                if (pathExists(thePath, pathType)) {
                    System.err.println("Warning: " + thePath + 
                                       " already exists in the " + (pathType == CURRYPATH ? "CurryPath" : "NativePath"));
                    continue;
                }

                File checkPath = new File(thePath);

                if (checkPath != null && checkPath.isDirectory()
                    && checkPath.canRead()) {
		    if (pathType == CURRYPATH)
			curryPaths.add(thePath);
		    else
			nativePaths.add(thePath);
                } else {
                    System.err.println("Warning: " + thePath + 
                                       " does not exist OR is not a directory OR cannot be read.");
                }
            } catch (IOException ioe) {
                System.err.println("Warning: The path " + paths[i] + 
                                   " cannot be Normalized. Reason:" + 
                                   ioe.getMessage());
            }
        }
    }

    public static void appendPkgs (String path) throws IOException {
        if (path == null) {
            System.err.println("Warning: null packages. nothing to append");
            return;
        }
        String[] pkgs = splitPath(path, NATIVEPKG);
        for (int i = 0; i < pkgs.length; i++) {
	    nativePkgs.add (pkgs[i]);
	}
    }


    /**
     * Search the CurryModulePath for file containing this module
     * and return the complete path of that file.  If some path in
     * the CurryModulePath is not accessible, then it should be
     * reported as a warning on stderr and the search should
     * continue.  If multiple files containing the specified
     * module exists, then it should be reported as an exception.
     *
     * This method will be called by LoadManager to obtain the
     * complete file path of the module file that needs to be
     * loaded.
     *
     * @return The nonnull path String
     * @throws code.loader.except.ModFileNotFoundException
     *
     * @throws code.loader.except.ModFileDuplicatedException
     *
     */
    public static String getFilePath(String moduleName)
	throws ModFileNotFoundException, ModFileDuplicatedException {

        String[] extensions = {".txt"};
        String completePath = null;
        int flag = 0;
	Vector<String> modules = new Vector<String>();

        Enumeration paths = curryPaths.elements();
        while (paths.hasMoreElements()) {
            String nextPath = (String) paths.nextElement();
            File abstractPath = new File(nextPath);
            // allCurryModulePath maybe misformated by the user, 
            // so abstractPath and filelist can be null!!!
            // check if this curryModulePath[i] exists and is valid
            if (abstractPath == null) {
                // if we have problems accessing some path, then we skip it.
                System.err.println("Warning: Problem accessing " + nextPath);
                continue;
            } else {
                for (int i = 0; i < extensions.length; i++) {
                    String fullModuleName = moduleName + extensions[i];
                    String tempPath = nextPath + "/" + fullModuleName;

                    File moduleFile = new File(tempPath);
                    if (moduleFile.exists()) {
                        flag++;
			modules.add(tempPath);
                        if (completePath == null)
			  completePath = tempPath;
                    }
                }
            }
        }

        String moduleClassName = moduleName + ".class";
        paths = nativePaths.elements();
        while (paths.hasMoreElements()) {
            String nextPath = (String) paths.nextElement();
            File abstractPath = new File(nextPath);
            // allCurryModulePath maybe misformated by the user, 
            // so abstractPath and filelist can be null!!!
            // check if this curryModulePath[i] exists and is valid
            if (abstractPath == null) {
                // if we have problems accessing some path, then we skip it.
                System.err.println("Warning: Problem accessing " + nextPath);
                continue;
            } else {
		Enumeration pkgs = nativePkgs.elements();
		while (pkgs.hasMoreElements()) {
		    String pkg1 = (String) pkgs.nextElement();
		    String pkg2 = pkg1.replace ('.', '/');
                    String tempPath = nextPath + "/" + pkg2 + "/" + moduleClassName;
                    File moduleFile = new File(tempPath);
                    if (moduleFile.exists()) {
                        flag++;
			modules.add(tempPath);
                        if (completePath == null)
			  completePath = nextPath + "/" + pkg1 + "." + moduleClassName;
                    }
                }
            }
        }

        // check if curryModulePath contains the module or not, and also check
        // the duplicate module in all curryModulePath
        // TODO: print the possible paths in the error messages
        if (flag == 0) {
            throw new ModFileNotFoundException
		("The file \"" + moduleName + 
		 "\" was not found in any of the following paths \n" +
		 showPaths());
        }
        if (flag > 1) {
	    String multModsMsg = 
		"\nFound multiple modules named \"" + moduleName + "\"";
	    String mods = "";
	    for (String mod : modules) mods += mod +"\n";
            System.err.println(multModsMsg);
            System.err.println(mods);
	    System.err.println ("Loading first instance :\n\t" +
			       completePath);
	    // TODO: It should really exit here, 
	    // since one instance is loaded arbitrarily
            // System.exit(1);
            // throw new ModFileDuplicatedException
	    // ("file \"" + moduleName + "\" duplicated in possible paths");
        }
        return completePath;
    }

    /**
     * Shows/Prints the contents of the CurryModulePath on the
     * specified PrintStream.  The paths are printed in sorted
     * order, so that repeated tests print the paths in the same
     * order.
     *
     * @param out a PrintStream on which to show the contents of
     * CurryModulePath.
     */
    public static void show(Writer out) {
	try {
	    out.write(showPaths());
	    out.flush();
	} catch (IOException ex) {
	    System.err.println("Cannot print! "+ex.getMessage());
	    System.exit(1);
	}
    }

    public static String showPaths() {
	String result = "";
	for (String path : curryPaths) result += path+"\n";
	for (String path : nativePaths) 
	    for (String pkg : nativePkgs) 
		result += (path + "/" + pkg.replace ('.', '/') + "/" + "\n");
	return result;
    }

    /**
     * Set the CurryModulePath to the specified path.
     * <p/>
     * This is implemented by assigning a new Vector to 
     * the ModulePath static data member
     * and then appending the specified path.
     */
    public static void set(String path) {
        try {
            curryPaths = new Vector();
            append(path, CURRYPATH);
        } catch (Exception e) {
            System.err.println("Warning: Error in setting the CurryModulePath to " + path + ". Reason: " + e.getMessage());
        }
    }

    /**
     * Removes the specified path(s) from CurryModulePath.
     * Several paths can be specified as a colon (:) separated
     * list.  If the specified path is not found in the
     * CurryModulePath, then a warning is printed on stderr.
     *
     * @param path The path(s) that should be removed from CurryModulePath
     */
    public static void remove(String path) {
        try {
            String[] paths = splitPath(path, CURRYPATH);
            for (int i = 0; i < paths.length; i++) {
                boolean found = false;
                String normalizedPath = getNormalizedPath(paths[i]);
                Enumeration modulepaths = curryPaths.elements();
                while (modulepaths != null && modulepaths.hasMoreElements()) {
                    String searchPath = (String) modulepaths.nextElement();
                    if (searchPath.compareTo(normalizedPath) == 0) {
                        curryPaths.remove(searchPath);
                        found = true;
                    }
                }
                if (!found) {
                    System.err.println
                        ("Warning: CurryModulePath does not contain " + 
                         paths[i]);
                }
            }
        } catch (IOException ioe) {
            System.err.println("Warning: Error in removing path " + 
                               path + " Reason: " + ioe.getMessage());
        }
    }

    /**
     * Appends each element of the paths array to ModulePath.
     * This method must use the public append method to do the actual task.
     *
     * @param paths An array of paths, each of which is assumed to
     * be not Colon separated.
     */
    private static void append(String[] paths) throws IOException {
        if (paths == null) {
            System.err.println("Warning: paths == null. Cannot Append");
            return;
        }
        for (int i = 0; i < paths.length; i++) {
            append(paths[i], CURRYPATH);
        }

    }

    /**
     * Split a colon (:) separated string into an array of strings.
     *
     * @param path A Colon Separated Path
     * @return An array of String
     * @throws IOException
     */
    private static String[] splitPath(String path, int pathType) throws IOException {
	String type = pathType==CURRYPATH ? "CurryPath" : (pathType==NATIVEPATH ? "NativeLib_ClassPath" : "NativeLib_Pkg");
        //path maybe misformated by the user!!!
        StringTokenizer st = new StringTokenizer(path, ":");
        if (st.countTokens() == 0) {
            throw new IOException(type + " is wrongly formatted!");
        }
        String[] curryModulePath = new String[st.countTokens()];
        if (Tracer.symbols) {
            Logger.logln("\npossible " + type + "s are: ");
        }
        for (int i = 0; st.hasMoreTokens(); ++i) {
            curryModulePath[i] = st.nextToken();
            if (Tracer.symbols) {
                Logger.logln(curryModulePath[i]);
            }
        }
        return curryModulePath;
    }

    /**
     * Returns the normalized path of the specified path.
     * Normalized representation is independent of the 
     * "current working directory".
     * Currently the canonical path is the normalized path.
     * <p/>
     * ToDo: Enhance the functionality of getNormalizedPath method to interpret
     * some META CHARACTERS and TAGS and replace them with actual path fragments.
     * For example: we could have a path like ~/curry/lib
     * which could translate to /home/pravin/curry/lib
     * Another example: we could have a path like $FLVMHOME/test1
     * which could translate to /home/pravin/curry/test1
     * if $FLVMHOME environment variable's value is /home/pravin/curry
     *
     * @param somePath Some path whose Normalized path is desired.
     * @return The normalized path
     */
    private static String getNormalizedPath(String somePath) throws IOException {
        File someFile = new File(somePath);
        return someFile.getCanonicalPath();
    }

    /**
     * Returns true if the specified path already exists in the
     * CurryModulePath. Otherwise it returns false.
     *
     * @param path The path that should be checked for existance
     * in CurryModulePath
     * @return A boolean indicating if the specified path exists
     * (true) or not (false)
     */
    private static boolean pathExists(String path, int pathType) {
        Enumeration paths = null;
	if (pathType == CURRYPATH)
 		paths = curryPaths.elements();
	else
 		paths = nativePaths.elements();
        while (paths != null && paths.hasMoreElements()) {
            String searchPath = (String) paths.nextElement();
            if (searchPath.compareTo(path) == 0) {
                return true;
            }
        }
        return false;
    }

}
