package com.lts.remoting;

import com.lts.core.commons.utils.StringUtils;
import com.lts.core.domain.KVPair;
import com.lts.core.logger.Logger;
import com.lts.core.logger.LoggerFactory;
import com.lts.core.support.SystemClock;
import com.lts.remoting.codec.Codec;
import com.lts.remoting.codec.DefaultCodec;
import com.lts.remoting.common.RemotingHelper;
import com.lts.remoting.common.SemaphoreReleaseOnlyOnce;
import com.lts.remoting.common.ServiceThread;
import com.lts.remoting.exception.RemotingSendRequestException;
import com.lts.remoting.exception.RemotingTimeoutException;
import com.lts.remoting.exception.RemotingTooMuchRequestException;
import com.lts.remoting.protocol.RemotingCommand;
import com.lts.remoting.protocol.RemotingCommandHelper;
import com.lts.remoting.protocol.RemotingProtos;

import java.util.HashMap;
import java.util.Iterator;
import java.util.Map.Entry;
import java.util.concurrent.*;


/**
 * Server与Client公用抽象类
 */
public abstract class AbstractRemoting {
    private static final Logger LOGGER = LoggerFactory.getLogger(RemotingHelper.RemotingLogName);

    // 信号量，Oneway情况会使用，防止本地缓存请求过多
    protected final Semaphore semaphoreOneway;

    // 信号量，异步调用情况会使用，防止本地缓存请求过多
    protected final Semaphore semaphoreAsync;

    // 缓存所有对外请求
    protected final ConcurrentHashMap<Integer /* opaque */, ResponseFuture> responseTable =
            new ConcurrentHashMap<Integer, ResponseFuture>(256);
    // 注册的各个RPC处理器
    protected final HashMap<Integer/* request code */, KVPair<RemotingProcessor, ExecutorService>> processorTable =
            new HashMap<Integer, KVPair<RemotingProcessor, ExecutorService>>(64);
    protected final RemotingEventExecutor remotingEventExecutor = new RemotingEventExecutor();
    // 默认请求代码处理器
    protected KVPair<RemotingProcessor, ExecutorService> defaultRequestProcessor;
    protected final ChannelEventListener channelEventListener;

    public AbstractRemoting(final int permitsOneway, final int permitsAsync, ChannelEventListener channelEventListener) {
        this.semaphoreOneway = new Semaphore(permitsOneway, true);
        this.semaphoreAsync = new Semaphore(permitsAsync, true);
        this.channelEventListener = channelEventListener;
    }

    public ChannelEventListener getChannelEventListener() {
        return this.channelEventListener;
    }

    public void putRemotingEvent(final RemotingEvent event) {
        this.remotingEventExecutor.putRemotingEvent(event);
    }

