/*************************************************************************
 *
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * Copyright 2000, 2010 Oracle and/or its affiliates.
 *
 * OpenOffice.org - a multi-platform office productivity suite
 *
 * This file is part of OpenOffice.org.
 *
 * OpenOffice.org is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License version 3
 * only, as published by the Free Software Foundation.
 *
 * OpenOffice.org is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License version 3 for more details
 * (a copy is included in the LICENSE file that accompanied this code).
 *
 * You should have received a copy of the GNU Lesser General Public License
 * version 3 along with OpenOffice.org.  If not, see
 * <http://www.openoffice.org/license.html>
 * for a copy of the LGPLv3 License.
 *
 ************************************************************************/
package com.sun.star.lib.uno.bridges.java_remote;


import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;


import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Map;
import java.util.Vector;

import com.sun.star.lib.util.DisposeListener;
import com.sun.star.lib.util.DisposeNotifier;

import com.sun.star.bridge.XBridge;
import com.sun.star.bridge.XInstanceProvider;

import com.sun.star.connection.XConnection;
import com.sun.star.lang.EventObject;
import com.sun.star.lang.XComponent;
import com.sun.star.lang.XEventListener;
import com.sun.star.lang.DisposedException;

import com.sun.star.lib.uno.environments.java.java_environment;
import com.sun.star.lib.uno.environments.remote.IProtocol;
import com.sun.star.lib.uno.environments.remote.IReceiver;
import com.sun.star.lib.uno.environments.remote.Job;
import com.sun.star.lib.uno.environments.remote.Message;
import com.sun.star.lib.uno.environments.remote.ThreadId;
import com.sun.star.lib.uno.environments.remote.ThreadPoolManager;
import com.sun.star.lib.uno.environments.remote.IThreadPool;

import com.sun.star.lib.uno.typedesc.MethodDescription;
import com.sun.star.lib.uno.typedesc.TypeDescription;


import com.sun.star.uno.IBridge;
import com.sun.star.uno.IEnvironment;
import com.sun.star.uno.UnoRuntime;
import com.sun.star.uno.XInterface;
import com.sun.star.uno.Type;
import com.sun.star.uno.TypeClass;
import com.sun.star.uno.Any;

/**
 * This class implements a remote bridge. Therefor
 * various interfaces are implemented.
 * <p>
 * The protocol to used is passed by name, the bridge
 * then looks for it under <code>com.sun.star.lib.uno.protocols</code>.
 * <p>
 * @version     $Revision: 1.45 $ $ $Date: 2008-04-11 11:18:08 $
 * @author      Kay Ramme
 * @since       UDK1.0
 */
