/*
 * Copyright 2004-2008 Sun Microsystems, Inc. All Rights Reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER
 * 
 * This code is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 2
 * only, as published by the Free Software Foundation.
 * 
 * This code is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * General Public License version 2 for more details (a copy is
 * included in the LICENSE file that accompanied this code).
 * 
 * You should have received a copy of the GNU General Public License
 * version 2 along with this work; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA
 * 
 * Please contact Sun Microsystems, Inc., 16 Network Circle, Menlo
 * Park, CA 94025 or visit www.sun.com if you need additional
 * information or have any questions.
 */

package com.sun.squawk.builder.commands;

import java.io.*;
import java.util.*;

import com.sun.squawk.builder.*;
import com.sun.squawk.builder.ccompiler.*;
import com.sun.squawk.builder.util.*;

/**
 * This is the command that produces the bootstrap suite for the Squawk VM and
 * also compiles the Squawk VM.
 *
 */
public class RomCommand extends Command {

    public static final File VM_SRC_DIR = new File("vmcore/src/vm");
    public static final File VM_BLD_DIR = new File("vmcore/build");
    public static final File FP_SRC_DIR = new File("vmcore/src/vm/fp");
    public static final File UTIL_SRC_DIR = new File("vmcore/src/vm/util");
    public static final File VM_SRC_RTS_DIR = new File("vmcore/src/rts");
    public static final File VM_SRC_FILE = new File(VM_SRC_DIR, "squawk.c");
    public static final File VM2C_SRC_FILE = new File(VM_SRC_DIR, "vm2c.c.spp");

    public static final String PREPROCESSED_FOR_VM2C_DIR_NAME = "preprocessed-vm2c";

    /**
     * The name of the bootstrap suite.
     */
    private String bootstrapSuiteName;

    /**
     * Determines if the C compilation step should be executed or not.
     */
    private boolean compilationEnabled = true;
    
    /**
     * Used to indicate that run does not need to compile, since the rom command was used with a -parent: argument.
     */
    protected boolean noNeedToCompile;

    /**
     * The root classes to search for methods to convert
     */

    private ArrayList<String> vm2cRootClasses;

    public RomCommand(Build env) {
        super(env, "rom");
    }

    /**
     * {@inheritDoc}
     */
    public String getDescription() {
        return "processes a collection of modules to produce the bootstrap suite and Squawk VM executable";
    }

    public void usage(String errMsg) {
        PrintStream out = System.out;

        out.println();
        out.println("usage: [-coptions] rom [-options] [modules...]");
        out.println("where options include:");
        out.println();
        out.println("    <any romizer option - run 'romize -h' for more details>");
        out.println();
        out.println("A module is a directory that has a 'j2meclasses' subdirectory or a jar/zip");
        out.println("file. More than one suite can be created by separating the modules for each");
        out.println("suite with '--'. For example:");
        out.println();
        out.println("  rom cldc graphics -- translator -- samples");
        out.println();
        out.println("will generate 'squawk.suite', 'translator.suite' and 'samples.suite' where");
        out.println("each suite is bound to the previous one");
        out.println("");
        out.println("The '-coptions' refer to the C compilations options supported by the builder.");
        out.println();
    }

