package edu.uw.zookeeper.protocol.client;

import java.lang.ref.Reference;
import java.util.Iterator;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.Executor;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.atomic.AtomicReference;

import org.apache.logging.log4j.Logger;

import com.google.common.base.MoreObjects;
import com.google.common.collect.Queues;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;

import edu.uw.zookeeper.common.Actors;
import edu.uw.zookeeper.common.Automaton;
import edu.uw.zookeeper.common.Promise;
import edu.uw.zookeeper.common.PromiseTask;
import edu.uw.zookeeper.common.TimeValue;
import edu.uw.zookeeper.net.Connection;
import edu.uw.zookeeper.protocol.Message;
import edu.uw.zookeeper.protocol.ConnectMessage;
import edu.uw.zookeeper.protocol.Operation;
import edu.uw.zookeeper.protocol.ProtocolConnection;
import edu.uw.zookeeper.protocol.proto.OpCodeXid;


public abstract class PendingQueueClientExecutor<
    I extends Operation.Request, 
    O extends Operation.ProtocolResponse<?>,
    T extends PendingQueueClientExecutor.RequestTask<I,O>,
    C extends ProtocolConnection<? super Message.ClientSession, ? extends Operation.Response,?,?,?>,
    V extends PendingQueueClientExecutor.PendingTask>
    extends AbstractConnectionClientExecutor<I,O,T,C,V> {

    protected final Pending<O,V,C> pending;

    protected PendingQueueClientExecutor(
            Logger logger,
            ListenableFuture<ConnectMessage.Response> session,
            C connection,
            TimeValue timeOut,
            ScheduledExecutorService scheduler) {
        super(session, connection, timeOut, scheduler);
        this.pending = Pending.<O,V,C>create(connection, this, logger);
    }
    
    protected PendingQueueClientExecutor(
            Logger logger,
            ListenableFuture<ConnectMessage.Response> session,
            C connection,
            Listeners listeners,
            TimeOutServer<Operation.Response> timer,
            AtomicReference<Throwable> failure) {
        super(session, connection, listeners, timer, failure);
        this.pending = Pending.<O,V,C>create(connection, this, logger);
    }

    @Override
    public void handleConnectionRead(Operation.Response message) {
        super.handleConnectionRead(message);
        pending.handleConnectionRead(message);
    }
    
    @Override
    public void handleConnectionState(Automaton.Transition<Connection.State> event) {
        super.handleConnectionState(event);
        pending.handleConnectionState(event);
    }
    
    @Override
    protected void doStop() {
        pending.stop();
        super.doStop();
    }
    
    protected Executor executor() {
        return connection;
    }

    public static class PendingTask
        extends PromiseTask<Reference<? extends Message.ClientRequest<?>>, Message.ServerResponse<?>>
        implements Operation.RequestId, FutureCallback<Message.ClientRequest<?>> {
        
        public static PendingTask create(
                Reference<? extends Message.ClientRequest<?>> task,
                Promise<Message.ServerResponse<?>> delegate) {
            return new PendingTask(task, delegate);
        }

        protected final int xid;
        
        public PendingTask(
                Reference<? extends Message.ClientRequest<?>> task,
                Promise<Message.ServerResponse<?>> delegate) {
            super(task, delegate);
            this.xid = task().get().xid();
        }
        
        @Override
        public int xid() {
            return xid;
        }
        
        public Message.ClientRequest<?> getRequest() {
            return task().get();
        }
        
        @Override
        public void onSuccess(Message.ClientRequest<?> result) {
        }

        @Override
        public void onFailure(Throwable t) {
            setException(t);
        }

        @Override
        protected MoreObjects.ToStringHelper toStringHelper(MoreObjects.ToStringHelper toString) {
            return super.toStringHelper(toString.add("xid", xid));
        }
    } 
    
    public static class PendingPromiseTask extends PendingTask {

        public static PendingPromiseTask create(
                Promise<Message.ServerResponse<?>> promise,
                Reference<? extends Message.ClientRequest<?>> task,
                Promise<Message.ServerResponse<?>> delegate) {
            return new PendingPromiseTask(promise, task, delegate);
        }

        protected final Promise<Message.ServerResponse<?>> promise;

        public PendingPromiseTask(
                Promise<Message.ServerResponse<?>> promise,
                Reference<? extends Message.ClientRequest<?>> task,
                Promise<Message.ServerResponse<?>> delegate) {
            super(task, delegate);
            this.promise = promise;
        }

        public Promise<Message.ServerResponse<?>> getPromise() {
            return promise;
        }
    } 
    
    public static class Pending<
    O extends Operation.ProtocolResponse<?>,
    T extends PendingTask,
    C extends ProtocolConnection<? super Message.ClientSession, ? extends Operation.Response,?,?,?>> extends Actors.ExecutedPeekingQueuedActor<T> implements Connection.Listener<Operation.Response> {

        public static <O extends Operation.ProtocolResponse<?>,T extends PendingTask,C extends ProtocolConnection<? super Message.ClientSession, ? extends Operation.Response,?,?,?>> Pending<O,T,C> create(
                C connection,
                FutureCallback<? super T> callback,
                Logger logger) {
            return new Pending<O,T,C>(connection, callback, Queues.<T>newConcurrentLinkedQueue(), logger);
        }

        protected final FutureCallback<? super T> callback;
        protected final C connection;
        
        protected Pending(
                C connection,
                FutureCallback<? super T> callback,
                Queue<T> mailbox,
                Logger logger) {
            super(connection, mailbox, logger);
            this.callback = callback;
            this.connection = connection;
        }

        @Override
        protected synchronized boolean doSend(T message) {
            // we use synchronized to preserve queue/send ordering
            // task needs to be in the queue before calling write
            if (! mailbox.offer(message)) {
                return false;
            }
            try {
                if (! message.isDone()) {
                    // mark pings as done on send because ZooKeeper doesn't care about their ordering
                    if (message.xid() == OpCodeXid.PING.xid()) {
                        message.set(null);
                    }
                    Message.ClientRequest<?> request = message.getRequest();
                    if (request != null) {
                        ListenableFuture<? extends Message.ClientRequest<?>> writeFuture = connection.write(request);
                        Futures.addCallback(writeFuture, message);
                    }
                }
            } catch (Throwable t) {
                message.onFailure(t);
            }
            if (state() == State.TERMINATED) {
                message.cancel(true);
                apply(message);
            }
            if (! message.isDone()) {
                message.addListener(this, executor);
            } else {
                run();
            }
            return true;
        }
        
        @Override
        public void handleConnectionRead(Operation.Response message) {
            if (message instanceof Message.ServerResponse<?>) {
                int xid = ((Message.ServerResponse<?>) message).xid();
                if (! ((xid == OpCodeXid.PING.xid()) || (xid == OpCodeXid.NOTIFICATION.xid()))) {
                    Iterator<T> tasks = mailbox.iterator();
                    T task = null;
                    while (tasks.hasNext()) {
                        T next = tasks.next();
                        if ((next.xid() == xid) && !next.isDone()) {
                            task = next;
                            break;
                        }
                    }
                    if (task != null) {
                        task.set((Message.ServerResponse<?>) message);
                    } else if (state() != State.TERMINATED) {
                        // This could happen if someone submitted a message without
                        // going through us
                        // or, it could be a bug
                        logger.warn("{} xid doesn't match {} ({})", message, mailbox.peek(), this);
                    }
                }
            }
        }

        @Override
        public void handleConnectionState(Automaton.Transition<Connection.State> state) {
            // TODO
        }

        public boolean isReady() {
            T next = mailbox.peek();
            return ((next != null) && next.isDone());
        }
        
        @Override
        public String toString() {
            return MoreObjects.toStringHelper(this).addValue(callback).toString();
        }
        
        @Override
        protected void doStop() {
            T task;
            while ((task = mailbox.peek()) != null) {
                task.cancel(true);
                apply(task);
            }
        }

        @Override
        protected synchronized boolean apply(T input) {
            if (input.isDone()) {
                if (mailbox.remove(input)) {
                    callback.onSuccess(input);
                    return true;
                }
            }
            return false;
        }
    }

    public abstract class ForwardingActor extends Actors.ExecutedQueuedActor<T> {

        protected ForwardingActor(Executor executor,
                Logger logger) {
            super(executor, new ConcurrentLinkedQueue<T>(), logger);
        }
        
        public Logger logger() {
            return logger;
        }
        
        @Override
        protected void doStop() {
            PendingQueueClientExecutor.this.doStop();
            
            T request;
            while ((request = mailbox.poll()) != null) {
                request.cancel(true);
            }
        }
        
        @Override
        public String toString() {
            return MoreObjects.toStringHelper(this).addValue(PendingQueueClientExecutor.this).toString();
        }
    }
}
