/*
 * Copyright (c) 2015 ezivkoc
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

package com.zepchat.core.transport.discovery;

import com.zepchat.core.data.User;
import com.zepchat.core.transport.ZEPDiscoveryTransport;
import zutil.log.LogUtil;
import zutil.net.threaded.ThreadedUDPNetwork;
import zutil.net.threaded.ThreadedUDPNetworkThread;
import zutil.parser.DataNode;
import zutil.parser.json.JSONParser;
import zutil.parser.json.JSONWriter;

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.InetAddress;
import java.util.HashMap;
import java.util.Timer;
import java.util.TimerTask;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * @author Ziver
 */
public class JSONDiscoveryTransport extends ThreadedUDPNetwork implements ThreadedUDPNetworkThread, ZEPDiscoveryTransport {
	private static final Logger logger = LogUtil.getLogger();
	private static final int TIMEOUT = 10*60; // 10 min
	private static final String MULTICAST_ADDR = "239.255.255.250";
	private static final int PORT = 1901;

	// instance specific values
    private User localUser;
	private NotifyTimer notifyTimer = null;
    private UserStatusListener userListener;


	public JSONDiscoveryTransport(User localUser) throws IOException{
		super( null, MULTICAST_ADDR, PORT );
		super.setThread(this);

		this.localUser = localUser;

        notifyTimer = new NotifyTimer();
        Timer timer = new Timer();
        timer.schedule(new NotifyTimer(), 0, TIMEOUT * 1000 / 2);
	}


    public void setUserStatusListener(UserStatusListener userListener){
        this.userListener = userListener;
    }


	/**
	 * Handles the incoming packets
	 */
	public void receivedPacket(DatagramPacket packet, ThreadedUDPNetwork network) {
        DataNode root = JSONParser.read(new String( packet.getData()));

        if(root != null){
            if(root.get("Discover") != null){
                DataNode discover = root.get("Discover-Response");
                sendDiscoverResponse(packet.getAddress(), packet.getPort());
            }
            if(root.get("Discover-Response") != null){
                DataNode discovery = root.get("Discover-Response");
                User user = new User(
                        discovery.getString("Alias"),
                        discovery.getString("PublicKey"));
                if(userListener!= null) userListener.addRemoteUser(user);
            }
            if(root.get("Notify-Alive") != null){
                DataNode alive = root.get("Notify-Alive");
                if (userListener != null){
                    User user = new User(
                            alive.getString("Alias"),
                            alive.getString("PublicKey"));
                    userListener.addRemoteUser(user);
                }
            }
            if(root.get("Notify-Leave") != null){
                DataNode leave = root.get("Notify-Leave");
                if (userListener != null){
                    User user = new User(
                            leave.getString("Alias"),
                            leave.getString("PublicKey"));
                    userListener.removeRemoteUser(user);
                }
            }
        }
	}


    /**
     * Sends discover message to find new users on the network
     *
     * ********** Message ex:
     * 	{
     *		Notify-Alive: {
     *			Alias: "<SENDER_ALIAS>",
     *			PublicKey: "<SENDER_BASE64_PUBLIC_KEY>",
     *			Timeout: <SENDER_IP_VALIDITY_IN_SECONDS>,
     *			CRC: "<SIGNED_PUBLIC_KEY_SHA1>"
     *		}
     *	}
     */
    public void sendDiscover(){
        try {
            // Generate the JSON message
            DataNode discover = new DataNode(DataNode.DataType.Map);
            discover.set("Alias", localUser.getAlias());
            discover.set("PublicKey", localUser.getPublicKey());
            discover.set("Timeout", TIMEOUT);
            discover.set("CRC", "none");
            DataNode root = new DataNode(DataNode.DataType.Map);
            root.set("Discover", discover);
            String msg = JSONWriter.toString(root);

            logger.log(Level.FINEST, "NotifyAlive: " + msg);
            byte[] data = msg.toString().getBytes();
            DatagramPacket packet = new DatagramPacket(
                    data, data.length,
                    InetAddress.getByName( MULTICAST_ADDR ),
                    PORT );
            super.send( packet );

        } catch (Exception e) {
            logger.log(Level.SEVERE, null, e);
        }
    }