    /**
     * Converts the arguments given to this command to romizer arguments.
     *
     * @param   args   the arguments passed to this command
     * @param   arch   the target architecture hint provided by the C compiler
     * @return  <code>args</code> reformatted for the romizer
     */
    private String[] convertToRomizerOptions(String[] args, String arch) {
        String extraCP = "";
        List<String> romizerArgs = new ArrayList<String>();
        String endian = env.getPlatform().isBigEndian() ? "big" : "little";
        String parentSuite = null;
        boolean createMetadatas = false;
        SuiteMetadata parentSuiteMetadata = new SuiteMetadata();
        SuiteMetadata suiteMetadata = new SuiteMetadata();

        if (env.getspecfifiedBuildDotOverrideFileName() != null) {
             romizerArgs.add("-override:" + env.getspecfifiedBuildDotOverrideFileName());
        }
        int argc = 0;
        while (argc != args.length) {
            String arg = args[argc];
            if (arg.charAt(0) == '-') {
                if (arg.startsWith("-cp:")) {
                    extraCP = arg.substring("-cp:".length());
                } else if (arg.startsWith("-h")) {
                    usage(null);
                    // Stop the builder
                    throw new BuildException("", 0);
                } else if (arg.startsWith("-o:")) {
                    bootstrapSuiteName = arg.substring("-o:".length());
                } else if (arg.startsWith("-arch:")) {
                    arch = arg.substring("-arch:".length());
                } else if (arg.startsWith("-endian:")) {
                    endian = arg.substring("-endian:".length());
                } else if (arg.startsWith("-parent:")) {
                    parentSuite = arg.substring("-parent:".length());
                    noNeedToCompile = true;
                    try {
                        FileInputStream fileIn = new FileInputStream(parentSuite + ".suite.suitemetadata");
                        ObjectInputStream in = new ObjectInputStream(fileIn);
                        parentSuiteMetadata = (SuiteMetadata) in.readObject();
                    } catch (IOException e) {
                    } catch (ClassNotFoundException e) {
                    }
                } else if (arg.startsWith("-metadata")) {
                    createMetadatas = true;
                } else {
                    romizerArgs.add(arg);
                }
            } else {
                break;
            }
            argc++;
        }

        // pass in properties builder was passed on command line:
        for (Object key: env.dynamicProperties.keySet()) {
            Object value = env.dynamicProperties.get(key);
            romizerArgs.add("-D" + key.toString() + "=" + value.toString());
        }

        // The remaining args are the modules making up one or more suites
        boolean isBootstrapSuite = parentSuite == null;
        List<Target.FilePair> allClassesLocations = new ArrayList<Target.FilePair>();
        List<Target.FilePair> allJava5ClassesLocations = new ArrayList<Target.FilePair>();
        while (argc != args.length) {

            List<Target.FilePair> classesLocations = new ArrayList<Target.FilePair>();
            List<Target.FilePair> java5ClassesLocations = new ArrayList<Target.FilePair>();
            String suiteName = null;
            boolean createJars = true;

            while (argc != args.length) {

                String module = args[argc++];

                if (module.equals("--")) {
                    break;
                }

                if (module.startsWith("-o:")) {
                    suiteName = module.substring("-o:".length());
                    continue;
                } else if (module.charAt(0) == '-') {
                    throw new BuildException("cannot specify romizer options for any suite except the first: " + module);
                }

                if (module.endsWith(".jar") || module.endsWith(".zip")) {
                    classesLocations.add(new Target.FilePair(new File(module)));
                    module = new File(module).getName();
                    if (module.endsWith("_classes.jar")) {
                        // This is most likely the jar file build by a previous execution of the romizer
                        module = module.substring(0, module.length() - "_classes.jar".length());
                        createJars = false;
                    } else {
                        module = module.substring(0, module.length() - ".jar".length());
                    }
                } else {
                    Command moduleCommand = env.getCommand(module);
                    Target target = null;
                    if (moduleCommand instanceof Target) {
                        target = (Target) moduleCommand;
                    } else {
                        // see if we can conjure up a target for this module.
                        File moduleDir = new File(module);
                        if (moduleDir.exists() && new File(moduleDir, "src").exists()) {
                            target = env.addTarget(true, module, "");
                        }
                    }
                    if (target != null) {
                        suiteMetadata.addTargetIncluded(module);
                        if (!parentSuiteMetadata.includesTarget(module)) {
                            suiteMetadata.addTargetIncluded(module);
                            suiteMetadata.addTargetsIncluded(target.getDependencyNames());
                            List<Target.FilePair> dirs = new ArrayList<Target.FilePair>();
                            target.addDependencyDirectories(target.getPreverifiedDirectoryName(), dirs, parentSuiteMetadata.getTargetsIncluded());
                            target.addDependencyDirectories(target.getResourcesDirectoryName(), dirs, parentSuiteMetadata.getTargetsIncluded());
                            for (Target.FilePair file : dirs) {
                                if (!allClassesLocations.contains(file) && file.getCanonicalFile().exists()) {
                                    classesLocations.add(file);
                                    allClassesLocations.add(file);
                                }
                            }
                            if (target.j2me) {
                                dirs = new ArrayList<Target.FilePair>();
                                target.addDependencyDirectories(target.getCompiledDirectoryName(), dirs, parentSuiteMetadata.getTargetsIncluded());
                                for (Target.FilePair file : dirs) {
                                    if (!allJava5ClassesLocations.contains(file) && file.getCanonicalFile().exists()) {
                                        java5ClassesLocations.add(file);
                                        allJava5ClassesLocations.add(file);
                                    }
                                }
                            }
                        }
                    } else {
                        throw new BuildException("'" + module + "' module is not a jar/zip file and does not have a target defined for it");
                    }
                }

                // The default name of the current suite is the name of the first module
                if (suiteName == null) {
                    suiteName = module;
                }
            }

            if (isBootstrapSuite) {
                suiteName = bootstrapSuiteName;
                romizerArgs.add("-o:" + suiteName);
                if (extraCP != "") {
                    classesLocations.add(new Target.FilePair(new File(extraCP)));
                }
                romizerArgs.add("-arch:" + arch);
                romizerArgs.add("-endian:" + endian);
            } else {
                romizerArgs.add("--");
                romizerArgs.add("-o:" + suiteName);
            }
            StringBuilder cp = new StringBuilder();
            cp.append("-cp:");
            for (Target.FilePair fp: classesLocations) {
                File file = fp.getFile();
                cp.append(file.getPath());
                cp.append(File.pathSeparatorChar);
            }
            romizerArgs.add(cp.toString());
            if (!java5ClassesLocations.isEmpty()) {
            	StringBuilder java5Cp = new StringBuilder();
            	java5Cp.append("-java5cp:");
                for (Target.FilePair fp: java5ClassesLocations) {
                    File file = fp.getFile();
                	java5Cp.append(file.getPath());
                	java5Cp.append(File.pathSeparatorChar);
                }
                romizerArgs.add(java5Cp.toString());
            }
            if (createMetadatas) {
                romizerArgs.add("-metadata");
            }
            if (createJars) {
                romizerArgs.add("-jars");
            }

            for (Target.FilePair fp: classesLocations) {
                File file = fp.getFile();
                String path = file.getPath();
                if (!romizerArgs.contains(path)) {
                    romizerArgs.add(path);
                }
            }

            if (createMetadatas) {
                FileOutputStream fileOut;
                ObjectOutputStream out = null;
                try {
                    File file = new File(suiteName + ".suite.suitemetadata");
                    env.log(env.verbose, "SUITEMETADATA:" + file.getPath());
                    fileOut = new FileOutputStream(file);
                    out = new ObjectOutputStream(fileOut);
                    out.writeObject(suiteMetadata);
                } catch (FileNotFoundException e1) {
                } catch (IOException e) {
                } finally {
                    if (out != null) try {out.close();} catch (IOException e) {}
                }
            }
            
            isBootstrapSuite = false;
        }

        if (parentSuite != null) {
            String arg = "-parent:" + parentSuite;
            if (romizerArgs.get(0).equals("--")) {
                romizerArgs.set(0, arg);
            } else {
                romizerArgs.add(0, arg);
            }
        }

        args = new String[romizerArgs.size()];
        romizerArgs.toArray(args);
        return args;
    }