    public void processRequestCommand(final Channel channel, final RemotingCommand cmd) {
        final KVPair<RemotingProcessor, ExecutorService> matched = this.processorTable.get(cmd.getCode());
        final KVPair<RemotingProcessor, ExecutorService> pair =
                null == matched ? this.defaultRequestProcessor : matched;

        if (pair != null) {
            Runnable run = new Runnable() {
                @Override
                public void run() {
                    try {
                        final RemotingCommand response = pair.getKey().processRequest(channel, cmd);
                        // Oneway形式忽略应答结果
                        if (!RemotingCommandHelper.isOnewayRPC(cmd)) {
                            if (response != null) {
                                response.setOpaque(cmd.getOpaque());
                                RemotingCommandHelper.markResponseType(cmd);
                                try {
                                    channel.writeAndFlush(response).addListener(new ChannelHandlerListener() {
                                        @Override
                                        public void operationComplete(Future future) throws Exception {
                                            if (!future.isSuccess()) {
                                                LOGGER.error("response to " + RemotingHelper.parseChannelRemoteAddr(channel) + " failed", future.cause());
                                                LOGGER.error(cmd.toString());
                                                LOGGER.error(response.toString());
                                            }
                                        }
                                    });
                                } catch (Exception e) {
                                    LOGGER.error("process request over, but response failed", e);
                                    LOGGER.error(cmd.toString());
                                    LOGGER.error(response.toString());
                                }
                            } else {
                                // 收到请求，但是没有返回应答，可能是processRequest中进行了应答，忽略这种情况
                            }
                        }
                    } catch (Exception e) {
                        LOGGER.error("process request exception", e);
                        LOGGER.error(cmd.toString());

                        if (!RemotingCommandHelper.isOnewayRPC(cmd)) {
                            final RemotingCommand response =
                                    RemotingCommand.createResponseCommand(RemotingProtos.ResponseCode.SYSTEM_ERROR.code(),//
                                            StringUtils.toString(e));
                            response.setOpaque(cmd.getOpaque());
                            channel.writeAndFlush(response);
                        }
                    }
                }
            };

            try {
                // 这里需要做流控，要求线程池对应的队列必须是有大小限制的
                pair.getValue().submit(run);
            } catch (RejectedExecutionException e) {
                LOGGER.warn(RemotingHelper.parseChannelRemoteAddr(channel) //
                        + ", too many requests and system thread pool busy, RejectedExecutionException " //
                        + pair.getKey().toString() //
                        + " request code: " + cmd.getCode());
                if (!RemotingCommandHelper.isOnewayRPC(cmd)) {
                    final RemotingCommand response =
                            RemotingCommand.createResponseCommand(RemotingProtos.ResponseCode.SYSTEM_BUSY.code(),
                                    "too many requests and system thread pool busy, please try another server");
                    response.setOpaque(cmd.getOpaque());
                    channel.writeAndFlush(response);
                }
            }
        } else {
            String error = " request type " + cmd.getCode() + " not supported";
            final RemotingCommand response =
                    RemotingCommand.createResponseCommand(RemotingProtos.ResponseCode.REQUEST_CODE_NOT_SUPPORTED.code(),
                            error);
            response.setOpaque(cmd.getOpaque());
            channel.writeAndFlush(response);
            LOGGER.error(RemotingHelper.parseChannelRemoteAddr(channel) + error);
        }
    }

    public void processResponseCommand(Channel channel, RemotingCommand cmd) {
        final ResponseFuture responseFuture = responseTable.get(cmd.getOpaque());
        if (responseFuture != null) {
            responseFuture.setResponseCommand(cmd);

            responseFuture.release();

            // 异步调用
            if (responseFuture.getAsyncCallback() != null) {
                boolean runInThisThread = false;
                ExecutorService executor = this.getCallbackExecutor();
                if (executor != null) {
                    try {
                        executor.submit(new Runnable() {
                            @Override
                            public void run() {
                                try {
                                    responseFuture.executeInvokeCallback();
                                } catch (Exception e) {
                                    LOGGER.warn("excute callback in executor exception, and callback throw", e);
                                }
                            }
                        });
                    } catch (Exception e) {
                        runInThisThread = true;
                        LOGGER.warn("excute callback in executor exception, maybe executor busy", e);
                    }
                } else {
                    runInThisThread = true;
                }

                if (runInThisThread) {
                    try {
                        responseFuture.executeInvokeCallback();
                    } catch (Exception e) {
                        LOGGER.warn("", e);
                    }
                }
            }
            // 同步调用
            else {
                responseFuture.putResponse(cmd);
            }
        } else {
            LOGGER.warn("receive response, but not matched any request, "
                    + RemotingHelper.parseChannelRemoteAddr(channel));
            LOGGER.warn(cmd.toString());
        }

        responseTable.remove(cmd.getOpaque());
    }

    public void processMessageReceived(Channel channel, final RemotingCommand cmd) throws Exception {
        if (cmd != null) {
            switch (RemotingCommandHelper.getRemotingCommandType(cmd)) {
                case REQUEST_COMMAND:
                    processRequestCommand(channel, cmd);
                    break;
                case RESPONSE_COMMAND:
                    processResponseCommand(channel, cmd);
                    break;
                default:
                    break;
            }
        }
    }

    protected abstract ExecutorService getCallbackExecutor();

