import java.applet.Applet;
import java.applet.AudioClip;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Toolkit;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileWriter;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.HttpURLConnection;
import java.net.InetAddress;
import java.net.URL;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.StringTokenizer;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;



/**
 * Copyright (c) 2009, Jeremy Shupe
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *   1. Redistributions of source code must retain the above copyright
 *      notice, this list of conditions and the following disclaimer.
 *   2. Redistributions in binary form must reproduce the above copyright
 *      notice, this list of conditions and the following disclaimer in the
 *      documentation and/or other materials provided with the distribution.
 *   3. Neither the name, Netrek, nor the
 *      names of its contributors may be used to endorse or promote products
 *      derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY Jeremy Shupe ''AS IS'' AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL <copyright holder> BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 * 
 * @class TPoller.java
 * 
 * Poll the meta server and update the display as an
 * alert to the user. 
 * 
 * 4-23-09 Added audio alerts that will be played once per event. Meaning that the
 * poller will not play the t-mode sound with every iteration, but every time the number
 * of players moves from 7 to 8.  Likewise the 7 player clip will play every time the 
 * number of players moves from 7 to 8, or 6 to 7
 * 4-24-09 Refined the audio alerts to play the "T Lost" alert whenever T is lost, and to
 * play the "T Gained" sound whenever T starts.  These sounds override the number of
 * players sound clips.
 * 4-27-09 Experimental version of the poller.  This is a UDP test class.
 *
 * @author Jeremy Shupe
 * @version 3-18-09
 */
public class TWatcher extends JFrame implements Runnable
{
    public static final String SEPARATOR = System.getProperties().getProperty("file.separator");
    public static final int POLL_DELAY = 15000;
    public static final String METASERVER_ADDRESS = "metaserver.us.netrek.org";
    public static final String METASERVER_ADDRESS_2 = "metaserver.netrek.org";
    public static final String[] SERVER_LIST = {METASERVER_ADDRESS, METASERVER_ADDRESS_2};
    public static final int DEFAULT_WIDTH = 150;
    public static final int DEFAULT_HEIGHT = 60;
    public static final double MAX_SCREEN_PERCENT = .15;
    //Red alert = 0-4 players
    public static final int RED_ALERT = 1010101;
    //Yellow alert = 5-7 players
    public static final int YELLOW_ALERT = 2020202;
    //Green alert = 8+ players
    public static final int GREEN_ALERT = 3030303;
    //Malfunction
    public static final int ORANGE_ALERT = 4040404;
    //Start up
    public static final int BLUE_ALERT = 5050505;
    
    //Runtime variables
    private int myDelay;
    private boolean run = true, wait = false;
    private JPanel myPanel = new JPanel();
    private JLabel playerCount;
    private boolean debug = false;
    private String inactiveMessage = "Not Connected";
    
    //Sound variables
    private AudioClip acTModeStart, acTModeEnd, acAlert7, acAlert6;
    private JarFile file;
    public static final int SOUND_OFF = 1000;
    public static final int SOUND_ON = 1001;
    private boolean tMode = false;
    private boolean played7 = false;
    private boolean played6 = false;
    
    //Error reporting
    private List<String> errorLog = new ArrayList<String>();
    private String path = "C:/Netrek_Errors/";
    private String filePath = path + "ErrorLog_" + System.currentTimeMillis() + "_.txt";
    
