package com.hmsoft.remote.rmi.rpc;

import java.net.InetSocketAddress;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import org.apache.log4j.Logger;
import com.hmsoft.remote.rmi.common.utils.ExecutorUtil;
import com.hmsoft.remote.rmi.common.utils.NamedThreadFactory;
import com.hmsoft.remote.rmi.common.utils.NetUtils;
import com.hmsoft.remote.rmi.exception.RemotingException;
import com.hmsoft.remote.rmi.handler.ChannelHandler;
import com.hmsoft.remote.rmi.transport.NettyChannel;


/**
 * 
 * @author zq
 *
 */
public abstract class AbstractClient extends AbstractPeer implements Client {

	private static final Logger logger = Logger.getLogger(AbstractClient.class);
	private static final AtomicInteger CLIENT_THREAD_POOL_ID = new AtomicInteger();
	private static final ScheduledThreadPoolExecutor reconnectExecutorService = new ScheduledThreadPoolExecutor(1, new NamedThreadFactory("MtzjClientReconnectTimer", true));
    private final Lock connectLock = new ReentrantLock();
    private final boolean send_reconnect;
    private final AtomicInteger reconnect_count = new AtomicInteger(0);
    // Reconnection error log has been called before?
    private final AtomicBoolean reconnect_error_log_flag = new AtomicBoolean(false);
    // reconnect warning period. Reconnect warning interval (log warning after how many times) //for test
    private final int reconnect_warning_period;
    private final long shutdown_timeout;
    protected volatile ExecutorService executor;
    private volatile ScheduledFuture<?> reconnectExecutorFuture = null;
    // the last successed connected time
    private long lastConnectedTime = System.currentTimeMillis();
    private int timeout = 3000;
    
    public AbstractClient(String url, ChannelHandler handler) throws RemotingException {
        super(url, handler);
        //init
        send_reconnect = true;
        shutdown_timeout = 2000;
        // The default reconnection interval is 2s, 1800 means warning interval is 1 hour.
        reconnect_warning_period = 1800;

        try {
            doOpen();
        } catch (Throwable t) {
            close();
            throw new RemotingException("connect to server fail");
        }
        try {
            // connect.
            connect();
        } catch (RemotingException t) {
            t.printStackTrace();
        } catch (Throwable t) {
            close();
            throw new RemotingException("fail to start");
        }

    }
    
    public void send(Object message) throws RemotingException {
        if (send_reconnect && !isConnected()) {
            connect();
        }
        NettyChannel channel = getChannel();
        if (channel == null || ! channel.isConnected()) {
        	throw new RemotingException("message can not send, because channel is closed . url:" + getUrl());
        }
        channel.send(message);
    }
    
    public void send(Object message, boolean sent) throws RemotingException {
        if (send_reconnect && !isConnected()){
            connect();
        }
        NettyChannel channel = getChannel();
        if (channel == null || ! channel.isConnected()) {
          throw new RemotingException("message can not send, because channel is closed . url:" + getUrl());
        }
        channel.send(message, sent);
    }
    
    protected void connect() throws RemotingException {
        connectLock.lock();
        try {
            if (isConnected()) {
                return;
            }
            initConnectStatusCheckCommand();
            doConnect();
            if (!isConnected()) {
            	throw new RemotingException("Failed connect to server " + getRemoteAddress() + " from " + getClass().getSimpleName() + " "
                        + NetUtils.getLocalHost() + " using netty version 4.1" 
                        + ", cause: Connect wait timeout: " + timeout + "ms.");
            } else {
                if (logger.isInfoEnabled()) {
                    logger.info("Successed connect to server " + getRemoteAddress() + " from " + getClass().getSimpleName() + " "
                            + NetUtils.getLocalHost() + " using version " 
                            + ", channel is " + this.getChannel());
                }
            }
            reconnect_count.set(0);
            reconnect_error_log_flag.set(false);
        } catch (RemotingException e) {
            throw e;
        } catch (Throwable e) {
            throw new RemotingException("Failed connect to server " + getRemoteAddress() + " from " + getClass().getSimpleName() + " "
                    + NetUtils.getLocalHost() + " using dubbo version " 
                    + ", cause: " + e.getMessage());
        } finally {
            connectLock.unlock();
        }
    }
    
