/*
 * This file is part of the LibreOffice project.
 *
 * 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/.
 *
 * This file incorporates work covered by the following license notice:
 *
 *   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 .
 */
package com.sun.star.script.framework.provider.javascript;

import com.sun.star.comp.loader.FactoryHelper;

import com.sun.star.document.XScriptInvocationContext;

import com.sun.star.frame.XModel;

import com.sun.star.lang.XMultiComponentFactory;
import com.sun.star.lang.XMultiServiceFactory;
import com.sun.star.lang.XSingleServiceFactory;

import com.sun.star.reflection.InvocationTargetException;

import com.sun.star.registry.XRegistryKey;

import com.sun.star.script.framework.container.ScriptMetaData;
import com.sun.star.script.framework.log.LogUtils;
import com.sun.star.script.framework.provider.ClassLoaderFactory;
import com.sun.star.script.framework.provider.ScriptContext;
import com.sun.star.script.framework.provider.ScriptEditor;
import com.sun.star.script.framework.provider.ScriptProvider;
import com.sun.star.script.provider.ScriptExceptionRaisedException;
import com.sun.star.script.provider.ScriptFrameworkErrorException;
import com.sun.star.script.provider.ScriptFrameworkErrorType;
import com.sun.star.script.provider.XScript;

import com.sun.star.uno.XComponentContext;

import java.net.URL;

import org.mozilla.javascript.Context;
import org.mozilla.javascript.ImporterTopLevel;
import org.mozilla.javascript.JavaScriptException;
import org.mozilla.javascript.Scriptable;

public class ScriptProviderForJavaScript {

    public static class ScriptProviderForJavaScript_2 extends ScriptProvider {

        public ScriptProviderForJavaScript_2(XComponentContext ctx) {
            super(ctx, "JavaScript");
        }

        @Override
        public XScript getScript(/*IN*/String scriptURI)
        throws com.sun.star.uno.RuntimeException, ScriptFrameworkErrorException {
            ScriptMetaData scriptData = null;

            try {
                scriptData = getScriptData(scriptURI);
                ScriptImpl script = new ScriptImpl(m_xContext, scriptData, m_xModel,
                                                   m_xInvocContext);
                return script;
            } catch (com.sun.star.uno.RuntimeException re) {
                throw new ScriptFrameworkErrorException(
                    "Failed to create script object: " + re.getMessage(),
                    null, scriptData.getLanguageName(), language, ScriptFrameworkErrorType.UNKNOWN);
            }
        }

        @Override
        public boolean hasScriptEditor() {
            return true;
        }

        @Override
        public ScriptEditor getScriptEditor() {
            return ScriptEditorForJavaScript.getEditor();
        }
    }

    /**
     * Returns a factory for creating the service.
     * This method is called by the <code>JavaLoader</code>
     * <p>
     *
     * @param  implName      the name of the implementation for which a service is desired
     * @param  multiFactory  the service manager to be used if needed
     * @param  regKey        the registryKey
     * @return               returns a <code>XSingleServiceFactory</code> for creating
     *                          the component
     * @see                  com.sun.star.comp.loader.JavaLoader
     */
    public static XSingleServiceFactory __getServiceFactory(String implName,
            XMultiServiceFactory multiFactory, XRegistryKey regKey) {

        XSingleServiceFactory xSingleServiceFactory = null;

        if (implName.equals(
                ScriptProviderForJavaScript.ScriptProviderForJavaScript_2.class.getName())) {

            xSingleServiceFactory =
                FactoryHelper.getServiceFactory(
                    ScriptProviderForJavaScript.ScriptProviderForJavaScript_2.class,
                    "com.sun.star.script.provider.ScriptProviderForJavaScript",
                    multiFactory, regKey);

        }

        return xSingleServiceFactory;
    }
}

class ScriptImpl implements XScript {

    private final ScriptMetaData metaData;
    private final XComponentContext m_xContext;
    private XMultiComponentFactory m_xMultiComponentFactory;
    private final XModel m_xModel;
    private final XScriptInvocationContext m_xInvocContext;

    ScriptImpl(XComponentContext ctx, ScriptMetaData metaData, XModel xModel,
               XScriptInvocationContext xInvocContext) throws
        com.sun.star.uno.RuntimeException {

        this.metaData = metaData;
        this.m_xContext = ctx;
        this.m_xModel = xModel;
        this.m_xInvocContext = xInvocContext;

        try {
            this.m_xMultiComponentFactory = m_xContext.getServiceManager();
        } catch (Exception e) {
            throw new com.sun.star.uno.RuntimeException(e);
        }

        LogUtils.DEBUG("ScriptImpl [javascript] script data = " + metaData);
    }

