/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.commons.jci.compilers;

import java.io.ByteArrayInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

import org.apache.commons.jci.problems.CompilationProblem;
import org.apache.commons.jci.readers.ResourceReader;
import org.apache.commons.jci.stores.ResourceStore;
import org.apache.commons.jci.utils.ConversionUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.mozilla.javascript.CompilerEnvirons;
import org.mozilla.javascript.Context;
import org.mozilla.javascript.ErrorReporter;
import org.mozilla.javascript.EvaluatorException;
import org.mozilla.javascript.GeneratedClassLoader;
import org.mozilla.javascript.ImporterTopLevel;
import org.mozilla.javascript.JavaScriptException;
import org.mozilla.javascript.NativeArray;
import org.mozilla.javascript.Scriptable;
import org.mozilla.javascript.ScriptableObject;
import org.mozilla.javascript.optimizer.ClassCompiler;

/**
 * @author tcurdt
 */
public final class RhinoJavaCompiler extends AbstractJavaCompiler {

    private final Log log = LogFactory.getLog(RhinoJavaCompiler.class);

    private final JavaCompilerSettings defaultSettings;
    
    
    public RhinoJavaCompiler() {
        defaultSettings = new RhinoJavaCompilerSettings();
    }
    
    /**
     * based on code from dev.helma.org
     * http://dev.helma.org/source/file/helma/branches/rhinoloader/src/org/helma/javascript/RhinoLoader.java/?revision=95
     */
    private final class RhinoCompilingClassLoader extends ClassLoader {

        private final ScriptableObject scope;
        private final ResourceReader reader;
        private final ResourceStore store;

        private final Collection<CompilationProblem> problems = new ArrayList<CompilationProblem>();
        
        private final class ProblemCollector implements ErrorReporter {

            public void error(String pMessage, String pFileName, int pLine, String pScript, int pColumn) {

                final CompilationProblem problem = new RhinoCompilationProblem(pMessage, pFileName, pLine, pScript, pColumn, true); 

                if (problemHandler != null) {
                    problemHandler.handle(problem);
                }

                problems.add(problem); 
            }

            public void warning(String pMessage, String pFileName, int pLine, String pScript, int pColumn) {

                final CompilationProblem problem = new RhinoCompilationProblem(pMessage, pFileName, pLine, pScript, pColumn, false); 

                if (problemHandler != null) {
                    problemHandler.handle(problem);
                }

                problems.add(problem); 
            }

            public EvaluatorException runtimeError(String pMessage, String pFileName, int pLine, String pScript, int pColumn) {
                return new EvaluatorException(pMessage, pFileName, pLine, pScript, pColumn);
            }
        }

        public RhinoCompilingClassLoader( final ResourceReader pReader, final ResourceStore pStore, final ClassLoader pClassLoader) {
            super(pClassLoader);

            reader = pReader;
            store = pStore;

            final Context context = Context.enter();
            scope = new ImporterTopLevel(context);
            Context.exit();
        }

        public Collection<CompilationProblem> getProblems() {
            return problems;
        }

        @Override
        protected Class<?> findClass( final String pName ) throws ClassNotFoundException {
            final Context context = Context.enter();
            context.setErrorReporter(new ProblemCollector());

            try {
                return compileClass(context, pName);
            } catch( EvaluatorException e ) {
                throw new ClassNotFoundException(e.getMessage(), e);
            } catch (IOException e) {
                throw new ClassNotFoundException(e.getMessage(), e);
            } finally {
                Context.exit();
            }
        }


