/*
 * Copyright (c) 2015 jMonkeyEngine
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 *
 * * Redistributions of source code must retain the above copyright
 *   notice, this list of conditions and the following disclaimer.
 *
 * * Redistributions in binary form must reproduce the above copyright
 *   notice, this list of conditions and the following disclaimer in the
 *   documentation and/or other materials provided with the distribution.
 *
 * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
 *   may be used to endorse or promote products derived from this software
 *   without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package com.jme3.network.service.rmi;

import com.jme3.network.HostedConnection;
import com.jme3.network.MessageConnection;
import com.jme3.network.Server;
import com.jme3.network.serializing.Serializer;
import com.jme3.network.service.AbstractHostedService;
import com.jme3.network.service.HostedServiceManager;
import com.jme3.network.service.rpc.RpcHostedService;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;


/**
 *  A service that can be added to the host to support a simple
 *  shared objects protocol.
 *
 *  <p>Objects are shared by adding them to the RmiRegistry with one of the
 *  share() methods.  Shared objects must have a separate interface and implementation.
 *  The interface is what the other end of the connection will use to interact
 *  with the object and that interface class must be available on both ends of
 *  the connection.  The implementing class need only be on the sharing end.</p>
 *
 *  <p>Shared objects can be accessed on the other end of the connection by
 *  using one of the RmiRegistry's getRemoteObject() methods.  These can be
 *  used to lookup an object by class if it is a shared singleton or by name
 *  if it was registered with a name.</p>
 *
 *  <p>On the hosting side, a special shardGlobal() method is provided that
 *  will register shared objects that will automatically be provided to every
 *  new joining client and they will all be calling the same server-side instance.
 *  Normally, shared objects themselves are connection specific and handled
 *  at the connection layer.  The shareGlobal() space is a way to have global
 *  resources passed directly though the need is relatively rare.</p>
 * 
 *  <p>Note: This RMI implementation is not as advanced as Java's regular
 *  RMI as it won't marshall shared references, ie: you can't pass
 *  a shared objects as an argument to another shared object's method.</p>
 *
 *  @author    Paul Speed
 */
public class RmiHostedService extends AbstractHostedService {

    static final Logger log = Logger.getLogger(RpcHostedService.class.getName());
    
    public static final String ATTRIBUTE_NAME = "rmi";

    private RpcHostedService rpcService;
    private short rmiId;
    private byte defaultChannel;
    private boolean autoHost;
    private final Map<String, GlobalShare> globalShares = new ConcurrentHashMap<String, GlobalShare>();

    public RmiHostedService() {
        this((short)-1, (byte)MessageConnection.CHANNEL_DEFAULT_RELIABLE, true);
    }

    public RmiHostedService( byte defaultChannel ) {
        this((short)-1, defaultChannel, true);
    }

    public RmiHostedService( short rmiId, byte defaultChannel, boolean autoHost ) {
        this.rmiId = rmiId;
        this.defaultChannel = defaultChannel;
        this.autoHost = autoHost;
        
        Serializer.registerClasses(ClassInfo.class, MethodInfo.class);
    }

    /**
     *  Shares a server-wide object associated with the specified type.  All connections
     *  with RMI hosting started will have access to this shared object as soon as they 
     *  connect and they will all share the same instance.  It is up to the shared object 
     *  to handle any multithreading that might be required.
     */     
    public <T> void shareGlobal( T object, Class<? super T> type ) {
        shareGlobal(defaultChannel, type.getName(), object, type);
    }
    
    /**
     *  Shares a server-wide object associated with the specified name.  All connections
     *  with RMI hosting started will have access to this shared object as soon as they 
     *  connect and they will all share the same instance.  It is up to the shared object 
     *  to handle any multithreading that might be required.
     */     
    public <T> void shareGlobal( String name, T object, Class<? super T> type ) {
        shareGlobal(defaultChannel, name, object, type);
    }
    
    /**
     *  Shares a server-wide object associated with the specified name over the specified
     *  channel.  All connections with RMI hosting started will have access to this shared 
     *  object as soon as they connect and they will all share the same instance.  It is up 
     *  to the shared object to handle any multithreading that might be required.
     *  All network communcation associated with the shared object will be done over
     *  the specified channel. 
     */     
    public <T> void shareGlobal( byte channel, String name, T object, Class<? super T> type ) {
        GlobalShare share = new GlobalShare(channel, object, type);
        GlobalShare existing = globalShares.put(name, share);
        if( existing != null ) {
            // Shouldn't need to do anything actually.
        }
        
        // Go through all of the children
        for( HostedConnection conn : getServer().getConnections() ) {
            RmiRegistry child = getRmiRegistry(conn);
            if( child == null ) {
                continue;
            }
            child.share(channel, name, object, type);
        }
    } 