    public void close() {
        try {
            if (executor != null) {
                ExecutorUtil.shutdownNow(executor, 100);
            }
        } catch (Throwable e) {
            logger.warn(e.getMessage(), e);
        }
//        try {
//            super.close();
//        } catch (Throwable e) {
//            //logger.warn(e.getMessage(), e);
//        }
        try {
            disconnect();
        } catch (Throwable e) {
            logger.warn(e.getMessage(), e);
        }
        try {
            doClose();
        } catch (Throwable e) {
            logger.warn(e.getMessage(), e);
        }
    }
    
    public void disconnect() {
        connectLock.lock();
        try {
            destroyConnectStatusCheckCommand();
            try {
            	NettyChannel channel = getChannel();
                if (channel != null) {
                    channel.close();
                }
            } catch (Throwable e) {
                logger.warn(e.getMessage(), e);
            }
            try {
                doDisConnect();
            } catch (Throwable e) {
                logger.warn(e.getMessage(), e);
            }
        } finally {
            connectLock.unlock();
        }
    }
    
    /**
     * init reconnect thread
     */
    private synchronized void initConnectStatusCheckCommand() {
        //reconnect=false to close reconnect
        long initalDelay = 3000;
        long delay = 3000;
        if (initalDelay > 0 && (reconnectExecutorFuture == null || reconnectExecutorFuture.isCancelled())) {
            Runnable connectStatusCheckCommand = new Runnable() {
                @Override
                public void run() {
                    try {
                        if (!isConnected()) {
                            connect();
                        } else {
                            lastConnectedTime = System.currentTimeMillis();
                        }
                    } catch (Throwable t) {
                        String errorMsg = "client reconnect to " + getRemoteAddress() + " find error . url: " + getUrl() ;
                        // wait registry sync provider list
                        if (System.currentTimeMillis() - lastConnectedTime > shutdown_timeout) {
                            if (!reconnect_error_log_flag.get()) {
                                reconnect_error_log_flag.set(true);
                                logger.error(errorMsg, t);
                                return;
                            }
                        }
                        if (reconnect_count.getAndIncrement() % reconnect_warning_period == 0) {
                            logger.warn(errorMsg, t);
                        }
                    }
                }
            };
            reconnectExecutorFuture = reconnectExecutorService.scheduleWithFixedDelay(connectStatusCheckCommand, 
            		initalDelay, delay, TimeUnit.MILLISECONDS);
        }
    }
    
    private synchronized void destroyConnectStatusCheckCommand() {
        try {
            if (reconnectExecutorFuture != null && !reconnectExecutorFuture.isDone()) {
                reconnectExecutorFuture.cancel(true);
                reconnectExecutorService.purge();
            }
        } catch (Throwable e) {
            logger.warn(e.getMessage(), e);
        }
    }
    
    public InetSocketAddress getLocalAddress() {
    	NettyChannel channel = getChannel();
        if (channel == null)
            return InetSocketAddress.createUnresolved(NetUtils.getLocalHost(), 0);
        return channel.getLocalAddress();
    }
    
    public InetSocketAddress getRemoteAddress() {
    	NettyChannel channel = getChannel();
    	if (channel == null) {
    		return this.getConnectAddress();
    	}
        return channel.getRemoteAddress();
    }
    
    public String toString() {
        return getClass().getName() + " [" + getLocalAddress() + " -> " + getRemoteAddress() + "]";
    }
    
    public boolean isConnected() {
    	NettyChannel channel = getChannel();
        if (channel == null)
            return false;
        return channel.isConnected();
    }
    
    public InetSocketAddress getConnectAddress() {
    	String host = this.getUrl().split(":")[0];
    	String port = this.getUrl().split(":")[1];
        return new InetSocketAddress(host, Integer.parseInt(port));
    }
    
    protected abstract void doOpen() throws Throwable;
    protected abstract void doClose() throws Throwable;
    protected abstract void doConnect() throws Throwable;
    protected abstract void doDisConnect() throws Throwable;
    public abstract NettyChannel getChannel();
}
