/*
 * 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;

import com.sun.star.beans.Property;
import com.sun.star.beans.XIntrospectionAccess;
import com.sun.star.beans.XPropertyChangeListener;
import com.sun.star.beans.XPropertySet;
import com.sun.star.beans.XPropertySetInfo;
import com.sun.star.beans.XVetoableChangeListener;

import com.sun.star.container.XNameContainer;

import com.sun.star.deployment.XPackage;

import com.sun.star.document.XScriptInvocationContext;

import com.sun.star.frame.XModel;
import com.sun.star.frame.XTransientDocumentsDocumentContentFactory;

import com.sun.star.lang.XInitialization;
import com.sun.star.lang.XMultiComponentFactory;
import com.sun.star.lang.XServiceInfo;
import com.sun.star.lang.XTypeProvider;

import com.sun.star.script.XInvocation;
import com.sun.star.script.browse.BrowseNodeTypes;
import com.sun.star.script.browse.XBrowseNode;
import com.sun.star.script.framework.browse.DialogFactory;
import com.sun.star.script.framework.browse.ProviderBrowseNode;
import com.sun.star.script.framework.container.ParcelContainer;
import com.sun.star.script.framework.container.ParsedScriptUri;
import com.sun.star.script.framework.container.ScriptMetaData;
import com.sun.star.script.framework.container.UnoPkgContainer;
import com.sun.star.script.framework.container.XMLParserFactory;
import com.sun.star.script.framework.log.LogUtils;
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.script.provider.XScriptContext;
import com.sun.star.script.provider.XScriptProvider;

import com.sun.star.sdbc.XRow;

import com.sun.star.ucb.Command;
import com.sun.star.ucb.UniversalContentBroker;
import com.sun.star.ucb.XCommandProcessor;
import com.sun.star.ucb.XContent;
import com.sun.star.ucb.XContentIdentifier;
import com.sun.star.ucb.XUniversalContentBroker;

import com.sun.star.uno.AnyConverter;
import com.sun.star.uno.Exception;
import com.sun.star.uno.Type;
import com.sun.star.uno.TypeClass;
import com.sun.star.uno.UnoRuntime;
import com.sun.star.uno.XComponentContext;

import com.sun.star.util.XMacroExpander;

public abstract class ScriptProvider implements
    XScriptProvider, XBrowseNode, XPropertySet, XInvocation, XInitialization,
    XTypeProvider, XServiceInfo, XNameContainer {

    private final String[] __serviceNames = {
        "com.sun.star.script.provider.ScriptProviderFor",
        "com.sun.star.script.provider.LanguageScriptProvider",
        "com.sun.star.script.browse.BrowseNode",
        "com.sun.star.script.provider.ScriptProvider"
    };

    protected String language;

    protected XComponentContext m_xContext;
    private XMultiComponentFactory m_xMultiComponentFactory;
    protected XModel m_xModel;
    protected XScriptInvocationContext m_xInvocContext;
    private ParcelContainer m_container;

    // proxies to helper objects which implement interfaces
    private XPropertySet m_xPropertySetProxy;
    private XInvocation m_xInvocationProxy;

    // TODO should this be implemented in this class
    private XBrowseNode m_xBrowseNodeProxy;
    private XScriptContext m_xScriptContext;

    public ScriptProvider(XComponentContext ctx, String language) {
        this.language = language;
        __serviceNames[0] += language;

        LogUtils.DEBUG("ScriptProvider: constructor - start. " + language);

        m_xContext = ctx;

        // Initialize DialogFactory class in case dialogs are required
        DialogFactory.createDialogFactory(m_xContext);

        try {
            m_xMultiComponentFactory = m_xContext.getServiceManager();

            if (m_xMultiComponentFactory == null) {
                throw new Exception("Error could not obtain a " +
                                    "multicomponent factory - rethrowing Exception.");
            }

            Object serviceObj =
                m_xContext.getValueByName(
                    "/singletons/com.sun.star.util.theMacroExpander");

            XMacroExpander me =
                (XMacroExpander) AnyConverter.toObject(
                    new Type(XMacroExpander.class), serviceObj);

            XMLParserFactory.setOfficeDTDURL(
                me.expandMacros(
                    "$BRAND_BASE_DIR/$BRAND_SHARE_SUBDIR/dtd/officedocument/1_0/"));

        } catch (Exception e) {
            LogUtils.DEBUG(LogUtils.getTrace(e));
            throw new com.sun.star.uno.RuntimeException(
                e, "Error constructing  ScriptProvider: " + e);
        }

        LogUtils.DEBUG("ScriptProvider: constructor - finished.");
    }

    synchronized public XScriptContext getScriptingContext() {
        if (m_xScriptContext == null) {
            m_xScriptContext = ScriptContext.createContext(m_xModel, m_xInvocContext,
                               m_xContext, m_xMultiComponentFactory);
        }

        return m_xScriptContext;
    }

    public void initialize(Object[] aArguments)
    throws com.sun.star.uno.Exception {
        LogUtils.DEBUG("entering XInit for language " + language);
        boolean isPkgProvider = false;

        if (aArguments.length == 1) {
            String contextUrl = null;

            if (AnyConverter.getType(aArguments[0]).getTypeClass().equals(
                    TypeClass.INTERFACE)) {
                // try whether it denotes a XScriptInvocationContext
                m_xInvocContext =
                    UnoRuntime.queryInterface(XScriptInvocationContext.class, aArguments[0]);

                if (m_xInvocContext != null) {
                    // if so, obtain the document - by definition, this must be
                    // the ScriptContainer
                    m_xModel =
                        UnoRuntime.queryInterface(XModel.class, m_xInvocContext.getScriptContainer());
                } else {
                    // otherwise, check whether it's an XModel
                    m_xModel =
                        UnoRuntime.queryInterface(XModel.class, aArguments[0]);
                }

                if (m_xModel == null) {
                    throw new com.sun.star.uno.Exception(
                        "ScriptProvider argument must be either a string, a valid XScriptInvocationContext, "
                        + "or an XModel", this);
                }

                contextUrl = getDocUrlFromModel(m_xModel);
                m_container = new ParcelContainer(m_xContext, contextUrl, language);

            } else if (AnyConverter.isString(aArguments[0])) {

                String originalContextURL = AnyConverter.toString(aArguments[0]);
                LogUtils.DEBUG("creating Application, path: " + originalContextURL);
                contextUrl = originalContextURL;

                // TODO no support for packages in documents yet
                if (originalContextURL.startsWith("vnd.sun.star.tdoc")) {
                    m_container = new ParcelContainer(m_xContext, contextUrl, language);
                    m_xModel = getModelFromDocUrl(originalContextURL);
                } else {
                    String extensionDb = "vnd.sun.star.expand:${$BRAND_INI_DIR/"
                                   + PathUtils.BOOTSTRAP_NAME + "::UserInstallation}/user";

                    String extensionRepository = null;

                    if (originalContextURL.startsWith("bundled")) {
                        contextUrl = "vnd.sun.star.expand:$BUNDLED_EXTENSIONS";
                        extensionRepository = "bundled";
                    } else if (originalContextURL.startsWith("share")) {
                        contextUrl = "vnd.sun.star.expand:$BRAND_BASE_DIR/$BRAND_SHARE_SUBDIR";
                        extensionRepository = "shared";
                    } else if (originalContextURL.startsWith("user")) {
                        contextUrl = "vnd.sun.star.expand:${$BRAND_INI_DIR/"
                                     + PathUtils.BOOTSTRAP_NAME + "::UserInstallation}/user";
                        extensionRepository = "user";
                    }

                    if (originalContextURL.endsWith("uno_packages")) {
                        isPkgProvider = true;

                        if (!originalContextURL.equals(contextUrl)
                            && extensionRepository != null
                            && !extensionRepository.equals("bundled")) {

                            contextUrl = PathUtils.make_url(contextUrl, "uno_packages");
                        }
                    }

                    if (isPkgProvider) {
                        m_container =
                            new UnoPkgContainer(m_xContext, contextUrl, extensionDb, extensionRepository,
                                                language);
                    } else {
                        m_container =
                            new ParcelContainer(m_xContext, contextUrl, language);
                    }
                }
            } else {
                throw new com.sun.star.uno.RuntimeException(
                    "ScriptProvider created with invalid argument");
            }

            LogUtils.DEBUG("Modified Application path is: " + contextUrl);
            LogUtils.DEBUG("isPkgProvider is: " + isPkgProvider);

            // TODO should all be done in this class instead of
            // delegation????
            m_xBrowseNodeProxy =
                new ProviderBrowseNode(this, m_container, m_xContext);

            m_xInvocationProxy =
                UnoRuntime.queryInterface(XInvocation.class, m_xBrowseNodeProxy);
            m_xPropertySetProxy =
                UnoRuntime.queryInterface(XPropertySet.class, m_xBrowseNodeProxy);
        } else {
            // this is ok, for example when executing a script from the
            // command line
            LogUtils.DEBUG("ScriptProviderFor" + language +
                           " initialized without a context");
        }

        LogUtils.DEBUG("leaving XInit");
    }

    /**
     *  Gets the types attribute of the ScriptProvider object
     *
     * @return    The types value
     */
    public com.sun.star.uno.Type[] getTypes() {
        Type[] retValue = new Type[ 8 ];
        retValue[ 0 ] = new Type(XScriptProvider.class);
        retValue[ 1 ] = new Type(XBrowseNode.class);
        retValue[ 2 ] = new Type(XInitialization.class);
        retValue[ 3 ] = new Type(XTypeProvider.class);
        retValue[ 4 ] = new Type(XServiceInfo.class);
        retValue[ 5 ] = new Type(XPropertySet.class);
        retValue[ 6 ] = new Type(XInvocation.class);
        retValue[ 7 ] = new Type(com.sun.star.container.XNameContainer.class);

        return retValue;
    }

    /**
     *  Gets the implementationId attribute of the ScriptProvider object
     *
     * @return    The implementationId value
     */
    public byte[] getImplementationId() {
        return new byte[0];
    }

    /**
     *  Gets the implementationName attribute of the ScriptProvider object
     *
     * @return    The implementationName value
     */
    public String getImplementationName() {
        return getClass().getName();
    }

    /**
     *  Description of the Method
     *
     * @param  serviceName  Description of the Parameter
     * @return              Description of the Return Value
     */
    public boolean supportsService(String serviceName) {
        for (int index = __serviceNames.length; index-- > 0;) {
            if (serviceName.equals(__serviceNames[ index ])) {
                return true;
            }
        }

        return false;
    }

    /**
     *  Gets the supportedServiceNames attribute of the ScriptProvider object
     *
     * @return    The supportedServiceNames value
     */
    public String[] getSupportedServiceNames() {
        return __serviceNames;
    }

    public abstract XScript getScript(/*IN*/String scriptURI) throws
        com.sun.star.uno.RuntimeException, ScriptFrameworkErrorException;

    // TODO need to encapsulate this better,
    // Some factory concept for creating/accessing Editor
    // How this is passed down or how it is accessible by BrowseNode
    // implementations needs thinking about
    // This method is used to determine whether the ScriptProvider
    // has a ScriptEditor
    public abstract boolean hasScriptEditor();

    // TODO see above
    // This method is used to get the ScriptEditor for this ScriptProvider
    public abstract ScriptEditor getScriptEditor();

    public ScriptMetaData  getScriptData(/*IN*/String scriptURI) throws
        ScriptFrameworkErrorException {

        try {
            ParsedScriptUri details = m_container.parseScriptUri(scriptURI);

            try {
                ScriptMetaData scriptData = m_container.findScript(details);

                if (scriptData == null) {
                    throw new ScriptFrameworkErrorException(
                        details.function + " does not exist", null, details.function, language,
                        ScriptFrameworkErrorType.NO_SUCH_SCRIPT);
                }

                return scriptData;
            } catch (com.sun.star.container.NoSuchElementException nse) {
                ScriptFrameworkErrorException e2 =
                    new ScriptFrameworkErrorException(nse.getMessage(), null, details.function,
                                                      language, ScriptFrameworkErrorType.NO_SUCH_SCRIPT);
                e2.initCause(nse);
                throw e2;
            } catch (com.sun.star.lang.WrappedTargetException wta) {
                // TODO specify the correct error Type
                java.lang.Exception wrapped = (java.lang.Exception) wta.TargetException;
                String message = wta.getMessage();

                if (wrapped != null) {
                    message = wrapped.getMessage();
                }

                ScriptFrameworkErrorException e2 =
                    new ScriptFrameworkErrorException(message, null, details.function, language,
                                                      ScriptFrameworkErrorType.UNKNOWN);
                e2.initCause(wta);
                throw e2;
            }
        } catch (com.sun.star.lang.IllegalArgumentException ila) {
            // TODO specify the correct error Type
            ScriptFrameworkErrorException e2
                = new ScriptFrameworkErrorException(
                ila.getMessage(), null, scriptURI, language,
                ScriptFrameworkErrorType.UNKNOWN);
            e2.initCause(ila);
            throw e2;
        }
    }

    // Implementation of XBrowseNode interface
    public String getName() {
        return language;
    }

    public XBrowseNode[] getChildNodes() {
        if (m_xBrowseNodeProxy  == null) {
            LogUtils.DEBUG("No Nodes available ");
            return new XBrowseNode[0];
        }

        return m_xBrowseNodeProxy .getChildNodes();
    }

    public boolean hasChildNodes() {
        if (m_xBrowseNodeProxy == null) {
            LogUtils.DEBUG("No Nodes available ");
            return false;
        }

        return  m_xBrowseNodeProxy.hasChildNodes();
    }

    public short getType() {
        return BrowseNodeTypes.CONTAINER;
    }

    @Override
    public String toString() {
        return getName();
    }

    // implementation of XInvocation interface
    public XIntrospectionAccess getIntrospection() {
        return m_xInvocationProxy.getIntrospection();
    }

    public Object invoke(String aFunctionName, Object[] aParams,
                         short[][] aOutParamIndex, Object[][] aOutParam) throws
        com.sun.star.lang.IllegalArgumentException,
        com.sun.star.script.CannotConvertException,
        com.sun.star.reflection.InvocationTargetException {

        return m_xInvocationProxy.invoke(
                   aFunctionName, aParams, aOutParamIndex, aOutParam);
    }

    public void setValue(String aPropertyName, Object aValue) throws
        com.sun.star.beans.UnknownPropertyException,
        com.sun.star.script.CannotConvertException,
        com.sun.star.reflection.InvocationTargetException {

        m_xInvocationProxy.setValue(aPropertyName, aValue);
    }

    public Object getValue(String aPropertyName) throws
        com.sun.star.beans.UnknownPropertyException {
        return m_xInvocationProxy.getValue(aPropertyName);
    }

    public boolean hasMethod(String aName) {
        return m_xInvocationProxy.hasMethod(aName);
    }

    public boolean hasProperty(String aName) {
        return m_xInvocationProxy.hasProperty(aName);
    }

    public XPropertySetInfo getPropertySetInfo() {
        return m_xPropertySetProxy.getPropertySetInfo();
    }

    public void setPropertyValue(String aPropertyName, Object aValue) throws
        com.sun.star.beans.UnknownPropertyException,
        com.sun.star.beans.PropertyVetoException,
        com.sun.star.lang.IllegalArgumentException,
        com.sun.star.lang.WrappedTargetException {

        m_xPropertySetProxy.setPropertyValue(aPropertyName, aValue);
    }

    public Object getPropertyValue(String PropertyName) throws
        com.sun.star.beans.UnknownPropertyException,
        com.sun.star.lang.WrappedTargetException {

        return m_xPropertySetProxy.getPropertyValue(PropertyName);
    }

    public void addPropertyChangeListener(String aPropertyName,
                                          XPropertyChangeListener xListener) throws
        com.sun.star.beans.UnknownPropertyException,
        com.sun.star.lang.WrappedTargetException {

        m_xPropertySetProxy.addPropertyChangeListener(aPropertyName, xListener);
    }

    public void removePropertyChangeListener(
        String aPropertyName, XPropertyChangeListener aListener) throws
        com.sun.star.beans.UnknownPropertyException,
        com.sun.star.lang.WrappedTargetException {

        m_xPropertySetProxy.removePropertyChangeListener(
            aPropertyName, aListener);
    }

    public void addVetoableChangeListener(
        String PropertyName, XVetoableChangeListener aListener) throws
        com.sun.star.beans.UnknownPropertyException,
        com.sun.star.lang.WrappedTargetException {

        m_xPropertySetProxy.addVetoableChangeListener(PropertyName, aListener);
    }

    public void removeVetoableChangeListener(String PropertyName,
            XVetoableChangeListener aListener) throws
        com.sun.star.beans.UnknownPropertyException,
        com.sun.star.lang.WrappedTargetException {

        m_xPropertySetProxy.removeVetoableChangeListener(
            PropertyName, aListener);

    }
    public java.lang.Object getByName(String aName) throws
        com.sun.star.container.NoSuchElementException,
        com.sun.star.lang.WrappedTargetException {

        // TODO needs implementing?
        throw new com.sun.star.uno.RuntimeException("getByName not implemented");
    }

    public String[] getElementNames() {
        // TODO needs implementing?
        throw new com.sun.star.uno.RuntimeException("getElementNames not implemented");
    }

    // Performs the getRegStatus functionality for the PkgMgr
    public boolean hasByName(String aName) {
        return ((UnoPkgContainer)m_container).hasRegisteredUnoPkgContainer(aName);
    }

    public com.sun.star.uno.Type getElementType() {
        // TODO at the moment this returns void indicating
        // type is unknown should indicate XPackage ? do we implement XPackage
        return new Type();
    }

    public boolean hasElements() {
        // TODO needs implementing?
        throw new com.sun.star.uno.RuntimeException("hasElements not implemented");
    }
    public void replaceByName(String aName, java.lang.Object aElement) throws
        com.sun.star.lang.IllegalArgumentException,
        com.sun.star.container.NoSuchElementException,
        com.sun.star.lang.WrappedTargetException {

        // TODO needs implementing
        throw new com.sun.star.uno.RuntimeException("replaceByName not implemented");
    }

    public void insertByName(String aName, java.lang.Object aElement) throws
        com.sun.star.lang.IllegalArgumentException,
        com.sun.star.container.ElementExistException,
        com.sun.star.lang.WrappedTargetException {

        LogUtils.DEBUG("Provider for " + language + " received register for package "
                       +                       aName);

        XPackage newPackage = UnoRuntime.queryInterface(XPackage.class, aElement);

        if (aName == null || aName.length() == 0) {
            throw new  com.sun.star.lang.IllegalArgumentException("Empty name");
        }

        if (newPackage == null) {
            throw new com.sun.star.lang.IllegalArgumentException("No package supplied");
        }

        ((UnoPkgContainer)m_container).processUnoPackage(newPackage, language);
    }

    // de-register for library only !!
    public void removeByName(String Name) throws
        com.sun.star.container.NoSuchElementException,
        com.sun.star.lang.WrappedTargetException {

        LogUtils.DEBUG("In ScriptProvider.removeByName() for " + Name
                       +                       " this provider = " + language);

        ParcelContainer c =
            ((UnoPkgContainer)m_container).getRegisteredUnoPkgContainer(
                Name);

        if (c != null) {
            String libName;

            if (Name.endsWith("/")) {
                String tmp = Name.substring(0, Name.lastIndexOf('/'));
                libName = tmp.substring(tmp.lastIndexOf('/') + 1);
            } else {
                libName = Name.substring(Name.lastIndexOf('/') + 1);
            }

            LogUtils.DEBUG("Deregistering library " + libName);

            if (c.removeParcel(libName)) {
                ((UnoPkgContainer)m_container).deRegisterPackageContainer(Name);
            } else {
                throw new com.sun.star.container.NoSuchElementException(
                    libName + " cannot be removed from container.");
            }
        } else {
            throw new com.sun.star.container.NoSuchElementException(
                Name + " doesn't exist for " + language);
        }

        // TODO see if we want to remove the ParcelContainer is no Parcels/Libraries left
    }

    private String getDocUrlFromModel(XModel document) {
        XTransientDocumentsDocumentContentFactory factory = null;

        try {

            factory = UnoRuntime.queryInterface(
                          XTransientDocumentsDocumentContentFactory.class,
                          m_xMultiComponentFactory.createInstanceWithContext(
                              "com.sun.star.frame.TransientDocumentsDocumentContentFactory",
                              m_xContext));

        } catch (Exception ex) {
        }

        if (factory == null)
            throw new com.sun.star.uno.RuntimeException(
                "ScriptProvider: unable to create a TDOC context factory.", this);

        try {
            XContent content = factory.createDocumentContent(document);
            return content.getIdentifier().getContentIdentifier();
        } catch (com.sun.star.lang.IllegalArgumentException ex) {
        }

        LogUtils.DEBUG("unable to determine the model's TDOC URL");
        return "";
    }

    private  XModel getModelFromDocUrl(String docUrl) {

        LogUtils.DEBUG("getModelFromDocUrl - searching for match for ->"
                       + docUrl + "<-");

        XModel xModel = null;

        try {
            XUniversalContentBroker ucb = UniversalContentBroker.create(m_xContext);

            XContentIdentifier xCntId = ucb.createContentIdentifier(docUrl);

            XContent xCnt = ucb.queryContent(xCntId);

            XCommandProcessor xCmd =
                UnoRuntime.queryInterface(XCommandProcessor.class, xCnt);

            Property[] pArgs = new Property[ ] { new Property() };
            pArgs[ 0 ].Name = "DocumentModel";
            pArgs[ 0 ].Handle = -1;

            Command command = new Command();

            command.Handle = -1;
            command.Name = "getPropertyValues";
            command.Argument = pArgs;

            com.sun.star.ucb.XCommandEnvironment env = null ;
            Object result =  xCmd.execute(command, 0, env) ;

            XRow values = UnoRuntime.queryInterface(XRow.class, result);

            xModel = UnoRuntime.queryInterface(XModel.class,
                                               values.getObject(1, null));

        } catch (Exception ignore) {
            LogUtils.DEBUG("Failed to get model exception " + ignore);
        }

        return xModel;
    }
}