        private Class<?> compileClass( final Context pContext, final String pClassName) throws IOException, ClassNotFoundException {

            Class<?> superclass = null;

            final String pSourceName = pClassName.replace('.', '/') + ".js";

            final Scriptable target = evaluate(pContext, pSourceName);

            final Object baseClassName = ScriptableObject.getProperty(target, "__extends__");

            if (baseClassName instanceof String) {
                superclass = Class.forName((String) baseClassName);
            }

            final List<Class<?>> interfaceClasses = new ArrayList<Class<?>>();

            final Object interfaceNames = ScriptableObject.getProperty(target, "__implements__");

            if (interfaceNames instanceof NativeArray) {

                final NativeArray interfaceNameArray = (NativeArray) interfaceNames;

                for (int i=0; i<interfaceNameArray.getLength(); i++) {

                    final Object obj = interfaceNameArray.get(i, interfaceNameArray);

                    if (obj instanceof String) {
                        interfaceClasses.add(Class.forName((String) obj));
                    }
                }

            } else if (interfaceNames instanceof String) {

                interfaceClasses.add(Class.forName((String) interfaceNames));

            }

            final Class<?>[] interfaces;

            if (!interfaceClasses.isEmpty()) {
                interfaces = new Class[interfaceClasses.size()];
                interfaceClasses.toArray(interfaces);
            } else {
                // FIXME: hm ...really no empty array good enough?
                interfaces = null;
            }

            return compileClass(pContext, pSourceName, pClassName, superclass, interfaces);

        }


        private Class<?> compileClass( final Context pContext, final String pSourceName, final String pClassName, final Class<?> pSuperClass, final Class<?>[] pInterfaces) {

            final CompilerEnvirons environments = new CompilerEnvirons();
            environments.initFromContext(pContext);
            final ClassCompiler compiler = new ClassCompiler(environments);

            if (pSuperClass != null) {
                compiler.setTargetExtends(pSuperClass);
            }

            if (pInterfaces != null) {
                compiler.setTargetImplements(pInterfaces);
            }

            final byte[] sourceBytes = reader.getBytes(pSourceName);

            final Object[] classes = compiler.compileToClassFiles(new String(sourceBytes), getName(pSourceName), 1, pClassName);

            final GeneratedClassLoader loader = pContext.createClassLoader(pContext.getApplicationClassLoader());

            Class<?> clazz = null;

            for (int i = 0; i < classes.length; i += 2) {

                final String clazzName = (String) classes[i];
                final byte[] clazzBytes = (byte[]) classes[i+1];

                store.write(clazzName.replace('.', '/') + ".class", clazzBytes);

                Class<?> c = loader.defineClass(clazzName, clazzBytes);
                loader.linkClass(c);

                if (i == 0) {
                    clazz = c;
                }

            }

            return clazz;
        }

        private String getName(String s) {
            final int i = s.lastIndexOf('/');
            if (i < 0) {
                return s;
            }

            return s.substring(i + 1);
        }

        private Scriptable evaluate( final Context pContext, final String pSourceName) throws JavaScriptException, IOException {

            if (!reader.isAvailable(pSourceName)) {
                throw new FileNotFoundException("File " + pSourceName + " not found");
            }

            final Scriptable target = pContext.newObject(scope);

            final byte[] sourceBytes = reader.getBytes(pSourceName);

            final Reader reader = new InputStreamReader(new ByteArrayInputStream(sourceBytes));

            pContext.evaluateReader(target, reader, getName(pSourceName), 1, null);

            return target;
        }

    }
    
    
    public CompilationResult compile( final String[] pResourcePaths, final ResourceReader pReader, final ResourceStore pStore, final ClassLoader pClassLoader, final JavaCompilerSettings pSettings ) {

        final RhinoCompilingClassLoader cl = new RhinoCompilingClassLoader(pReader, pStore, pClassLoader);

        for (int i = 0; i < pResourcePaths.length; i++) {
            log.debug("compiling " + pResourcePaths[i]);
            
            final String clazzName = ConversionUtils.convertResourceToClassName(pResourcePaths[i]);
            try {
                cl.loadClass(clazzName);
            } catch (ClassNotFoundException e) {
            }
        }

        final Collection<CompilationProblem> problems = cl.getProblems();
        final CompilationProblem[] result = new CompilationProblem[problems.size()];
        problems.toArray(result);
        return new CompilationResult(result);
    }


    public JavaCompilerSettings createDefaultSettings() {
        return defaultSettings;
    }

}
