/*
 * Copyright (c) 1998, 2018 Oracle and/or its affiliates. All rights reserved.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v. 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0,
 * or the Eclipse Distribution License v. 1.0 which is available at
 * http://www.eclipse.org/org/documents/edl-v10.php.
 *
 * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
 */

// Contributors:
//     Oracle - initial API and implementation from Oracle TopLink
package org.eclipse.persistence.sessions.coordination;

import java.net.*;
import java.io.IOException;
import org.eclipse.persistence.exceptions.DiscoveryException;
import org.eclipse.persistence.internal.sessions.coordination.*;
import org.eclipse.persistence.sessions.SessionProfiler;

/**
 * <p>
 * <b>Purpose</b>: Detects new members of a logical EclipseLink cluster.
 * <p>
 * <b>Description</b>: Each RemoteCommandManager has its own DiscoveryManager,
 * which handles the detection of other remote command services as they become available.
 * The DiscoveryManager is an active object (in that it extends Thread) and becomes
 * a separate thread when it is started using startDiscovery().
 * <p>
 * Discovery is done through the use of a multicast. Each discovery manager
 * joins the multicast group and announces itself to the group. As it receives
 * service announcements from other discovery managers it notifies the RCM to
 * establish connections to and from the new service.
 *
 * @see RemoteCommandManager
 * @see java.net.MulticastSocket
 * @author Steven Vo
 * @since OracleAS TopLink 10<i>g</i> (9.0.4)
 */
public class DiscoveryManager implements Runnable {

    /** Default value constants */
    public static final String DEFAULT_MULTICAST_GROUP = "226.10.12.64";
    public static final int DEFAULT_MULTICAST_PORT = 3121;
    public static final int DEFAULT_PACKET_TIME_TO_LIVE = 2;
    public static final int DEFAULT_ANNOUNCEMENT_DELAY = 1000;

    /** Defines the IP address of the multicast group */
    protected String multicastGroupAddress = DEFAULT_MULTICAST_GROUP;

    /** Defines the port the multicast socket will be using */
    protected int multicastPort = DEFAULT_MULTICAST_PORT;

    /** The multicast socket used for discovery */
    protected MulticastSocket communicationSocket;

    /**
     * Number of hops in the life of the datapacket
     * Default is 2, a hub and an interface card to prevent the data packets from leaving the localnetwork.
     */
    protected int packetTimeToLive = DEFAULT_PACKET_TIME_TO_LIVE;

    /** Indicates to the listening thread that it should stop */
    protected boolean stopListening = false;

    /** Delay time in millis between initialization and when the announcement is sent */
    protected int announcementDelay = DEFAULT_ANNOUNCEMENT_DELAY;

    /** The remote command manager responsible for this service */
    protected RemoteCommandManager rcm;

    /**
     * Constructors to create a discovery manager.
     */
    public DiscoveryManager(RemoteCommandManager mgr) {
        this.rcm = mgr;
    }

    public DiscoveryManager(String address, int port, RemoteCommandManager mgr) {
        this(mgr);
        this.multicastGroupAddress = address;
        this.multicastPort = port;
    }

    public DiscoveryManager(String address, int port, int delay, RemoteCommandManager mgr) {
        this(address, port, mgr);
        this.announcementDelay = delay;
    }

    /**
     * INTERNAL:
     * Send out an announcement that we are here.
     */
    public void announceSession() {
        rcm.logDebug("sending_announcement", (Object[])null);

        ServiceAnnouncement outMsg = new ServiceAnnouncement(rcm.getServiceId());
        byte[] outBytes = outMsg.toBytes();

        try {
            // Create a packet to send and send it out to everyone listening
            DatagramPacket sendPacket = new DatagramPacket(outBytes, outBytes.length, InetAddress.getByName(multicastGroupAddress), multicastPort);
            getCommunicationSocket().send(sendPacket);

            Object[] args = null;
            rcm.logInfo("announcement_sent", args);

        } catch (Exception ex) {
            // We got an exception. Map it to an RCM exception and call the handler
            DiscoveryException discoveryEx = DiscoveryException.errorSendingAnnouncement(ex);
            rcm.handleException(discoveryEx);
        }
    }

