package org.sdnplatform.sync.internal.remote;

import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.util.Collection;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicInteger;

import org.jboss.netty.bootstrap.ClientBootstrap;
import org.jboss.netty.channel.Channel;
import org.jboss.netty.channel.ChannelFuture;
import org.jboss.netty.channel.group.ChannelGroup;
import org.jboss.netty.channel.group.DefaultChannelGroup;
import org.jboss.netty.channel.socket.nio.NioClientSocketChannelFactory;
import org.sdnplatform.sync.error.RemoteStoreException;
import org.sdnplatform.sync.error.SyncException;
import org.sdnplatform.sync.error.SyncRuntimeException;
import org.sdnplatform.sync.error.UnknownStoreException;
import org.sdnplatform.sync.internal.AbstractSyncManager;
import org.sdnplatform.sync.internal.config.AuthScheme;
import org.sdnplatform.sync.internal.rpc.RPCService;
import org.sdnplatform.sync.internal.rpc.TProtocolUtil;
import org.sdnplatform.sync.internal.store.IStore;
import org.sdnplatform.sync.internal.store.MappingStoreListener;
import org.sdnplatform.sync.internal.util.ByteArray;
import org.sdnplatform.sync.thrift.AsyncMessageHeader;
import org.sdnplatform.sync.thrift.SyncMessage;
import org.sdnplatform.sync.thrift.MessageType;
import org.sdnplatform.sync.thrift.RegisterRequestMessage;
import org.sdnplatform.sync.thrift.Store;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import net.floodlightcontroller.core.annotations.LogMessageCategory;
import net.floodlightcontroller.core.annotations.LogMessageDoc;
import net.floodlightcontroller.core.module.FloodlightModuleContext;
import net.floodlightcontroller.core.module.FloodlightModuleException;
import net.floodlightcontroller.core.module.IFloodlightService;

/**
 * Implementation of a sync service that passes its functionality off to a
 * remote sync manager over a TCP connection
 * @author readams
 */
@LogMessageCategory("State Synchronization")
public class RemoteSyncManager extends AbstractSyncManager {
    protected static final Logger logger =
            LoggerFactory.getLogger(RemoteSyncManager.class.getName());
    
    /**
     * Channel group that will hold all our channels
     */
    final ChannelGroup cg = new DefaultChannelGroup("Internal RPC");
    RemoteSyncPipelineFactory pipelineFactory;
    ExecutorService bossExecutor;
    ExecutorService workerExecutor;

    /**
     * Active connection to server
     */
    protected volatile Channel channel;
    private volatile int connectionGeneration = 0;
    protected Object readyNotify = new Object();
    protected volatile boolean ready = false;
    protected volatile boolean shutdown = false;

    /**
     * The remote node ID of the node we're connected to
     */
    protected Short remoteNodeId;
    
    /**
     * Client bootstrap
     */
    protected ClientBootstrap clientBootstrap;
    
    /**
     * Transaction ID used in message headers in the RPC protocol
     */
    private AtomicInteger transactionId = new AtomicInteger();

    /**
     * The hostname of the server to connect to
     */
    protected String hostname = "localhost";
    
    /**
     * Port to connect to
     */
    protected int port = 6642;
    
    protected AuthScheme authScheme;
    protected String keyStorePath;
    protected String keyStorePassword;
    
    private ConcurrentHashMap<Integer, RemoteSyncFuture> futureMap = 
            new ConcurrentHashMap<Integer, RemoteSyncFuture>();
    private Object futureNotify = new Object();
    private static int MAX_PENDING_REQUESTS = 1000;
    
    // ************
    // ISyncService
    // ************

    public RemoteSyncManager() {
    }

    @Override
    public void registerStore(String storeName, Scope scope) 
            throws SyncException {
        doRegisterStore(storeName, scope, false);
    }

    @Override
    public void registerPersistentStore(String storeName, Scope scope)
            throws SyncException {
        doRegisterStore(storeName, scope, true);
    }