    /**
     * Sends discover message to find new users on the network
     *
     * ********** Message ex:
     * 	{
     *		Discover-Response: {
     *			Alias: "<SENDER_ALIAS>",
     *			PublicKey: "<SENDER_BASE64_PUBLIC_KEY>",
     *			Timeout: <SENDER_IP_VALIDITY_IN_SECONDS>,
     *			CRC: "<SIGNED_PUBLIC_KEY_SHA1>"
     *		}
     *	}
     */
    public void sendDiscoverResponse(InetAddress addr, int port){
        try {
            // Generate the JSON message
            DataNode discover = new DataNode(DataNode.DataType.Map);
            discover.set("Alias", localUser.getAlias());
            discover.set("PublicKey", localUser.getPublicKey());
            discover.set("Timeout", TIMEOUT);
            discover.set("CRC", "none");
            DataNode root = new DataNode(DataNode.DataType.Map);
            root.set("Discover-Response", discover);
            String msg = JSONWriter.toString(root);

            logger.log(Level.FINEST, "NotifyAlive: " + msg);
            byte[] data = msg.toString().getBytes();
            DatagramPacket packet = new DatagramPacket(
                    data, data.length,
                    addr,
                    port );
            super.send( packet );

        } catch (Exception e) {
            logger.log(Level.SEVERE, null, e);
        }
    }

    @Override
    public void scan() {
        sendDiscover();
    }


    /**
	 * This thread is a timer task that sends an
	 * alive message to the network every
	 * cache_time/2 seconds.
	 *
	 * @author Ziver
	 */
	private class NotifyTimer extends TimerTask {
		public void run(){
            sendNotifyAlive();
		}
	}

	/**
	 * Sends an keep-alive message to update all users
	 *
	 * ********** Message ex:
	 * 	{
	 *		Notify-Alive: {
	 *			Alias: "<SENDER_ALIAS>",
	 *			PublicKey: "<SENDER_BASE64_PUBLIC_KEY>",
	 *			Timeout: <SENDER_IP_VALIDITY_IN_SECONDS>,
	 *			CRC: "<SIGNED_PUBLIC_KEY_SHA1>"
	 *		}
	 *	}
	 */
	public void sendNotifyAlive(){
		try {
            // Generate the JSON message
            DataNode alive = new DataNode(DataNode.DataType.Map);
            alive.set("Alias", localUser.getAlias());
            alive.set("PublicKey", localUser.getPublicKey());
            alive.set("Timeout", TIMEOUT);
            alive.set("CRC", "none");
            DataNode root = new DataNode(DataNode.DataType.Map);
            root.set("Notify-Alive", alive);
            String msg = root.getString();

			logger.log(Level.FINEST, "NotifyAlive: " + msg);
			byte[] data = msg.toString().getBytes();
			DatagramPacket packet = new DatagramPacket( 
					data, data.length, 
					InetAddress.getByName( MULTICAST_ADDR ),
					PORT );
			super.send( packet );

		} catch (Exception e) {
			logger.log(Level.SEVERE, null, e);
		}
	}


	/**
	 * Send a disconnect message to all users
	 * 
	 * **********  Message ex:
	 *	{
	 *		Notify-Leave: {
	 *			PublicKey: "<SENDER_BASE64_PUBLIC_KEY>",
	 *			CRC: "<SIGNED_PUBLIC_KEY_SHA1>"
	 *		}
	 *	}
	 */
	public void sendNotifyLeave(){
		try {
			// Generate the JSON message
            DataNode leave = new DataNode(DataNode.DataType.Map);
            leave.set("PublicKey", localUser.getPublicKey());
            leave.set("CRC", "none");
            DataNode root = new DataNode(DataNode.DataType.Map);
            root.set("Notify-Leave", leave);
			String msg = root.getString();

			logger.log(Level.FINEST, "NotifyLeave: " + msg);
			byte[] data = msg.toString().getBytes();
			DatagramPacket packet = new DatagramPacket( 
					data, data.length, 
					InetAddress.getByName( MULTICAST_ADDR ),
					PORT );
			super.send( packet );

		} catch (Exception e) {
			logger.log(Level.SEVERE, null, e);
		}
	}
}