    /**
     * ADVANCED:
     * Announce the local service and join the cluster
     */
    public void startDiscovery() {
        this.rcm.getCommandProcessor().updateProfile(SessionProfiler.RcmStatus, "Started");

        // Only start if we are currently stopped
        if (this.isDiscoveryStopped()) {
            this.rcm.getServerPlatform().launchContainerRunnable(this);
        }
    }

    /**
     * ADVANCED:
     * Stop accepting announcements from other services becoming available.
     * Note that this does not remove the local service from the cluster.
     */
    public void stopDiscovery() {
        this.rcm.getCommandProcessor().updateProfile(SessionProfiler.RcmStatus, "Stopped");

        stopListening();
        try {
            // Put in a sleep to give the listener thread a chance to stop
            Thread.sleep(500);
        } catch (InterruptedException exception) {
        }
        this.communicationSocket = null;
    }

    /**
     * ADVANCED:
     * Return true if discovery has been stopped at the time this method is called.
     * If false is returned then it is undefined whether discovery is started or
     * stopped. It may be started, or it may be in the process of starting or
     * stopping.
     */
    public boolean isDiscoveryStopped() {
        return (communicationSocket == null);
    }

    /**
     * INTERNAL:
     * Create the multicast socket and join the multicast group.
     */
    public void createCommunicationSocket() {
        Object[] args = { multicastGroupAddress, "" + multicastPort };
        rcm.logDebug("initializing_discovery_resources", args);
        if (communicationSocket == null) {
            try {
                communicationSocket = new MulticastSocket(multicastPort);
                communicationSocket.setTimeToLive(getPacketTimeToLive());
                communicationSocket.joinGroup(InetAddress.getByName(multicastGroupAddress));
            } catch (IOException ex) {
                // Either we couldn't create the socket or we couldn't join the group
                DiscoveryException discoveryEx = DiscoveryException.errorJoiningMulticastGroup(ex);
                rcm.handleException(discoveryEx);
            }
        }
    }

    /**
      * INTERNAL:
      * Return the socket that will be used for the multicast.
      */
    public MulticastSocket getCommunicationSocket() {
        return communicationSocket;
    }

    /**
     * INTERNAL:
     * This is the main execution method of discovery.  It will create a socket to
     * listen on, create a local connection for this service and announce that we are
     * open for business.
    */
    public void run() {
        //Initialize the communication socket
        createCommunicationSocket();

        // Create the local connection from which we will receive commands
        rcm.getTransportManager().createLocalConnection();

        // Announce to all other discovery managers that this service is up. The
        // delay allows time for posting of connections to the name service
        try {
            Thread.sleep(announcementDelay);
        } catch (InterruptedException exception) {
        }
        announceSession();

        // Listen for other sessions that are joining
        startListening();
    }

    /**
     * INTERNAL:
     * This method puts us into the listening mode loop. This thread blocks, waiting
     * on announcements that we receive from other discovery managers.
     */
    public void startListening() {
        byte[] recvBuf = new byte[128];

        // Only stop when we get the directive to stop
        stopListening = false;
        rcm.logInfo("discovery_manager_active", (Object[])null);
        while (!stopListening) {
            DatagramPacket recvPacket = new DatagramPacket(recvBuf, recvBuf.length);
            ServiceAnnouncement inMsg;

            // Block waiting for a message
            try {
                getCommunicationSocket().receive(recvPacket);
            } catch (IOException exception) {
                if (stopListening) {
                    // We caused the exception by closing the socket
                    rcm.logInfo("discovery_manager_stopped", (Object[])null);
                    return;
                } else {
                    // Exception was caused by something else (e.g. network error, etc.)
                    rcm.handleException(DiscoveryException.errorReceivingAnnouncement(exception));
                }
            }

            // We received a message, unmarshall it into an announcement
            try {
                inMsg = new ServiceAnnouncement(recvPacket.getData());
            } catch (Exception ex) {
                // Log a warning that we couldn't process the announcement
                Object[] args = { ex };
                rcm.logWarning("received_corrupt_announcement", args);
                continue;
            }

            // If the msg is not from ourselves, and is announcing a service on
            // the same channel as we are on then we should do something about it
            if (!rcm.getServiceId().getId().equals(inMsg.getServiceId().getId()) && (rcm.getServiceId().getChannel().equalsIgnoreCase(inMsg.getServiceId().getChannel()))) {
                receivedAnnouncement(inMsg.getServiceId());
            }
        }
    }

