/*
 * Copyright [2012] [ShopWiki]
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.shopwiki.roger.rpc;

import java.io.IOException;
import java.util.*;
import java.util.concurrent.*;

import org.codehaus.jackson.type.TypeReference;

import com.google.common.util.concurrent.AbstractFuture;
import com.rabbitmq.client.*;
import com.rabbitmq.client.AMQP.BasicProperties;
import com.shopwiki.roger.*;

/**
 * Mainly for testing a {@link RpcServer}s.
 *
 * @param <O> response type
 *
 * @author rstewart
 */
public class RpcClient<O> {

    private static final boolean DEBUG = MessagingUtil.DEBUG;

    private final Channel channel;
    private final Route requestRoute;
    private final TypeReference<O> responseType;
    private final boolean exceptionsAsJson;

    private final String responseQueueName;

    // TODO: Make this a Guava Cache with TTLs on the entries ???
    private final Map<String,RpcFuture<O>> idToFuture = new ConcurrentHashMap<String,RpcFuture<O>>();

    /**
     * @param channel
     * @param requestRoute
     * @param responseQueueArgs
     * @param exceptionAsJson
     * @return RpcClient that returns all responses as MapMessages.
     * @throws IOException
     */
    public static RpcClient<MapMessage> create(
            Channel channel,
            Route requestRoute,
            Map<String,Object> responseQueueArgs,
            boolean exceptionAsJson
            ) throws IOException {

        return new RpcClient<MapMessage>(channel, requestRoute, responseQueueArgs, MapMessage.TYPE_REF, exceptionAsJson);
    }

    /**
     * @param channel
     * @param requestRoute
     * @param responseQueueArgs
     * @param responseType
     * @return RpcClient that returns responses using the supplied responseType.
     * @throws IOException
     */
    public static <O> RpcClient<O> create(
            Channel channel,
            Route requestRoute,
            Map<String,Object> responseQueueArgs,
            TypeReference<O> responseType
            ) throws IOException {

        return new RpcClient<O>(channel, requestRoute, responseQueueArgs, responseType, false);
    }

    private RpcClient(
            Channel channel,
            Route requestRoute,
            Map<String,Object> responseQueueArgs,
            TypeReference<O> responseType,
            boolean exceptionsAsJson
            ) throws IOException {

        this.channel = channel;
        this.requestRoute = requestRoute;
        this.responseType = responseType;
        this.exceptionsAsJson = exceptionsAsJson;

        if (exceptionsAsJson && ! responseType.getType().equals(MapMessage.TYPE_REF.getType())) {
            throw new IllegalArgumentException("Can't have exceptionsAsJson unless your responseType is MapMessage!");
        }

        responseQueueName = QueueUtil.declareAnonymousQueue(channel, responseQueueArgs).getQueue();
        Consumer responseConsumer = new ResponseConsumer(channel);
        channel.basicConsume(responseQueueName, true, responseConsumer);
    }

    private class ResponseConsumer extends DefaultConsumer {

        public ResponseConsumer(Channel channel) {
            super(channel);
        }

        @Override
        public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties props, byte[] body) throws IOException {
            // AUTO-ACKING

            RpcFuture<O> future = idToFuture.remove(props.getCorrelationId());
            if (future == null) {
                System.err.println("### Received a response not meant for me! ###");
                System.err.println("### " + MessagingUtil.prettyPrint(props));
                System.err.println("### " + MessagingUtil.prettyPrintMessage(body));
                return;
            }

            ResponseStatus status = RpcResponse.getStatus(props);

            final Object debugMessage;

            if (status == ResponseStatus.OK || exceptionsAsJson) {
                O message = MessagingUtil.getDeliveryBody(body, responseType);
                debugMessage = message;
                RpcResponse<O> response = new RpcResponse<O>(props, message);
                future.complete(response);
            } else {
                MapMessage message = MessagingUtil.getDeliveryBody(body, MapMessage.TYPE_REF);
                debugMessage = message;
                future.completeException(status, message);
            }

            if (DEBUG) {
                System.out.println("*** ResponseConsumer RECEIVED RESPONSE ***");
                System.out.println("*** consumerTag: " + consumerTag);
                System.out.println("*** envelope:\n" + MessagingUtil.prettyPrint(envelope));
                System.out.println("*** properties:\n" + MessagingUtil.prettyPrint(props));
                System.out.println("*** response: " + MessagingUtil.prettyPrintMessage(debugMessage));
            }
        }
    }

    /**
     * @param request
     * @return response
     * @throws IOException
     */
    public Future<RpcResponse<O>> sendRequest(Object request) throws IOException {
        String id = java.util.UUID.randomUUID().toString();
        RpcFuture<O> future = new RpcFuture<O>();
        idToFuture.put(id, future);
        MessagingUtil.sendRequest(channel, requestRoute, request, responseQueueName, id);
        return future;
    }

    private static class RpcFuture<T> extends AbstractFuture<RpcResponse<T>> {
        void complete(RpcResponse<T> response) {
            set(response);
        }

        void completeException(ResponseStatus status, MapMessage exceptionResponse) {
            String exceptionName = (String)exceptionResponse.get("exceptionName");
            String exceptionMsg  = (String)exceptionResponse.get("exceptionMsg");
            Exception e = new Exception(status + ": " + exceptionName + "\n" + exceptionMsg);
            setException(e);
        }
    }
}

@@ -1386,16 +1386,190 @@
 entries 
+so we don't leak Futures that never complete ???%0A    // Or better yet... make it a Map of WeakReferences, and have a background thread delete keys that no longer have values 
 ???%0A    

Update Roger TODO in RpcClient about not leaking Futures that never complete