    public void scanResponseTable() {
        Iterator<Entry<Integer, ResponseFuture>> it = this.responseTable.entrySet().iterator();
        while (it.hasNext()) {
            Entry<Integer, ResponseFuture> next = it.next();
            ResponseFuture rep = next.getValue();

            if ((rep.getBeginTimestamp() + rep.getTimeoutMillis() + 1000) <= SystemClock.now()) {
                it.remove();
                rep.release();
                try {
                    rep.executeInvokeCallback();
                } catch (Exception e) {
                    LOGGER.error("scanResponseTable, operationComplete exception", e);
                }

                LOGGER.warn("remove timeout request, " + rep);
            }
        }
    }

    public RemotingCommand invokeSyncImpl(final Channel channel, final RemotingCommand request,
                                          final long timeoutMillis) throws InterruptedException, RemotingSendRequestException,
            RemotingTimeoutException {
        try {
            final ResponseFuture responseFuture =
                    new ResponseFuture(request.getOpaque(), timeoutMillis, null, null);
            this.responseTable.put(request.getOpaque(), responseFuture);
            channel.writeAndFlush(request).addListener(new ChannelHandlerListener() {
                @Override
                public void operationComplete(Future future) throws Exception {
                    if (future.isSuccess()) {
                        responseFuture.setSendRequestOK(true);
                        return;
                    } else {
                        responseFuture.setSendRequestOK(false);
                    }

                    responseTable.remove(request.getOpaque());
                    responseFuture.setCause(future.cause());
                    responseFuture.putResponse(null);
                    LOGGER.warn("send a request command to channel <" + channel.remoteAddress() + "> failed.");
                    LOGGER.warn(request.toString());
                }
            });

            RemotingCommand responseCommand = responseFuture.waitResponse(timeoutMillis);
            if (null == responseCommand) {
                // 发送请求成功，读取应答超时
                if (responseFuture.isSendRequestOK()) {
                    throw new RemotingTimeoutException(RemotingHelper.parseChannelRemoteAddr(channel),
                            timeoutMillis, responseFuture.getCause());
                }
                // 发送请求失败
                else {
                    throw new RemotingSendRequestException(RemotingHelper.parseChannelRemoteAddr(channel),
                            responseFuture.getCause());
                }
            }

            return responseCommand;
        } finally {
            this.responseTable.remove(request.getOpaque());
        }
    }

    public void invokeAsyncImpl(final Channel channel, final RemotingCommand request,
                                final long timeoutMillis, final AsyncCallback asyncCallback) throws InterruptedException,
            RemotingTooMuchRequestException, RemotingTimeoutException, RemotingSendRequestException {
        boolean acquired = this.semaphoreAsync.tryAcquire(timeoutMillis, TimeUnit.MILLISECONDS);
        if (acquired) {
            final SemaphoreReleaseOnlyOnce once = new SemaphoreReleaseOnlyOnce(this.semaphoreAsync);

            final ResponseFuture responseFuture =
                    new ResponseFuture(request.getOpaque(), timeoutMillis, asyncCallback, once);
            this.responseTable.put(request.getOpaque(), responseFuture);
            try {
                channel.writeAndFlush(request).addListener(new ChannelHandlerListener() {
                    @Override
                    public void operationComplete(Future future) throws Exception {
                        if (future.isSuccess()) {
                            responseFuture.setSendRequestOK(true);
                            return;
                        } else {
                            responseFuture.setSendRequestOK(false);
                        }

                        responseFuture.putResponse(null);
                        responseFuture.executeInvokeCallback();

                        responseTable.remove(request.getOpaque());
                        LOGGER.warn("send a request command to channel <" + channel.remoteAddress() + "> failed.");
                        LOGGER.warn(request.toString());
                    }
                });
            } catch (Exception e) {
                once.release();
                LOGGER.warn("write send a request command to channel <" + channel.remoteAddress() + "> failed.");
                throw new RemotingSendRequestException(RemotingHelper.parseChannelRemoteAddr(channel), e);
            }
        } else {
            if (timeoutMillis <= 0) {
                throw new RemotingTooMuchRequestException("invokeAsyncImpl invoke too fast");
            } else {
                LOGGER.warn("invokeAsyncImpl tryAcquire semaphore timeout, " + timeoutMillis
                        + " waiting thread nums: " + this.semaphoreAsync.getQueueLength());
                LOGGER.warn(request.toString());

                throw new RemotingTimeoutException("tryAcquire timeout(ms) " + timeoutMillis);
            }
        }
    }