    //UDP Comm variables
    private byte[] receiveData = new byte[1024];
    
    
    /**
     * No argument constructor created for testing.
     */
    public TWatcher()
    {}
    
    
    /**
     * Initiate the program and poll the meta server once per delay.
     * Note:  Tests where the poll delay is less than 6 seconds, resulted in a cascade
     * failure, basically the ports, and variables don't have time to reset before the 
     * next iteration.  This results in an error that will continue until the virtual
     * machine cleans itself up.  Obviously, the longer the poll delay the less likely the
     * code is to fail. 
     * @param delay  The delay between polls of the metaserver.
     */
    public TWatcher(int delay)
    {
        myDelay = delay;
        myPanel.setBackground(Color.BLUE);
        this.add(myPanel);
        playerCount = new JLabel(inactiveMessage);
        myPanel.add(playerCount);
        this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
        Dimension mySize = new Dimension(Math.min(DEFAULT_WIDTH, (int)(screenSize.width * MAX_SCREEN_PERCENT)),
                Math.min(DEFAULT_HEIGHT, (int)(screenSize.height * MAX_SCREEN_PERCENT)));
        this.setLocation(screenSize.width - (Math.max(125, mySize.width)), screenSize.height - (int)(mySize.height * 1.5));
        this.setSize(mySize);
        this.setVisible(true);
        loadSounds();
        run();
    }
    
    
    /**
     * Execute the program.
     * 1. Connect to the meta server.
     * 2. Parse the response.
     * 3. Update the display.
     * 4. Sleep.
     */
    public void run()
    {
        while(run)
        {
            try
            {
                int maxPlayers = -1;
                wait = false;
                //1. Iterate through the server list and connect to each server.
                //Try to get the information via UDP, if that fails get it by TCP/IP
                for(int i = 0; i < SERVER_LIST.length; i++)
                {
                    int playerCount = -1;
                    
                    //UDP
                    playerCount = determineStatusUDP(SERVER_LIST[i]);
                    
                    //Check for failure
                    if(playerCount < 0)
                        //UDP failed get by TCP/IP
                        playerCount = determineStatusTCPIP(SERVER_LIST[i]);
                    
                    //Set to the max value
                    if(playerCount > maxPlayers)
                        maxPlayers = playerCount;
                }

                //3. Update the display.
                int status = ORANGE_ALERT;
                if(maxPlayers >= 8)
                    status = GREEN_ALERT;
                else if(maxPlayers > 4 && maxPlayers < 8 )
                    status = YELLOW_ALERT;
                else if(maxPlayers <= 4 && maxPlayers >= 0)
                    status = RED_ALERT;
                
                switch(status)
                {
                    case(RED_ALERT):
                        myPanel.setBackground(Color.RED);break;
                    case(YELLOW_ALERT):
                        myPanel.setBackground(Color.YELLOW);break;
                    case(GREEN_ALERT):
                        myPanel.setBackground(Color.GREEN);break;
                    case(ORANGE_ALERT):
                        myPanel.setBackground(Color.ORANGE);break;
                    case(BLUE_ALERT):
                        myPanel.setBackground(Color.BLUE);break;
                }
                if(maxPlayers >= 0)
                {
                    if(!wait)
                    {
                        this.playerCount.setText("T-Status: " + maxPlayers + " Players");
                        this.setTitle(maxPlayers + " players");
                    }
                    else
                    {
                        this.playerCount.setText("Wait Queue: " + maxPlayers + " Players");
                        this.setTitle("Wait Queue " + maxPlayers + " Players");
                    }
                    playSounds(maxPlayers);
                }
                else
                {
                    this.playerCount.setText(inactiveMessage);
                    this.setTitle("No information");
                }
                //4. Sleep.
                Thread.sleep(myDelay);
            }
            catch(Exception e)
            {
                errorLog.add("ERROR: " + e.getMessage());
                File file = new File(path);
                file.mkdirs();
                try
                {
                    PrintWriter pwriter = new PrintWriter((new FileWriter(new File(filePath))));
                    for(int j = 0; j < errorLog.size(); j++)
                        pwriter.println(errorLog.get(j));
                    pwriter.close();
                }
                catch(Exception exception)
                {
                    exception.printStackTrace();
                }
                e.printStackTrace();
            }
        }
    }
    
    
    /**
     * Try to get the information needed by the poller via a UDP packet.
     * If the method returns -1, the the program will assume that the 
     * UDP attempt failed for some reason and attempt to get the information
     * via TCP/IP.
     * @param servers  The meta server that we are currently trying to hit.
     * @return  The number of max number of players currently logged on.
     */
    private int determineStatusUDP(String server)
    {
        int maxPlayers = -1;
        try
        {
            //Create the socket.
            DatagramSocket socket = new DatagramSocket();
            socket.setSoTimeout(5000);
            InetAddress ipAddress = InetAddress.getByName(server);
            //Create and send the packet.
            String request = "?version=netrek-game-on";
            byte[] sendData = request.getBytes();
            DatagramPacket packet = new DatagramPacket(sendData, sendData.length, ipAddress, 3521); 
            socket.send(packet);
            //Create the receive packet, and get the response.
            DatagramPacket receivePacket = new DatagramPacket(receiveData, receiveData.length);
            socket.receive(receivePacket);
            String response = (new String(receivePacket.getData(), 0, receivePacket.getLength())).trim();
            //Close the socket.
            socket.close();
            //Response was null or blank, assume failure and move on.
            if(response == null || response.equals(""))
                return maxPlayers;
            //Response was valid, parse it and return the max number of players
            //Break the response down by line
            StringTokenizer lineTokenizer = new StringTokenizer(response, "\n");
            String lineData = lineTokenizer.nextToken();
            int count = 0;
            while(lineData != null)
            {
            	if(count > 0)
            	{
            		StringTokenizer tokenizer = new StringTokenizer(lineData, ",");
            		//address
            		String data = tokenizer.nextToken();
		            while(tokenizer.hasMoreTokens())
		            {
	                    //port
	                    data = tokenizer.nextToken().trim();
	                    //status
	                    data = tokenizer.nextToken().trim();
	                    //last update
	                    data = tokenizer.nextToken().trim();
	                    //players connected
	                    data = tokenizer.nextToken().trim();
	                    int numPlayers = new Integer(data);
	                    if(numPlayers > maxPlayers)
	                        maxPlayers = numPlayers;
	                    //length of queue
	                    data = tokenizer.nextToken().trim();
	                    {
	                        int queue = new Integer(data);
	                        if(queue > 0)
	                        {
	                            wait = true;
	                            maxPlayers += new Integer(data);
	                        }
	                    }
		                data = tokenizer.nextToken();
		            }
            	}
            	if(lineTokenizer.hasMoreTokens())
            		lineData = lineTokenizer.nextToken();
            	else
            		lineData = null;
	            count++;
            }
        }
        catch(Exception exception)
        {
            errorLog.add("ERROR: " + exception.getMessage());
        }
        return maxPlayers;
    }
    
    
    /**
     * Get the information needed by the poller using TCP/IP.  This is a 
     * back up to the UDP method, and is meant to only be used if the UDP method fails.
     * @param servers  The meta server that we are currently trying to hit.
     * @return  The number of players currently logged on.
     */
    private int determineStatusTCPIP(String server)
    {
        int maxPlayers = -1;
        try
        {
            //1. Connect to the meta server.
            server = "http://" + server + ":3521";
            URL meta = new URL(server);
            HttpURLConnection huc = (HttpURLConnection)meta.openConnection();            
            huc.setRequestMethod("GET");
            huc.connect();
            
            //2. Parse the response.
            BufferedReader reader = new BufferedReader(new InputStreamReader(huc.getInputStream()));
            List<String> servers = new ArrayList<String>();
            String data = reader.readLine();
            while(data != null)
            {
                if(data.startsWith("-h"))
                    servers.add(data);
                if(data.endsWith("it!"))
                {
                    huc.disconnect();
                    data = null;
                }
                else
                    data = reader.readLine();
            }
    
            for(int i = 0; i < servers.size(); i++)
            {
                if(debug)
                    System.out.println(servers.get(i));
                StringTokenizer tokenizer = new StringTokenizer(servers.get(i));
                String token = null;
                for(int j = 0; j <= 5; j++)
                    token = tokenizer.nextToken().trim();
                if(token.equals("Nobody"))
                    token = "0";
                else if(token.equals("*"))
                    token = "0";
                else if(token.equals("OPEN:"))
                    token = tokenizer.nextToken().trim();
                //One of the servers has a wait queue, just return
                else if(token.equals("Wait"))
                {
                    token = tokenizer.nextToken();
                    token = tokenizer.nextToken().trim();
                    wait = true;
                }
                try
                {
                    int numPlayers = new Integer(token);
                    if(numPlayers > maxPlayers)
                        maxPlayers = numPlayers;
                    if(wait)
                        maxPlayers += new Integer(token);
    
                }
                catch(NumberFormatException nfe)
                {
                    //Unexpected stream, send it to the error log for examination later
                    System.out.println("NF Exception on: " + token);
                    System.out.println(servers.get(i));
                    errorLog.add("NF Exception on: " + token);
                    errorLog.add(servers.get(i));
                    File file = new File(path);
                    file.mkdirs();
                    try
                    {
                        PrintWriter pwriter = new PrintWriter((new FileWriter(new File(filePath))));
                        for(int j = 0; j < errorLog.size(); j++)
                            pwriter.println(errorLog.get(j));
                        pwriter.close();
                    }
                    catch(Exception exception)
                    {
                        exception.printStackTrace();
                    }
                }
            }
        }
        catch (Exception e)
        {
            e.printStackTrace();
        }
        return maxPlayers;
    }
    
    
    /**
     * Create the AudioClip objects that will be needed by the
     * program.
     */
    private void loadSounds()
    {       
        URL name = getClass().getProtectionDomain().getCodeSource().getLocation();

        try
        {
            String fileString = name.toString();
            fileString = fileString.replace("file:", "");
            fileString = fileString.replace("%20", " ");
            file = new JarFile(fileString);
            Enumeration<JarEntry> entries = file.entries();
            while(entries.hasMoreElements())
            {
                JarEntry object = (JarEntry)entries.nextElement();
                String currentElement = object.toString();
                if(currentElement.endsWith("nt_start_tmode.wav"))
                {
                    URL url = TWatcher.class.getResource("nt_start_tmode.wav");
                    acTModeStart = Applet.newAudioClip(url);
                }
                else if(currentElement.endsWith("nt_red_alert.wav"))
                {
                    URL url = TWatcher.class.getResource("nt_red_alert.wav");
                    acAlert7 = Applet.newAudioClip(url);
                }
                else if(currentElement.endsWith("nt_message5.wav"))
                {
                    URL url = TWatcher.class.getResource("nt_message5.wav");
                    acAlert6 = Applet.newAudioClip(url);
                }
                else if(currentElement.endsWith("nt_stop_tmode.wav"))
                {
                    URL url = TWatcher.class.getResource("nt_stop_tmode.wav");
                    acTModeEnd = Applet.newAudioClip(url);
                }
            }
        }
        catch(Exception e)
        {
            e.printStackTrace();
        }
    }
    
    
    /**
     * Play the sounds as needed.  The boolean variables
     * keep the program from blaring out the clip with
     * every iteration.
     */
    private void playSounds(int maxPlayers)
    {
        Integer playCount = new Integer(maxPlayers);
        if(playCount > 8)
            playCount = 8;
        if(playCount < 8 && tMode == true)
        {
            acTModeEnd.play();
            tMode = false;
            if(playCount == 7)
                played7 = true;
            else
                played7 = false;
            if(playCount == 6)
                played6 = true;
            else
                played6 = false;
        }
        else
        {
            switch(playCount)
            {
                case(8):
                {
                    if(!tMode)
                    {
                        acTModeStart.play();
                        tMode = true;
                        played7 = false;
                        played6 = false;
                    }
                    break;
                }
                case(7):
                {
                    if(!played7)
                    {
                        acAlert7.play();
                        tMode = false;
                        played7 = true;
                        played6 = false;
                    }
                    break;
                }
                case(6):
                {
                    if(!played6)
                    {
                        acAlert6.play();
                        tMode = false;
                        played7 = false;
                        played6 = true;
                    }
                    break;
                }
                case(5):
                case(4):
                case(3):
                case(2):
                case(1):
                case(0):
                {
                    tMode = false;
                    played7 = false;
                    played6 = false;
                    break;
                }
            }
        }
    }
    
    
    /**
     * main method, used for testing only.
     * @param args
     */
    public static void main(String[] args)
    {
        if(args.length == 0)
            new TWatcher(POLL_DELAY);
    }
}
