/* -*- Mode: java; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*-
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

package org.mozilla.javascript.optimizer;

import org.mozilla.javascript.CompilerEnvirons;
import org.mozilla.javascript.IRFactory;
import org.mozilla.javascript.JavaAdapter;
import org.mozilla.javascript.ObjToIntMap;
import org.mozilla.javascript.Parser;
import org.mozilla.javascript.ScriptRuntime;
import org.mozilla.javascript.ast.AstRoot;
import org.mozilla.javascript.ast.FunctionNode;
import org.mozilla.javascript.ast.ScriptNode;

/**
 * Generates class files from script sources.
 *
 * since 1.5 Release 5
 * @author Igor Bukanov
 */

public class ClassCompiler
{
    /**
     * Construct ClassCompiler that uses the specified compiler environment
     * when generating classes.
     */
    public ClassCompiler(CompilerEnvirons compilerEnv)
    {
        if (compilerEnv == null) throw new IllegalArgumentException();
        this.compilerEnv = compilerEnv;
        this.mainMethodClassName = Codegen.DEFAULT_MAIN_METHOD_CLASS;
    }

    /**
     * Set the class name to use for main method implementation.
     * The class must have a method matching
     * <code>public static void main(Script sc, String[] args)</code>, it will be
     * called when <code>main(String[] args)</code> is called in the generated
     * class. The class name should be fully qulified name and include the
     * package name like in <code>org.foo.Bar</code>.
     */
    public void setMainMethodClass(String className)
    {
        // XXX Should this check for a valid class name?
        mainMethodClassName = className;
    }

    /**
     * Get the name of the class for main method implementation.
     * @see #setMainMethodClass(String)
     */
    public String getMainMethodClass()
    {
        return mainMethodClassName;
    }

    /**
     * Get the compiler environment the compiler uses.
     */
    public CompilerEnvirons getCompilerEnv()
    {
        return compilerEnv;
    }

    /**
     * Get the class that the generated target will extend.
     */
    public Class<?> getTargetExtends()
    {
        return targetExtends;
    }

    /**
     * Set the class that the generated target will extend.
     *
     * @param extendsClass the class it extends
     */
    public void setTargetExtends(Class<?> extendsClass)
    {
        targetExtends = extendsClass;
    }

    /**
     * Get the interfaces that the generated target will implement.
     */
    public Class<?>[] getTargetImplements()
    {
        return targetImplements == null ? null : (Class[])targetImplements.clone();
    }

    /**
     * Set the interfaces that the generated target will implement.
     *
     * @param implementsClasses an array of Class objects, one for each
     *                          interface the target will extend
     */
    public void setTargetImplements(Class<?>[] implementsClasses)
    {
        targetImplements = implementsClasses == null ? null : (Class[])implementsClasses.clone();
    }

    /**
     * Build class name for a auxiliary class generated by compiler.
     * If the compiler needs to generate extra classes beyond the main class,
     * it will call this function to build the auxiliary class name.
     * The default implementation simply appends auxMarker to mainClassName
     * but this can be overridden.
     */
    protected String makeAuxiliaryClassName(String mainClassName,
                                            String auxMarker)
    {
        return mainClassName+auxMarker;
    }

    /**
     * Compile JavaScript source into one or more Java class files.
     * The first compiled class will have name mainClassName.
     * If the results of {@link #getTargetExtends()} or
     * {@link #getTargetImplements()} are not null, then the first compiled
     * class will extend the specified super class and implement
     * specified interfaces.
     *
     * @return array where elements with even indexes specifies class name
     *         and the following odd index gives class file body as byte[]
     *         array. The initial element of the array always holds
     *         mainClassName and array[1] holds its byte code.
     */
    public Object[] compileToClassFiles(String source,
                                        String sourceLocation,
                                        int lineno,
                                        String mainClassName)
    {
        Parser p = new Parser(compilerEnv);
        AstRoot ast = p.parse(source, sourceLocation, lineno);
        IRFactory irf = new IRFactory(compilerEnv);
        ScriptNode tree = irf.transformTree(ast);

        // release reference to original parse tree & parser
        irf = null;
        ast = null;
        p = null;

        Class<?> superClass = getTargetExtends();
        Class<?>[] interfaces = getTargetImplements();
        String scriptClassName;
        boolean isPrimary = (interfaces == null && superClass == null);
        if (isPrimary) {
            scriptClassName = mainClassName;
        } else {
            scriptClassName = makeAuxiliaryClassName(mainClassName, "1");
        }

        Codegen codegen = new Codegen();
        codegen.setMainMethodClass(mainMethodClassName);
        byte[] scriptClassBytes
            = codegen.compileToClassFile(compilerEnv, scriptClassName,
                                         tree, tree.getEncodedSource(),
                                         false);

        if (isPrimary) {
            return new Object[] { scriptClassName, scriptClassBytes };
        }
        int functionCount = tree.getFunctionCount();
        ObjToIntMap functionNames = new ObjToIntMap(functionCount);
        for (int i = 0; i != functionCount; ++i) {
            FunctionNode ofn = tree.getFunctionNode(i);
            String name = ofn.getName();
            if (name != null && name.length() != 0) {
                functionNames.put(name, ofn.getParamCount());
            }
        }
        if (superClass == null) {
            superClass = ScriptRuntime.ObjectClass;
        }
        byte[] mainClassBytes
            = JavaAdapter.createAdapterCode(
                functionNames, mainClassName,
                superClass, interfaces, scriptClassName);

        return new Object[] { mainClassName, mainClassBytes,
                              scriptClassName, scriptClassBytes };
    }

    private String mainMethodClassName;
    private CompilerEnvirons compilerEnv;
    private Class<?> targetExtends;
    private Class<?>[] targetImplements;

}