    /**
     *  The invoke method of the ScriptProviderForJavaScript runs the
     *  JavaScript script specified in the URI
     *
     *
     *
     * @param params            All parameters; pure, out params are
     *                          undefined in sequence, i.e., the value
     *                          has to be ignored by the callee
     *
     * @param aOutParamIndex    Out indices
     *
     * @param aOutParam         Out parameters
     *
     * @return                  The value returned from the function
     *                          being invoked
     *
     * @throws ScriptFrameworkErrorException If there is no matching script name
     *
     *
     * @throws InvocationTargetException If the running script throws
     *                                   an exception this information
     *                                   is captured and rethrown as
     *                                   ScriptErrorRaisedException or
     *                                   ScriptExceptionRaisedException
     */
    public Object invoke(
        /*IN*/Object[]  params,
        /*OUT*/short[][]  aOutParamIndex,
        /*OUT*/Object[][]  aOutParam)
    throws ScriptFrameworkErrorException, InvocationTargetException {

        // Initialise the out parameters - not used at the moment
        aOutParamIndex[0] = new short[0];
        aOutParam[0] = new Object[0];

        ClassLoader cl = null;

        try {
            cl = ClassLoaderFactory.getURLClassLoader(metaData);
            metaData.getSourceURL();
        } catch (java.net.MalformedURLException mfu) {
            throw new ScriptFrameworkErrorException(
                mfu.getMessage(), null,
                metaData.getLanguageName(), metaData.getLanguage(),
                ScriptFrameworkErrorType.MALFORMED_URL);
        }

        Context ctxt = null;

        try {
            Object result = null;

            ScriptEditorForJavaScript editor =
                ScriptEditorForJavaScript.getEditor(metaData.getSourceURL());

            if (editor != null) {
                result = editor.execute();

                if (result != null  &&
                    result.getClass().getName().equals("org.mozilla.javascript.Undefined")) {
                    // Always return a string
                    // TODO revisit
                    return Context.toString(result);
                }

            }

            String source;

            if (editor != null && editor.isModified()) {
                LogUtils.DEBUG("GOT A MODIFIED SOURCE");
                source = editor.getText();
            } else {
                metaData.loadSource();
                source =  metaData.getSource();

            }

            if (source == null || source.length() == 0) {
                throw new ScriptFrameworkErrorException(
                    "Failed to read source data for script", null,
                    metaData.getLanguageName(), metaData.getLanguage(),
                    ScriptFrameworkErrorType.UNKNOWN);
            }

            /* Set the context ClassLoader on the current thread to
               be our custom ClassLoader. This is the suggested method
               for setting up a ClassLoader to be used by the Rhino
               interpreter
             */
            if (cl != null) {
                Thread.currentThread().setContextClassLoader(cl);
            }

            // Initialize a Rhino Context object
            ctxt = Context.enter();

            /* The ImporterTopLevel ensures that importClass and
               importPackage statements work in Javascript scripts
               Make the XScriptContext available as a global variable
               to the script
             */
            ImporterTopLevel scope = new ImporterTopLevel(ctxt);

            Scriptable jsCtxt =
                Context.toObject(
                    ScriptContext.createContext(
                        m_xModel, m_xInvocContext, m_xContext,
                        m_xMultiComponentFactory),
                    scope);

            scope.put("XSCRIPTCONTEXT", scope, jsCtxt);

            Scriptable jsArgs = Context.toObject(params, scope);
            scope.put("ARGUMENTS", scope, jsArgs);

            result = ctxt.evaluateString(scope, source, "<stdin>", 1, null);
            result = Context.toString(result);
            return result;
        } catch (JavaScriptException jse) {
            LogUtils.DEBUG("Caught JavaScriptException exception for JavaScript type = "
                           +       jse.getClass());
            String message = jse.getMessage();
            Object wrap = jse.getValue();
            LogUtils.DEBUG("\t message  " + message);
            LogUtils.DEBUG("\t wrapped type " + wrap.getClass());
            LogUtils.DEBUG("\t wrapped toString  " + wrap.toString());
            ScriptExceptionRaisedException se = new
            ScriptExceptionRaisedException(message);
            se.lineNum = -1;
            se.language = "JavaScript";
            se.scriptName = metaData.getLanguageName();
            se.exceptionType = wrap.getClass().getName();
            se.language = metaData.getLanguage();
            LogUtils.DEBUG("ExceptionRaised exception  ");
            LogUtils.DEBUG("\t message  " + se.getMessage());
            LogUtils.DEBUG("\t lineNum  " + se.lineNum);
            LogUtils.DEBUG("\t language  " + se.language);
            LogUtils.DEBUG("\t scriptName  " + se.scriptName);
            raiseEditor(se.lineNum);
            throw new InvocationTargetException(
                "JavaScript uncaught exception" + metaData.getLanguageName(), null, se);
        } catch (Exception ex) {
            LogUtils.DEBUG("Caught Exception " + ex);
            LogUtils.DEBUG("rethrowing as ScriptFramework error");
            throw new ScriptFrameworkErrorException(
                ex.getMessage(), null, metaData.getLanguageName(),
                metaData.getLanguage(), ScriptFrameworkErrorType.UNKNOWN);
        } finally {
            if (ctxt != null) {
                Context.exit();
            }
        }
    }

    private void raiseEditor(int lineNum) {
        try {
            URL sourceUrl = metaData.getSourceURL();

            ScriptEditorForJavaScript editor =
                ScriptEditorForJavaScript.getEditor(sourceUrl);

            if (editor == null) {
                editor = ScriptEditorForJavaScript.getEditor();

                editor.edit(
                    ScriptContext.createContext(m_xModel, m_xInvocContext,
                                                m_xContext, m_xMultiComponentFactory),
                    metaData);

                editor = ScriptEditorForJavaScript.getEditor(sourceUrl);
            }

            if (editor != null) {
                System.out.println("** Have raised IDE for JavaScript, calling indicateErrorLine for line "
                                   + lineNum);
                editor.indicateErrorLine(lineNum);
            }
        } catch (Exception ignore) {
        }
    }
}