    // *******************
    // AbstractSyncManager
    // *******************

    @Override
    public void addListener(String storeName, 
                            MappingStoreListener listener)
                                    throws UnknownStoreException {
        ensureConnected();
    }

    @Override
    public IStore<ByteArray, byte[]>
            getStore(String storeName) throws UnknownStoreException {
        ensureConnected();
        return new RemoteStore(storeName, this);
    }

    @Override
    public short getLocalNodeId() {
        ensureConnected();
        return remoteNodeId;
    }
    
    @Override
    public void shutdown() {
        shutdown = true;
        logger.debug("Shutting down Remote Sync Manager");
        try {
            if (!cg.close().await(5, TimeUnit.SECONDS)) {
                logger.debug("Failed to cleanly shut down remote sync");
                return;
            }
            if (clientBootstrap != null) {
                clientBootstrap.releaseExternalResources();
            }
            clientBootstrap = null;
            if (pipelineFactory != null)
                pipelineFactory.releaseExternalResources();
            pipelineFactory = null;
            if (workerExecutor != null)
                workerExecutor.shutdown();
            workerExecutor = null;
            if (bossExecutor != null)
                bossExecutor.shutdown();
            bossExecutor = null;
        } catch (InterruptedException e) {
            logger.debug("Interrupted while shutting down remote sync");
        }
    }

    // *****************
    // IFloodlightModule
    // *****************

    @Override
    public void init(FloodlightModuleContext context)
            throws FloodlightModuleException {
        Map<String, String> config = context.getConfigParams(this);
        if (null != config.get("hostname"))
            hostname = config.get("hostname");
        if (null != config.get("port"))
            port = Integer.parseInt(config.get("port"));
        keyStorePath = config.get("keyStorePath");
        keyStorePassword = config.get("keyStorePassword");
        authScheme = AuthScheme.NO_AUTH;
        try {
            authScheme = AuthScheme.valueOf(config.get("authScheme"));
        } catch (Exception e) {}
    }

    @Override
    public void startUp(FloodlightModuleContext context) 
            throws FloodlightModuleException {
        shutdown = false;
        bossExecutor = Executors.newCachedThreadPool();
        workerExecutor = Executors.newCachedThreadPool();
        
        final ClientBootstrap bootstrap =
                new ClientBootstrap(
                     new NioClientSocketChannelFactory(bossExecutor,
                                                       workerExecutor));
        bootstrap.setOption("child.reuseAddr", true);
        bootstrap.setOption("child.keepAlive", true);
        bootstrap.setOption("child.tcpNoDelay", true);
        bootstrap.setOption("child.sendBufferSize", 
                            RPCService.SEND_BUFFER_SIZE);
        bootstrap.setOption("child.receiveBufferSize", 
                            RPCService.SEND_BUFFER_SIZE);
        bootstrap.setOption("child.connectTimeoutMillis", 
                            RPCService.CONNECT_TIMEOUT);
        pipelineFactory = new RemoteSyncPipelineFactory(this);
        bootstrap.setPipelineFactory(pipelineFactory);
        clientBootstrap = bootstrap;
    }

    @Override
    public Collection<Class<? extends IFloodlightService>>
            getModuleDependencies() {
        return null;
    }
    
    // *****************
    // RemoteSyncManager
    // *****************
    
    /**
     * Get a suitable transaction ID for sending a message
     * @return the unique transaction iD
     */
    public int getTransactionId() {
        return transactionId.getAndIncrement();
    }

