/*
 *
 * Copyright (c) 2006 The Apache Software Foundation
 *
 * 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 org.apache.qpid.server.cluster;

import org.apache.qpid.AMQException;
import org.apache.qpid.server.cluster.util.LogMessage;
import org.apache.qpid.framing.AMQMethodBody;
import org.apache.log4j.Logger;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

/**
 * An implementation of the Member interface (through which data is sent to other
 * peers in the cluster). This class provides a base from which subclasses can
 * inherit some common behaviour for broadcasting GroupRequests and sending methods
 * that may expect a response. It also extends the Member abstraction to support
 * a richer set of operations that are useful within the package but should not be
 * exposed outside of it.
 *
 */
abstract class Broker extends SimpleMemberHandle implements Member
{
    private static final Logger _logger = Logger.getLogger(Broker.class);
    private static final int DEFAULT_CHANNEL = 1;
    private static final int START_CHANNEL = 2;
    private static final int END_CHANNEL = 10000;


    private MemberFailureListener _listener;
    //a wrap-around counter to allocate _requests a unique channel:
    private int _nextChannel = START_CHANNEL;
    //outstanding _requests:
    private final Map<Integer, ResponseHandler> _requests = new HashMap<Integer, ResponseHandler>();

    Broker(String host, int port)
    {
        super(host, port);
    }

    /**
     * Allows a listener to be registered that will receive callbacks when communication
     * to the peer this broker instance represents fails.
     * @param listener the callback to be notified of failures
     */
    public void addFailureListener(MemberFailureListener listener)
    {
        _listener = listener;
    }

    /**
     * Allows subclasses to signal comunication failures
     */
    protected void failed()
    {
        if (_listener != null)
        {
            _listener.failed(this);
        }
    }

    /**
     * Subclasses should call this on receiving message responses from the remote
     * peer.  They are matched to any outstanding request they might be response
     * to, with the completion and callback of that request being managed if
     * required.
     *
     * @param channel the channel on which the method was received
     * @param response the response received
     * @return  true if the response matched an outstanding request
     */
    protected synchronized boolean handleResponse(int channel, AMQMethodBody response)
    {
        ResponseHandler request = _requests.get(channel);
        if (request == null)
        {
            if(!_requests.isEmpty())
            {
                _logger.warn(new LogMessage("[next channel={3, integer}]: Response {0} on channel {1, integer} failed to match outstanding requests: {2}", response, channel, _requests, _nextChannel));
            }
            return false;
        }
        else
        {
            request.responded(response);
            return true;
        }
    }

    /**
     * Called when this broker is excluded from the group. Any requests made on
     * it are informed this member has left the group.
     */
    synchronized void remove()
    {
        for (ResponseHandler r : _requests.values())
        {
            r.removed();
        }
    }

    /**
     * Engages this broker in the specified group request
     *
     * @param request the request being made to a group of brokers
     * @throws AMQException if there is any failure
     */
    synchronized void invoke(GroupRequest request) throws AMQException
    {
        int channel = nextChannel();
        _requests.put(channel, new GroupRequestAdapter(request, channel));
        request.send(channel, this);
    }

    /**
     * Sends a message to the remote peer and undertakes to notify the specified
     * handler of the response.
     *
     * @param msg the message to send
     * @param handler the callback to notify of responses (or the removal of this broker
     * from the group)
     * @throws AMQException
     */
    synchronized void send(Sendable msg, ResponseHandler handler) throws AMQException
    {
        int channel;
        if (handler != null)
        {
            channel = nextChannel();
            _requests.put(channel, new RemovingWrapper(handler, channel));
        }
        else
        {
            channel = DEFAULT_CHANNEL;
        }

        msg.send(channel, this);
    }

    private int nextChannel()
    {
        int channel = _nextChannel++;
        if(_nextChannel >= END_CHANNEL)
        {
            _nextChannel = START_CHANNEL;
        }
        return channel;
    }

    /**
     * extablish connection without handling redirect
     */
    abstract boolean connect() throws IOException, InterruptedException;

    /**
     * Start connection process, including replay
     */
    abstract void connectAsynch(Iterable<AMQMethodBody> msgs);

    /**
     * Replay messages to the remote peer this instance represents. These messages
     * must be sent before any others whose transmission is requested through send() etc.
     *
     * @param msgs
     */
    abstract void replay(Iterable<AMQMethodBody> msgs);

    /**
     * establish connection, handling redirect if required...
     */
    abstract Broker connectToCluster() throws IOException, InterruptedException;

    private class GroupRequestAdapter implements ResponseHandler
    {
        private final GroupRequest request;
        private final int channel;

        GroupRequestAdapter(GroupRequest request, int channel)
        {
            this.request = request;
            this.channel = channel;
        }

        public void responded(AMQMethodBody response)
        {
            request.responseReceived(Broker.this, response);
            _requests.remove(channel);
        }

        public void removed()
        {
            request.removed(Broker.this);
        }

        public String toString()
        {
            return "GroupRequestAdapter{" + channel + ", " + request + "}";
        }
    }

    private class RemovingWrapper implements ResponseHandler
    {
        private final ResponseHandler handler;
        private final int channel;

        RemovingWrapper(ResponseHandler handler, int channel)
        {
            this.handler = handler;
            this.channel = channel;
        }

        public void responded(AMQMethodBody response)
        {
            handler.responded(response);
            _requests.remove(channel);
        }

        public void removed()
        {
            handler.removed();
        }

        public String toString()
        {
            return "RemovingWrapper{" + channel + ", " + handler + "}";
        }
    }
}