    /**
     * INTERNAL:
     * Signal this instance to stop listening.
     */
    public void stopListening() {
        this.stopListening = true;
        if (getCommunicationSocket() != null) {
            getCommunicationSocket().close();
        }
    }

    /**
     * INTERNAL:
     * Process the announcement that indicates that a new service is online
     */
    public void receivedAnnouncement(ServiceId serviceId) {
        Object[] args = { serviceId };
        rcm.logInfo("announcement_received", args);
        // Notify the RCM that a new service has been detected
        rcm.newServiceDiscovered(serviceId);
    }

    /**
     * PUBLIC:
     * Set the amount of time in millis that the service should wait between the time
     * that this Remote Service is available and a session announcement is sent out
     * to other discovery managers. This may be needed to give some systems more time
     * to post their connections into the naming service. Takes effect the next time
     * listening is started.
     */
    public void setAnnouncementDelay(int millisecondsToDelay) {
        announcementDelay = millisecondsToDelay;
    }

    /**
     * PUBLIC:
     * Return the amount of time in millis that the service should wait between the time
     * that this Remote Service is available and a session announcement is sent out
     * to other discovery managers.
     */
    public int getAnnouncementDelay() {
        return announcementDelay;
    }

    /**
     * PUBLIC:
     * Return the host address of the multicast group.
     */
    public String getMulticastGroupAddress() {
        return (multicastGroupAddress);
    }

    /**
     * PUBLIC:
     * Set the host address of the multicast group. Takes effect the next time
     * listening is started.
     */
    public void setMulticastGroupAddress(String address) {
        this.multicastGroupAddress = address;
    }

    /**
     * PUBLIC:
     * Set the multicast port used for discovery. Takes effect the next time
     * listening is started.
     */
    public void setMulticastPort(int port) {
        this.multicastPort = port;
    }

    /**
     * PUBLIC:
     * Return the multicast port used for discovery.
     */
    public int getMulticastPort() {
        return (multicastPort);
    }

    /**
     * INTERNAL: invoke when the RCM shutdown. Subclass overrides this method if necessary.
     */
    protected void shallowCopy(DiscoveryManager dmgr) {
        this.multicastGroupAddress = dmgr.multicastGroupAddress;
        this.multicastPort = dmgr.multicastPort;
        this.announcementDelay = dmgr.announcementDelay;
        this.rcm = dmgr.rcm;
    }

    /**
     * PUBLIC:
     * Returns the number of hops the data packets of the session announcement will take before expiring.
     * The default is 2, a hub and an interface card to prevent the data packets from leaving the local network.
     */
    public int getPacketTimeToLive() {
        return this.packetTimeToLive;
    }

    /**
     * PUBLIC:
     * Set the number of hops the data packets of the session announcement will take before expiring.
     * The default is 2, a hub and an interface card to prevent the data packets from leaving the local network.
     *
     * Note that if sessions are hosted on different LANs that are part of WAN, the announcement sending by one session
     * may not reach other sessions.  In this case, consult your network administrator for the right time-to-live value
     * or test your network by increase the value until sessions receive announcement sent by others.
     */
    public void setPacketTimeToLive(int newPacketTimeToLive) {
        this.packetTimeToLive = newPacketTimeToLive;
    }
}