    /**
     * Creates the "buildflags.h" file containing the string constant indicating what
     * C compiler flags the VM was built with.
     *
     * @param buildFlags   the C compiler flags
     */
    private void createBuildFlagsHeaderFile(String buildFlags) {
        File buildFlagsFile = new File(VM_SRC_DIR, "buildflags.h");
        try {
            FileOutputStream fos = new FileOutputStream(buildFlagsFile);
            PrintStream out = new PrintStream(fos);
            out.println("#define BUILD_FLAGS \"" + buildFlags + '"');
            fos.close();
        } catch (IOException e) {
            throw new BuildException("could not create " + buildFlagsFile, e);
        }
    }

    private File getFileDerivedFromSppFile(File sppFile) {
        String path = sppFile.getPath();
        path = path.substring(0, path.length() - ".spp".length());
        return new File(path);
    }

    private void updateVM2CGeneratedFile() {
        vm2cRootClasses = new ArrayList<String>();

        // always translate these classes:
        vm2cRootClasses.add("com.sun.squawk.VM");
        vm2cRootClasses.add("com.sun.squawk.MethodHeader");

        // may translate GC:
        if (env.getBooleanProperty("GC2C")) {
            vm2cRootClasses.add(env.getProperty("GC"));
        }

        FileSet.Selector isOutOfDate = new FileSet.AndSelector(Build.JAVA_SOURCE_SELECTOR, new FileSet.DependSelector(new FileSet.Mapper() {
            public File map(File from) {
                return VM2C_SRC_FILE;
            }
        }));

        File srcDir = new File("cldc", "src");
        if (!srcDir.exists()) {
            env.log(env.info, "No " + srcDir + " so assuming no need to generate vm2c files, ie remote build and files are already generated");
            return;
        }
        File preDir;
        if (env.isJava5SyntaxSupported()) {
            preDir = new File("cldc", "preprocessed");
        } else {
            Target cldcTarget = (Target) env.getCommandForced("cldc");
            preDir = env.preprocess(new File("cldc"), cldcTarget.srcDirs, true, true);
        }

        // Rebuilds the generated file if any of the *.java files in cldc/src or cldc/preprocessed have
        // a later modification date than the generated file.
        if (!VM2C_SRC_FILE.exists() || !new FileSet(srcDir, isOutOfDate).list().isEmpty() || (preDir.exists() && !new FileSet(preDir, isOutOfDate).list().isEmpty())) {
            // Verify that the vm2c commands were installed in my env, if not then throw an exception
            // This is to handle case where vm2c did not install itself, but we still wanted to run the rom command to do just compilation
            Command command = env.getCommand("runvm2c");
            if (command == null) {
                throw new BuildException("The module vm2c and runvm2c we're not installed, this is very likely due to these modules requiring the use of JDK 1.5 or higher");
            }
            // Ensure that the *existing* preprocessed files in cldc are in sync with the original sources
            FileSet.Selector selector = new FileSet.AndSelector(Build.JAVA_SOURCE_SELECTOR, new FileSet.DependSelector(new FileSet.SourceDestDirMapper(srcDir, preDir)) {
                public boolean isSelected(File file) {
                    File dependentFile = mapper.map(file);
                    return dependentFile.exists() && dependentFile.lastModified() < file.lastModified();

                }
            });
            List<File> ood = new FileSet(srcDir, selector).list();
            if (!ood.isEmpty()) {
                throw new BuildException("need to re-build 'cldc' as the preprocessed version of the following files are out of date: " + ood);
            }

            List<String> args = new ArrayList<String>();
            args.add("-o:" + VM2C_SRC_FILE);
            args.add("-cp:");
            args.add("-sp:." + File.pathSeparator + preDir.getPath());
            for (String rootClass: vm2cRootClasses) {
                env.log(env.verbose, "vm2c root class: " + rootClass);
                args.add("-root:" + rootClass);
            }
            args.addAll(new FileSet(preDir, Build.JAVA_SOURCE_SELECTOR).listStrings());

            File argsFile = new File("vm2c", "vm2c.input");
            JavaCompiler.createArgsFile(args, argsFile);
            VM2C_SRC_FILE.delete();
            VM2C_SRC_FILE.getParentFile().mkdirs();
            env.log(env.info, "[running 'vm2c @" + argsFile + "'...]");
            env.runCommand("runvm2c", new String[] { "@" + argsFile.getPath() });
            VM2C_SRC_FILE.setReadOnly();
        }
    }