    /**
     * Send a request to the server and generate a future for the 
     * eventual reply.  Note that this call can block if there is no active
     * connection while a new connection is re-established or if the maximum
     * number of requests is already pending
     * @param xid the transaction ID for the request
     * @param request the actual request to send
     * @return A {@link Future} for the reply message
     * @throws InterruptedException 
     */
    public Future<SyncReply> sendRequest(int xid,
                                            SyncMessage request) 
                                         throws RemoteStoreException {
        ensureConnected();
        RemoteSyncFuture future = new RemoteSyncFuture(xid, 
                                                       connectionGeneration);
        futureMap.put(Integer.valueOf(xid), future);

        if (futureMap.size() > MAX_PENDING_REQUESTS) {
            synchronized (futureNotify) {
                while (futureMap.size() > MAX_PENDING_REQUESTS) {
                    try {
                        futureNotify.wait();
                    } catch (InterruptedException e) {
                        throw new RemoteStoreException("Could not send request",
                                                       e);
                    }
                }
            }
        }
        channel.write(request); 
        return future;
    }

    @LogMessageDoc(level="WARN",
                   message="Unexpected sync message reply type={type} id={id}",
                   explanation="An error occurred in the sync protocol",
                   recommendation=LogMessageDoc.REPORT_CONTROLLER_BUG)
    public void dispatchReply(int xid,
                              SyncReply reply) {
        RemoteSyncFuture future = futureMap.get(Integer.valueOf(xid));
        if (future == null) {
            logger.warn("Unexpected sync message replyid={}", xid);
            return;
        }
        futureMap.remove(Integer.valueOf(xid));
        future.setReply(reply);
        synchronized (futureNotify) {
            futureNotify.notify();
        }
    }

    protected void channelDisconnected(SyncException why) {
        ready = false;
        connectionGeneration += 1;
        if (why == null) why = new RemoteStoreException("Channel disconnected");
        for (RemoteSyncFuture f : futureMap.values()) {
            if (f.getConnectionGeneration() < connectionGeneration)
                dispatchReply(f.getXid(), 
                              new SyncReply(null, null, false, why, 0));
        }
    }

    // ***************
    // Local methods
    // ***************

    protected void ensureConnected() {
        if (!ready) {
            for (int i = 0; i < 2; i++) {
                synchronized (this) {
                    connectionGeneration += 1;
                    if (connect(hostname, port))
                        return;
                }
                try {
                    Thread.sleep(1000);
                } catch (Exception e) {}
            }
            if (channel == null) 
                throw new SyncRuntimeException(new SyncException("Failed to establish connection"));
        }
    }

    protected boolean connect(String hostname, int port) {
        ready = false;
        if (channel == null || !channel.isConnected()) {
            SocketAddress sa =
                    new InetSocketAddress(hostname, port);
            ChannelFuture future = clientBootstrap.connect(sa);
            future.awaitUninterruptibly();
            if (!future.isSuccess()) {
                logger.error("Could not connect to " + hostname + 
                             ":" + port, future.getCause());
                return false;
            }
            channel = future.getChannel();
        }
        while (!ready && channel != null && channel.isConnected()) {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) { }
        }
        if (!ready || channel == null || !channel.isConnected()) {
            logger.warn("Timed out connecting to {}:{}", hostname, port);
            return false;
        }
        logger.debug("Connected to {}:{}", hostname, port);
        return true;
    }

    private void doRegisterStore(String storeName, Scope scope, boolean b) 
            throws SyncException{

        ensureConnected();
        RegisterRequestMessage rrm = new RegisterRequestMessage();
        AsyncMessageHeader header = new AsyncMessageHeader();
        header.setTransactionId(getTransactionId());
        rrm.setHeader(header);
        
        Store store = new Store(storeName);
        store.setScope(TProtocolUtil.getTScope(scope));
        store.setPersist(false);
        rrm.setStore(store);
        
        SyncMessage bsm = new SyncMessage(MessageType.REGISTER_REQUEST);
        bsm.setRegisterRequest(rrm);
        Future<SyncReply> future =
                sendRequest(header.getTransactionId(), bsm);
        try {
            future.get(5, TimeUnit.SECONDS);
        } catch (TimeoutException e) {
            throw new RemoteStoreException("Timed out on operation", e);
        } catch (Exception e) {
            throw new RemoteStoreException("Error while waiting for reply", e);
        }        
    }
}