/**
 *  Copyright 2014 Red Hat, Inc.
 *
 *  Red Hat licenses this file to you 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.openshift.ping.common;

import static org.openshift.ping.common.Utils.getSystemEnvInt;
import static org.openshift.ping.common.Utils.trimToNull;

import java.io.InputStream;
import java.lang.reflect.Method;
import java.net.InetSocketAddress;
import java.util.Collections;
import java.util.List;

import org.jgroups.Event;
import org.jgroups.Message;
import org.jgroups.PhysicalAddress;
import org.jgroups.annotations.Property;
import org.jgroups.protocols.PING;
import org.jgroups.stack.IpAddress;
import org.jgroups.stack.Protocol;
import org.openshift.ping.common.compatibility.CompatibilityException;
import org.openshift.ping.common.compatibility.CompatibilityUtils;
import org.openshift.ping.common.server.ServerFactory;

public abstract class OpenshiftPing extends PING {

    private String clusterName;

    private final String _systemEnvPrefix;

    @Property
    private int connectTimeout = 5000;
    private int _connectTimeout;

    @Property
    private int readTimeout = 30000;
    private int _readTimeout;

    @Property
    private int operationAttempts = 3;
    private int _operationAttempts;

    @Property
    private long operationSleep = 1000;
    private long _operationSleep;

    private static Method sendDownMethod; //handled via reflection due to JGroups 3/4 incompatibility

    public OpenshiftPing(String systemEnvPrefix) {
        super();
        _systemEnvPrefix = trimToNull(systemEnvPrefix);
        try {
            if(CompatibilityUtils.isJGroups4()) {
                sendDownMethod = Protocol.class.getMethod("down", Message.class);
            } else {
                sendDownMethod = Protocol.class.getMethod("down", Event.class);
            }
        } catch (Exception e) {
            throw new CompatibilityException("Could not find suitable 'up' method.", e);
        }
    }

    protected final String getSystemEnvName(String systemEnvSuffix) {
        StringBuilder sb = new StringBuilder();
        String suffix = trimToNull(systemEnvSuffix);
        if (suffix != null) {
            if (_systemEnvPrefix != null) {
                sb.append(_systemEnvPrefix);
            }
            sb.append(suffix);
        }
        return sb.length() > 0 ? sb.toString() : null;
    }

    protected final int getConnectTimeout() {
        return _connectTimeout;
    }

    protected final int getReadTimeout() {
        return _readTimeout;
    }

    protected final int getOperationAttempts() {
        return _operationAttempts;
    }

    protected final long getOperationSleep() {
        return _operationSleep;
    }

    protected abstract boolean isClusteringEnabled();

    protected abstract int getServerPort();

    public final void setServerFactory(ServerFactory serverFactory) {
    }

    @Override
    public void init() throws Exception {
        super.init();
        _connectTimeout = getSystemEnvInt(getSystemEnvName("CONNECT_TIMEOUT"), connectTimeout);
        _readTimeout = getSystemEnvInt(getSystemEnvName("READ_TIMEOUT"), readTimeout);
        _operationAttempts = getSystemEnvInt(getSystemEnvName("OPERATION_ATTEMPTS"), operationAttempts);
        _operationSleep = (long) getSystemEnvInt(getSystemEnvName("OPERATION_SLEEP"), (int) operationSleep);
    }

    @Override
    public void destroy() {
        _connectTimeout = 0;
        _readTimeout = 0;
        _operationAttempts = 0;
        _operationSleep = 0l;
        super.destroy();
    }

    @Override
    public void start() throws Exception {
        super.start();
    }

    @Override
    public void stop() {
        super.stop();
    }

    public Object down(Event evt) {
        switch (evt.getType()) {
        case Event.CONNECT:
        case Event.CONNECT_WITH_STATE_TRANSFER:
        case Event.CONNECT_USE_FLUSH:
        case Event.CONNECT_WITH_STATE_TRANSFER_USE_FLUSH:
            clusterName = (String) evt.getArg();
            break;
        }
        return super.down(evt);
    }

    public void handlePingRequest(InputStream stream) throws Exception {
        throw new UnsupportedOperationException("handlePingRequest() is no longer supported.");
    }

    private void sendDown(Object obj, Message msg) {
        try {
            if(CompatibilityUtils.isJGroups4()) {
                sendDownMethod.invoke(obj, msg);
            } else {
                sendDownMethod.invoke(obj, new Event(1, msg));
            }
        } catch (Exception e) {
            throw new CompatibilityException("Could not invoke 'down' method.", e);
        }
    }

    private List<InetSocketAddress> readAll() {
        if (isClusteringEnabled()) {
            return doReadAll(clusterName);
        } else {
            return Collections.emptyList();
        }
    }

    protected abstract List<InetSocketAddress> doReadAll(String clusterName);

    @Override
    protected void sendMcastDiscoveryRequest(Message msg) {
        final List<InetSocketAddress> hosts = readAll();
        final PhysicalAddress physical_addr = (PhysicalAddress) down(new Event(Event.GET_PHYSICAL_ADDRESS, local_addr));
        if (!(physical_addr instanceof IpAddress)) {
            log.error("Unable to send PING requests: physical_addr is not an IpAddress.");
            return;
        }
        // XXX: is it better to force this to be defined?
        // assume symmetry
        final int port = ((IpAddress) physical_addr).getPort();
        for (InetSocketAddress host: hosts) {
            // JGroups messages cannot be reused - https://github.com/belaban/workshop/blob/master/slides/admin.adoc#problem-9-reusing-a-message-the-sebastian-problem
            Message msgToHost = msg.copy();
            msgToHost.dest(new IpAddress(host.getAddress(), port));
            sendDown(down_prot, msgToHost);
        }
    }

}