    public void invokeOnewayImpl(final Channel channel, final RemotingCommand request,
                                 final long timeoutMillis) throws InterruptedException, RemotingTooMuchRequestException,
            RemotingTimeoutException, RemotingSendRequestException {
        RemotingCommandHelper.markOnewayRPC(request);
        boolean acquired = this.semaphoreOneway.tryAcquire(timeoutMillis, TimeUnit.MILLISECONDS);
        if (acquired) {
            final SemaphoreReleaseOnlyOnce once = new SemaphoreReleaseOnlyOnce(this.semaphoreOneway);
            try {
                channel.writeAndFlush(request).addListener(new ChannelHandlerListener() {
                    @Override
                    public void operationComplete(Future future) throws Exception {
                        once.release();
                        if (!future.isSuccess()) {
                            LOGGER.warn("send a request command to channel <" + channel.remoteAddress()
                                    + "> failed.");
                            LOGGER.warn(request.toString());
                        }
                    }
                });
            } catch (Exception e) {
                once.release();
                LOGGER.warn("write send a request command to channel <" + channel.remoteAddress() + "> failed.");
                throw new RemotingSendRequestException(RemotingHelper.parseChannelRemoteAddr(channel), e);
            }
        } else {
            if (timeoutMillis <= 0) {
                throw new RemotingTooMuchRequestException("invokeOnewayImpl invoke too fast");
            } else {
                LOGGER.warn("invokeOnewayImpl tryAcquire semaphore timeout, " + timeoutMillis
                        + " waiting thread nums: " + this.semaphoreOneway.getQueueLength());
                LOGGER.warn(request.toString());

                throw new RemotingTimeoutException("tryAcquire timeout(ms) " + timeoutMillis);
            }
        }
    }

    class RemotingEventExecutor extends ServiceThread {
        private final LinkedBlockingQueue<RemotingEvent> eventQueue = new LinkedBlockingQueue<RemotingEvent>();
        private final int MaxSize = 10000;

        public void putRemotingEvent(final RemotingEvent event) {
            if (this.eventQueue.size() <= MaxSize) {
                this.eventQueue.add(event);
            } else {
                LOGGER.warn("event queue size[{}] enough, so drop this event {}", this.eventQueue.size(),
                        event.toString());
            }
        }

        @Override
        public void run() {

            LOGGER.info(this.getServiceName() + " service started");

            final ChannelEventListener listener = AbstractRemoting.this.getChannelEventListener();

            while (!this.isStopped()) {
                try {
                    RemotingEvent event = this.eventQueue.poll(3000, TimeUnit.MILLISECONDS);
                    if (event != null) {
                        switch (event.getType()) {
                            case ALL_IDLE:
                                listener.onChannelIdle(IdleState.ALL_IDLE, event.getRemoteAddr(), event.getChannel());
                                break;
                            case WRITER_IDLE:
                                listener.onChannelIdle(IdleState.WRITER_IDLE, event.getRemoteAddr(), event.getChannel());
                                break;
                            case READER_IDLE:
                                listener.onChannelIdle(IdleState.READER_IDLE, event.getRemoteAddr(), event.getChannel());
                                break;
                            case CLOSE:
                                listener.onChannelClose(event.getRemoteAddr(), event.getChannel());
                                break;
                            case CONNECT:
                                listener.onChannelConnect(event.getRemoteAddr(), event.getChannel());
                                break;
                            case EXCEPTION:
                                listener.onChannelException(event.getRemoteAddr(), event.getChannel());
                                break;
                            default:
                                break;
                        }
                    }
                } catch (Exception e) {
                    LOGGER.warn(this.getServiceName() + " service has exception. ", e);
                }
            }

            LOGGER.info(this.getServiceName() + " service end");
        }

        @Override
        public String getServiceName() {
            return RemotingEventExecutor.class.getSimpleName();
        }
    }

    protected Codec getCodec() {
        // TODO 改为SPI
        return new DefaultCodec();
    }

}