    /**
     * {@inheritDoc}
     */
    public void run(String[] args) {
        CCompiler ccompiler = env.getCCompiler();

        args = runRomizer(args);
        if (noNeedToCompile) {
            return;
        }
        
        JDK jdk = env.getJDK();
        String options;
        if (compilationEnabled) {
        	options = ccompiler.options(false).trim();
        } else {
        	options = "Built via -nocomp, assuming custom cross-compilation";
        }
        createBuildFlagsHeaderFile(options);

        updateVM2CGeneratedFile();

        // Preprocess any files with the ".spp" suffix
        List<File> generatedFiles = new ArrayList<File>();
        FileSet sppFiles = new FileSet(VM_SRC_DIR, new FileSet.SuffixSelector(".spp"));
        for (File sppFile : sppFiles.list()) {
            SppFilePreprocessCommand.preprocess(sppFile, generatedFiles, env.getPreprocessor(), env.getMacroizer(), ccompiler.options.macroize);
        }

        // Run the C compiler to compile the slow VM
        if (compilationEnabled) {

            File[] includeDirs;

            if (ccompiler.isCrossPlatform()) {
                includeDirs = new File[] {VM_SRC_DIR, FP_SRC_DIR,
                            new File(VM_SRC_RTS_DIR, ccompiler.getRtsIncludeName())
                        };
            } else {
                includeDirs = new File[] {new File(jdk.getHome(), "include"),
                            jdk.getJNI_MDIncludePath(),
                            VM_SRC_DIR,
                            FP_SRC_DIR,
                            new File(VM_SRC_RTS_DIR, ccompiler.getRtsIncludeName())
                        };
            }

            Build.mkdir(VM_BLD_DIR);

            List<File> objectFiles = new ArrayList<File>();
            if (ccompiler.options.floatsSupported) {
                /*
                 * The floating point code must be compiled without optimization for the reasons stated in this
                 * excerpt from the KVM porting guide (Squawk's FP implementation is derived from the KVM):
                 *
                 * 10.1.2 Implementing Java virtual machine floating point semantics
                 *
                 *   Many processor architectures natively support IEEE 754 arithmetic on float and
                 *   double formats. Therefore, there is often a straightforward mapping between Java
                 *   virtual machine floating-point operations, C code implementing those operations,
                 *   and floating-point instructions on the underlying processor. However, various
                 *   complications are possible:
                 *
                 *   o The floating-point semantics of the Java virtual machine are tightly specified,
                 *     much more tightly specified than C language floating-point semantics. Therefore,
                 *     a C compiler could perform an  optimization  that was an allowed transformation
                 *     in C but broke Java virtual machine semantics. For example, in the Java virtual
                 *     machine and Java:
                 *
                 *        x + 0.0
                 *
                 *     cannot be replaced with
                 *
                 *        x
                 *
                 *     since different answers can be generated.
                 *
                 *     Therefore, the portions of the KVM that implement Java virtual machine floating-point
                 *     semantics should be compiled without aggressive optimization to help avoid such (in
                 *     this case) unhelpful code transformations. Many C compilers also have separate flags
                 *     affecting floating-point code generation, such as flags to improve floating-point
                 *     consistency and make the generated code have semantics more closely resembling a
                 *     literal translation of the source. Regardless of the processor architecture, using
                 *     such flags might be necessary to implement Java virtual machine semantics in C code.
                 */

                env.log(env.brief, "[compiling floating point sources in " + FP_SRC_DIR + " ...]");
                List<File> sources = new FileSet(FP_SRC_DIR, new FileSet.SuffixSelector(".c")).list();
                for (File source : sources) {
                    objectFiles.add(ccompiler.compile(includeDirs, source, VM_BLD_DIR, true));
                }
            }
            
            
          
            if (ccompiler.options.nativeVerification) {    	   
            	env.log(env.brief, "[compiling native verification sources in " + UTIL_SRC_DIR + " ...]");
                List<File> sources = new FileSet(UTIL_SRC_DIR, new FileSet.SuffixSelector(".c")).list();
                for (File source : sources) {
                    objectFiles.add(ccompiler.compile(includeDirs, source, VM_BLD_DIR, false));
                }
            }
        	
            

            env.log(env.brief, "[compiling '" + VM_SRC_FILE + "' ...]");
            objectFiles.add(ccompiler.compile(includeDirs, VM_SRC_FILE, VM_BLD_DIR, false));

	    if (!bootstrapSuiteName.equals("squawk")) {
		env.log(env.brief, "[linking '" + bootstrapSuiteName + "' ...]");
		ccompiler.link((File[])objectFiles.toArray(new File[objectFiles.size()]), bootstrapSuiteName, env.dll);
	    } else {
		env.log(env.brief, "[not linking squawk]");
	    }

            if (!env.verbose) {
            	for (File file : generatedFiles) {
                    Build.delete(file);
                    Build.clear(VM_BLD_DIR, true);
                }
            }
        }

        // Rebuild the jar of files used by the JVM embedded in Squawk if the bootstrap suite was (re)built
        if (args.length > 0) {
            env.runCommand("squawk.jar", Build.NO_ARGS);
        }
    }
    