public class java_remote_bridge
    implements IBridge, IReceiver, RequestHandler, XBridge, XComponent,
        DisposeNotifier
{
    /**
     * When set to true, enables various debugging output.
     */
    static private final boolean DEBUG = false;

    private final class MessageDispatcher extends Thread {
        public MessageDispatcher() {
            super("MessageDispatcher");
        }

        public void run() {
            try {
                for (;;) {
                    synchronized (this) {
                        if (terminate) {
                            break;
                        }
                    }
                    Message msg = _iProtocol.readMessage();
                    Object obj = null;
                    if (msg.isRequest()) {
                        String oid = msg.getObjectId();
                        Type type = new Type(msg.getType());
                        int fid = msg.getMethod().getIndex();
                        if (fid == MethodDescription.ID_RELEASE) {
                            _java_environment.revokeInterface(oid, type);
                            remRefHolder(type, oid);
                            if (msg.isSynchronous()) {
                                sendReply(false, msg.getThreadId(), null);
                            }
                            continue;
                        }
                        obj = _java_environment.getRegisteredInterface(
                            oid, type);
                        if (obj == null
                            && fid == MethodDescription.ID_QUERY_INTERFACE)
                        {
                            if (_xInstanceProvider == null) {
                                sendReply(
                                    true, msg.getThreadId(),
                                    new com.sun.star.uno.RuntimeException(
                                        "unknown OID " + oid));
                                continue;
                            } else {
                                UnoRuntime.setCurrentContext(
                                    msg.getCurrentContext());
                                try {
                                    obj = _xInstanceProvider.getInstance(oid);
                                } catch (com.sun.star.uno.RuntimeException e) {
                                    sendReply(true, msg.getThreadId(), e);
                                    continue;
                                } catch (Exception e) {
                                    sendReply(
                                        true, msg.getThreadId(),
                                        new com.sun.star.uno.RuntimeException(
                                            e.toString()));
                                    continue;
                                } finally {
                                    UnoRuntime.setCurrentContext(null);
                                }
                            }
                        }
                    }
                    _iThreadPool.putJob(
                        new Job(obj, java_remote_bridge.this, msg));
                }
            } catch (Throwable e) {
                dispose(new DisposedException(e.toString()));
            }
        }

        public synchronized void terminate() {
            terminate = true;
        }

        private boolean terminate = false;
    }

    protected XConnection       _xConnection;

    protected XInstanceProvider _xInstanceProvider;

    protected String            _name = "remote";
    private final String protocol;
    protected IProtocol         _iProtocol;
    protected IEnvironment      _java_environment;
    protected MessageDispatcher _messageDispatcher;
    protected int               _life_count = 0;    // determines if this bridge is alife, which is controlled by acquire and release calls

    private final Vector _listeners = new Vector();

    protected IThreadPool       _iThreadPool;

    // Variable disposed must only be used while synchronized on this object:
    private boolean disposed = false;

    /**
     * This method is for testing only.
     */
    int getLifeCount() {
        return _life_count;
    }

    /**
     * This method is for testing only.
     */
    IProtocol getProtocol() {
        return _iProtocol;
    }

    // The ref holder stuff strongly holds objects mapped out via this bridge
    // (the java_environment only holds them weakly).  When this bridge is
    // disposed, all remaining ref holder entries are released.

    private static final class RefHolder {
        public RefHolder(Type type, Object object) {
            this.type = type;
            this.object = object;
        }

        public Type getType() {
            return type;
        }

        public void acquire() {
            ++count;
        }

        public boolean release() {
            return --count == 0;
        }

        private final Type type;
        private final Object object;
        private int count = 1;
    }

    private final HashMap refHolders = new HashMap();
        // from OID (String) to LinkedList of RefHolder

    private boolean hasRefHolder(String oid, Type type) {
        synchronized (refHolders) {
            LinkedList l = (LinkedList) refHolders.get(oid);
            if (l != null) {
                for (Iterator i = l.iterator(); i.hasNext();) {
                    RefHolder rh = (RefHolder) i.next();
                    if (type.isSupertypeOf(rh.getType())) {
                        return true;
                    }
                }
            }
        }
        return false;
    }

    final void addRefHolder(Object obj, Type type, String oid) {
        synchronized (refHolders) {
            LinkedList l = (LinkedList) refHolders.get(oid);
            if (l == null) {
                l = new LinkedList();
                refHolders.put(oid, l);
            }
            boolean found = false;
            for (Iterator i = l.iterator(); !found && i.hasNext();) {
                RefHolder rh = (RefHolder) i.next();
                if (rh.getType().equals(type)) {
                    found = true;
                    rh.acquire();
                }
            }
            if (!found) {
                l.add(new RefHolder(type, obj));
            }
        }
        acquire();
    }

    final void remRefHolder(Type type, String oid) {
        synchronized (refHolders) {
            LinkedList l = (LinkedList) refHolders.get(oid);
            if (l != null) {
                for (Iterator i = l.iterator(); i.hasNext();) {
                    RefHolder rh = (RefHolder) i.next();
                    if (rh.getType().equals(type)) {
                        try {
                            if (rh.release()) {
                                l.remove(rh);
                                if (l.isEmpty()) {
                                    refHolders.remove(oid);
                                }
                            }
                        } finally {
                            release();
                        }
                        break;
                    }
                }
            }
        }
    }

    final void freeHolders() {
        synchronized (refHolders) {
            for (Iterator i1 = refHolders.entrySet().iterator(); i1.hasNext();)
            {
                Map.Entry e = (Map.Entry) i1.next();
                String oid = (String) e.getKey();
                LinkedList l = (LinkedList) e.getValue();
                for (Iterator i2 = l.iterator(); i2.hasNext();) {
                    RefHolder rh = (RefHolder) i2.next();
                    for (boolean done = false; !done;) {
                        done = rh.release();
                        _java_environment.revokeInterface(oid, rh.getType());
                        release();
                    }
                }
            }
            refHolders.clear();
        }
    }

    public java_remote_bridge(
        IEnvironment java_environment, IEnvironment remote_environment,
        Object[] args)
        throws Exception
    {
        _java_environment = java_environment;
        String proto = (String) args[0];
        _xConnection = (XConnection) args[1];
        _xInstanceProvider = (XInstanceProvider) args[2];
        if (args.length > 3) {
            _name = (String) args[3];
        }
        String attr;
        int i = proto.indexOf(',');
        if (i >= 0) {
            protocol = proto.substring(0, i);
            attr = proto.substring(i + 1);
        } else {
            protocol = proto;
            attr = null;
        }
        _iProtocol = (IProtocol) Class.forName(
            "com.sun.star.lib.uno.protocols." + protocol + "." + protocol).
            getConstructor(
                new Class[] {
                    IBridge.class, String.class, InputStream.class,
                    OutputStream.class }).
            newInstance(
                new Object[] {
                    this, attr,
                    new XConnectionInputStream_Adapter(_xConnection),
                    new XConnectionOutputStream_Adapter(_xConnection) });
        proxyFactory = new ProxyFactory(this, this);
        _iThreadPool = ThreadPoolManager.create();
        _messageDispatcher = new MessageDispatcher();
        _messageDispatcher.start();
        _iProtocol.init();
    }

    private void notifyListeners() {
        EventObject eventObject = new EventObject(this);

        Enumeration elements = _listeners.elements();
        while(elements.hasMoreElements()) {
            XEventListener xEventListener = (XEventListener)elements.nextElement();

            try {
                xEventListener.disposing(eventObject);
            }
            catch(com.sun.star.uno.RuntimeException runtimeException) {
                // we are here not interested in any exceptions
            }
        }
    }

    /**
     * Constructs a new bridge.
     * <p>
     * This method is not part of the provided <code>api</code>
     * and should only be used by the UNO runtime.
     * <p>
     * @deprecated as of UDK 1.0
     * <p>
     * @param  args               the custom parameters: arg[0] == protocol_name, arg[1] == xConnection, arg[2] == xInstanceProvider
     */
    public java_remote_bridge(Object args[]) throws Exception {
        this(UnoRuntime.getEnvironment("java", null), UnoRuntime.getEnvironment("remote", null), args);
    }

    // @see com.sun.star.uno.IBridge#mapInterfaceTo
    public Object mapInterfaceTo(Object object, Type type) {
        checkDisposed();
        if (object == null) {
            return null;
        } else {
            String[] oid = new String[1];
            object = _java_environment.registerInterface(object, oid, type);
            if (!proxyFactory.isProxy(object)) {
                // This branch must be taken iff object either is no proxy at
                // all or a proxy from some other bridge.  There are objects
                // that behave like objects for this bridge but that are not
                // detected as such by proxyFactory.isProxy.  The only known
                // case of such objects is com.sun.star.comp.beans.Wrapper,
                // which implements com.sun.star.lib.uno.Proxy and effectively
                // is a second proxy around a proxy that can be from this
                // bridge.  For that case, there is no problem, however:  Since
                // the proxies generated by ProxyFactory send each
                // queryInterface to the original object (i.e., they do not
                // short-circuit requests for a super-interface to themselves),
                // there will always be an appropriate ProxyFactory-proxy
                // registered at the _java_environment, so that the object
                // returned by _java_environment.registerInterface will never be
                // a com.sun.star.comp.beans.Wrapper.
                addRefHolder(object, type, oid[0]);
            }
            return oid[0];
        }
    }

    /**
     * Maps an object from destination environment to the source environment.
     * <p>
     * @return     the object in the source environment
     * @param      object     the object to map
     * @param      type       the interface under which is to be mapped
     * @see                   com.sun.star.uno.IBridge#mapInterfaceFrom
     */
    public Object mapInterfaceFrom(Object oId, Type type) {
        checkDisposed();
        // TODO  What happens if an exception is thrown after the call to
        // acquire, but before it is guaranteed that a pairing release will be
        // called eventually?
        acquire();
        String oid = (String) oId;
        Object object = _java_environment.getRegisteredInterface(oid, type);
        if (object == null) {
            object = _java_environment.registerInterface(
                proxyFactory.create(oid, type), new String[] { oid }, type);
                // the proxy sends a release when finalized
        } else if (!hasRefHolder(oid, type)) {
            sendInternalRequest(oid, type, "release", null);
        }
        return object;
    }

    /**
     * Gives the source environment.
     * <p>
     * @return   the source environment of this bridge
     * @see      com.sun.star.uno.IBridge#getSourceEnvironment
     */
    public IEnvironment getSourceEnvironment() {
        return _java_environment;
    }

    /**
     * Gives the destination environment.
     * <p>
     * @return   the destination environment of this bridge
     * @see      com.sun.star.uno.IBridge#getTargetEnvironment
     */
    public IEnvironment getTargetEnvironment() {
        return null;
    }

    /**
     * Increases the life count.
     * <p>
     * @see com.sun.star.uno.IBridge#acquire
     */
    public synchronized void acquire() {
        ++ _life_count;

        if(DEBUG) System.err.println("##### " + getClass().getName() + ".acquire:" + _life_count);
    }

    /**
     * Decreases the life count.
     * If the life count drops to zero, the bridge disposes itself.
     * <p>
     * @see com.sun.star.uno.IBridge#release
     */
    public void release() {
        boolean dispose;
        synchronized (this) {
            --_life_count;
            dispose = _life_count <= 0;
        }
        if (dispose) {
            dispose(new com.sun.star.uno.RuntimeException("end of life"));
        }
    }

    public void dispose() {
        dispose(new com.sun.star.uno.RuntimeException("user dispose"));
    }

    private void dispose(Throwable throwable) {
        synchronized (this) {
            if (disposed) {
                return;
            }
            disposed = true;
        }

        notifyListeners();
        for (Iterator i = disposeListeners.iterator(); i.hasNext();) {
            ((DisposeListener) i.next()).notifyDispose(this);
        }

        try {
            _messageDispatcher.terminate();

            _xConnection.close();

            if (Thread.currentThread() != _messageDispatcher
                && _messageDispatcher.isAlive())
            {
                // This is a workaround for a Linux Sun JDK1.3 problem:  The
                // message dispatcher stays in the socket read method, even if
                // the socket has been closed.  Suspending and resuming the
                // message dispatcher lets it notice the closed socket.  Only
                // use this workaround for Linux JRE 1.3.0 and 1.3.1 from Sun
                // and Blackdown.  This workaround is dangerouse and may
                // hardlock the VM.
                if (System.getProperty("os.name", "").toLowerCase().equals(
                        "linux")
                    && System.getProperty("java.version", "").startsWith("1.3.")
                    && (System.getProperty("java.vendor", "").toLowerCase().
                            indexOf("sun") != -1
                        || System.getProperty("java.vendor", "").toLowerCase().
                            indexOf("blackdown") != -1))
                {
                    _messageDispatcher.suspend();
                    _messageDispatcher.resume();
                }

                _messageDispatcher.join(1000);
                if (_messageDispatcher.isAlive()) {
                    _messageDispatcher.interrupt();
                    _messageDispatcher.join();
                }
            }

            // interrupt all jobs queued by this bridge
            _iThreadPool.dispose(throwable);

            // release all out-mapped objects and all in-mapped proxies:
            freeHolders();
            // assert _java_environment instanceof java_environment;
            ((java_environment) _java_environment).revokeAllProxies();

            if (DEBUG) {
                if (_life_count != 0) {
                    System.err.println(getClass().getName()
                                       + ".dispose - life count (proxies left):"
                                       + _life_count);
                }
                _java_environment.list();
            }

            // clear members
            _xConnection        = null;
            _java_environment   = null;
            _messageDispatcher  = null;
        } catch (InterruptedException e) {
            System.err.println(getClass().getName()
                               + ".dispose - InterruptedException:" + e);
        } catch (com.sun.star.io.IOException e) {
            System.err.println(getClass().getName() + ".dispose - IOException:"
                               + e);
        }
    }

    // @see com.sun.star.bridge.XBridge#getInstance
    public Object getInstance(String instanceName) {
        Type t = new Type(XInterface.class);
        return sendInternalRequest(
            instanceName, t, "queryInterface", new Object[] { t });
    }

    /**
     * Gives the name of this bridge
     * <p>
     * @return  the name of this bridge
     * @see     com.sun.star.bridge.XBridge#getName
     */
    public String getName() {
        return _name;
    }

    /**
     * Gives a description of the connection type and protocol used
     * <p>
     * @return  connection type and protocol
     * @see     com.sun.star.bridge.XBridge#getDescription
     */
    public String getDescription() {
        return protocol + "," + _xConnection.getDescription();
    }

    public void sendReply(boolean exception, ThreadId threadId, Object result) {
        if (DEBUG) {
            System.err.println("##### " + getClass().getName() + ".sendReply: "
                               + exception + " " + result);
        }

        checkDisposed();

        try {
            _iProtocol.writeReply(exception, threadId, result);
        } catch (IOException e) {
            dispose(e);
            throw new DisposedException("unexpected " + e);
        } catch (RuntimeException e) {
            dispose(e);
            throw e;
        } catch (Error e) {
            dispose(e);
            throw e;
        }
    }

    public Object sendRequest(
        String oid, Type type, String operation, Object[] params)
        throws Throwable
    {
        Object result = null;

        checkDisposed();

        boolean goThroughThreadPool = false;

        ThreadId threadId = _iThreadPool.getThreadId();
        Object handle = _iThreadPool.attach(threadId);
        try {
            boolean sync;
            try {
                sync = _iProtocol.writeRequest(
                    oid, TypeDescription.getTypeDescription(type), operation,
                    threadId, params);
            } catch (IOException e) {
                DisposedException d = new DisposedException(e.toString());
                dispose(d);
                throw d;
            }
            if (sync && Thread.currentThread() != _messageDispatcher) {
                result = _iThreadPool.enter(handle, threadId);
            }
        } finally {
            _iThreadPool.detach(handle, threadId);
            if(operation.equals("release"))
                release(); // kill this bridge, if this was the last proxy
        }

        if(DEBUG) System.err.println("##### " + getClass().getName() + ".sendRequest left:" + result);

        // On the wire (at least in URP), the result of queryInterface is
        // transported as an ANY, but in Java it shall be transported as a
        // direct reference to the UNO object (represented as a Java Object),
        // never boxed in a com.sun.star.uno.Any:
        if (operation.equals("queryInterface") && result instanceof Any) {
            Any a = (Any) result;
            if (a.getType().getTypeClass() == TypeClass.INTERFACE) {
                result = a.getObject();
            } else {
                result = null; // should never happen
            }
        }

        return result;
    }

    private Object sendInternalRequest(
        String oid, Type type, String operation, Object[] arguments)
    {
        try {
            return sendRequest(oid, type, operation, arguments);
        } catch (Error e) {
            throw e;
        } catch (RuntimeException e) {
            throw e;
        } catch (Throwable e) {
            throw new RuntimeException("Unexpected " + e);
        }
    }

    // Methods XComponent
    public void addEventListener(XEventListener xEventListener) {
        _listeners.addElement(xEventListener);
    }

    public void removeEventListener(XEventListener xEventListener) {
        _listeners.removeElement(xEventListener);
    }

    // @see NotifyDispose.addDisposeListener
    public void addDisposeListener(DisposeListener listener) {
        synchronized (this) {
            if (!disposed) {
                disposeListeners.add(listener);
                return;
            }
        }
        listener.notifyDispose(this);
    }

    // This function must only be called while synchronized on this object:
    private synchronized void checkDisposed() {
        if (disposed) {
            throw new DisposedException("java_remote_bridge " + this
                                        + " is disposed");
        }
    }

    private final ProxyFactory proxyFactory;

    // Access to disposeListeners must be synchronized on <CODE>this</CODE>:
    private final ArrayList disposeListeners = new ArrayList();
}