    /**
     *  Set to true if all new connections should automatically have RMI hosting started.
     *  Set to false if the game-specific connection setup will call startHostingOnConnection()
     *  after some connection setup is done (for example, logging in).  Note: generally
     *  is is safe to autohost RMI as long as callers are careful about what they've added
     *  using shareGlobal().  One reasonable use-case is to shareGlobal() some kind of login
     *  service and nothing else.  All other shared objects would then be added as connection
     *  specific objects during successful login processing. 
     */
    public void setAutoHost( boolean b ) {
        this.autoHost = b;
    }
 
    /**
     *  Returns true if RMI hosting is automatically started for all new connections. 
     */
    public boolean getAutoHost() {
        return autoHost;
    }

    /**
     *  Returns the RMI registry for the specific HostedConection.  Each connection
     *  has its own registry with its own connection-specific shared objects.
     */
    public RmiRegistry getRmiRegistry( HostedConnection hc ) {
        return hc.getAttribute(ATTRIBUTE_NAME);
    }

    /**
     *  Sets up RMI hosting services for the hosted connection allowing
     *  getRmiRegistry() to return a valid RmiRegistry object.
     *  This method is called automatically for all new connections if
     *  autohost is set to true.
     */
    public void startHostingOnConnection( HostedConnection hc ) {
        if( log.isLoggable(Level.FINEST) ) {
            log.log(Level.FINEST, "startHostingOnConnection:{0}", hc);
        }
        RmiRegistry rmi = new RmiRegistry(hc, rpcService.getRpcConnection(hc), 
                                          rmiId, defaultChannel); 
        hc.setAttribute(ATTRIBUTE_NAME, rmi);
        
        // Register any global shares
        for( Map.Entry<String, GlobalShare> e : globalShares.entrySet() ) {
            GlobalShare share = e.getValue();
            rmi.share(share.channel, e.getKey(), share.object, share.type); 
        }
    }

    /**
     *  Removes any RMI hosting services associated with the specified
     *  connection.  Calls to getRmiRegistry() will return null for
     *  this connection.  
     *  This method is called automatically for all leaving connections if
     *  autohost is set to true.
     */
    public void stopHostingOnConnection( HostedConnection hc ) {
        RmiRegistry rmi = hc.getAttribute(ATTRIBUTE_NAME);
        if( rmi == null ) {
            return;
        }
        if( log.isLoggable(Level.FINEST) ) {
            log.log(Level.FINEST, "stopHostingOnConnection:{0}", hc);
        }
        hc.setAttribute(ATTRIBUTE_NAME, null);
        //rpc.close();
    }

    @Override
    protected void onInitialize( HostedServiceManager s ) {
        this.rpcService = getService(RpcHostedService.class);
        if( rpcService == null ) {
            throw new RuntimeException("RmiHostedService requires RpcHostedService");
        }
    }
 
    /**
     *  Called internally when a new connection is detected for
     *  the server.  If the current autoHost property is true then
     *  startHostingOnConnection(hc) is called. 
     */
    @Override
    public void connectionAdded(Server server, HostedConnection hc) {
        if( log.isLoggable(Level.FINEST) ) {
            log.log(Level.FINEST, "connectionAdded({0}, {1})", new Object[]{server, hc});
        }    
        if( autoHost ) {
            startHostingOnConnection(hc);
        }
    }

    /**
     *  Called internally when an existing connection is leaving
     *  the server.  If the current autoHost property is true then
     *  stopHostingOnConnection(hc) is called. 
     */
    @Override
    public void connectionRemoved(Server server, HostedConnection hc) {
        if( log.isLoggable(Level.FINEST) ) {
            log.log(Level.FINEST, "connectionRemoved({0}, {1})", new Object[]{server, hc});
        }
        if( autoHost ) {    
            stopHostingOnConnection(hc);
        }
    }
    
    private class GlobalShare {
        byte channel;
        Object object;
        Class type;
        
        public GlobalShare( byte channel, Object object, Class type ) {
            this.channel = channel;
            this.object = object;
            this.type = type;
        }
    }
}