    public String[] runRomizer(String... args) {
        bootstrapSuiteName = "squawk";
        // Only run the romizer if there are arguments passed to this command
        if (args.length > 0) {
            CCompiler ccompiler = env.getCCompiler();
            String arch = ccompiler == null ? "X86" : ccompiler.getArchitecture();
            args = convertToRomizerOptions(args, arch);
            env.runCommand("romize", args);
        }
        return args;
    }

    /**
     * Sets the flag enabling the C compilation step.
     *
     * @param  flag    the valur to which the flag should be set (true enables compilation, false disables)
     * @return boolean the previous value of the flag
     */
    public boolean enableCompilation(boolean flag) {
        boolean oldFlag = compilationEnabled;
        compilationEnabled = flag;
        return oldFlag;
    }

    /**
     * {@inheritDoc}
     */
    public void clean() {
        Build.clear(VM_BLD_DIR, true);
        Build.delete(new File(VM_SRC_DIR, "rom.h"));
        Build.delete(new File(VM_SRC_DIR, "buildflags.h"));
        FileSet generatedFiles = new FileSet(VM_SRC_DIR, new FileSet.SuffixSelector(".spp"));
        for (File sppFile : generatedFiles.list()) {
            File outputFile = getFileDerivedFromSppFile(sppFile);
            File preprocessedFile = new File(sppFile.getPath() + ".preprocessed");
            Build.delete(outputFile);
            Build.delete(preprocessedFile);
        }
        Build.delete(VM2C_SRC_FILE);
        if (!env.isJava5SyntaxSupported()) {
            Build.clear(new File("cldc", PREPROCESSED_FOR_VM2C_DIR_NAME), true);
        }
    }
}
