/* -*- 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 java.util.HashMap;
import org.mozilla.javascript.CompilerEnvirons;
import org.mozilla.javascript.IRFactory;
import org.mozilla.javascript.JavaAdapter;
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.
 *
 * <p>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 qualified 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 : 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 : 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, source);
        ScriptNode tree = irf.transformTree(ast);

        if (compilerEnv.isGeneratingSource()) {
            tree.setRawSource(source);
            tree.setRawSourceBounds(0, source.length());
        }

        // 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, source, false);

        if (isPrimary) {
            return new Object[] {scriptClassName, scriptClassBytes};
        }
        int functionCount = tree.getFunctionCount();
        HashMap<String, Integer> functionNames = new HashMap<>();
        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;
}
