package seng302;


import seng302.Networking.BinaryMessageEncoder;
import seng302.Networking.MessageEncoders.RaceVisionByteEncoder;
import seng302.Networking.MessageEncoders.XMLMessageEncoder;
import seng302.Networking.Messages.BoatLocation;
import seng302.Networking.Messages.Enums.MessageType;
import seng302.Networking.Messages.RaceStatus;
import seng302.Networking.Messages.XMLMessage;

import java.io.*;
import java.net.*;

import java.util.concurrent.ArrayBlockingQueue;

/**
 * TCP server to send race information to connected clients.
 */
public class MockOutput implements Runnable
{
    ///Timestamp of the last sent heartbeat message.
    private long lastHeartbeatTime;

    ///Period for the heartbeat - that is, how often we send it.
    private double heartbeatPeriod = 5.0;

    ///Port to expose server on.
    private int serverPort = 4942;
    ///Socket used to listen for clients on.
    private ServerSocket serverSocket;
    ///Socket used to communicate with a client.
    private Socket mockSocket;
    ///Output stream which wraps around mockSocket outstream.
    private DataOutputStream outToVisualiser;

    ///A queue that contains items that are waiting to be sent.
    private ArrayBlockingQueue<byte[]> messagesToSendQueue = new ArrayBlockingQueue<>(99999999);

    ///Sequence numbers used in messages.
    private short messageNumber = 1;
    private short xmlSequenceNumber = 1;
    private int heartbeatSequenceNum = 1;
    private int boatLocationSequenceNumber = 1;
    private int raceStatusSequenceNumber = 1;

    ///Strings containing XML data as strings.
    private String raceXml;
    private String regattaXml;
    private String boatsXml;


    /**
     * Ctor.
     * @throws IOException if server socket cannot be opened.
     */
    public MockOutput() throws  IOException {
        lastHeartbeatTime = System.currentTimeMillis();
        serverSocket = new ServerSocket(serverPort);

    }
    /**
     * Calculates the time since last heartbeat message, in seconds.
     * @return Time since last heartbeat message, in seconds.
     */
    private double timeSinceHeartbeat() {
        long now = System.currentTimeMillis();
        return (now - lastHeartbeatTime) / 1000.0;
    }

    //returns the heartbeat message

    /**
     * Increment the heartbeat value
     * @return message for heartbeat data
     */
    private byte[] heartbeat(){
        byte[] heartbeatMessage = RaceVisionByteEncoder.heartBeat(heartbeatSequenceNum);
        heartbeatSequenceNum++;
        BinaryMessageEncoder binaryMessageEncoder = new BinaryMessageEncoder(MessageType.HEARTBEAT, System.currentTimeMillis(), messageNumber, (short)heartbeatMessage.length, heartbeatMessage);
        messageNumber++;
        return binaryMessageEncoder.getFullMessage();
    }

    /**
     * Used to give the mockOutput an xml string to be made into a message and sent
     * @param xmlString the xml string to send
     * @param messageType the kind of xml string, values given in AC35 spec (5 regatta, 6 race, 7 boat)
     */
    public synchronized void parseXMLString(String xmlString, int messageType){
        XMLMessageEncoder encoder = new XMLMessageEncoder(messageNumber, System.currentTimeMillis(), messageType, xmlSequenceNumber,(short) xmlString.length(), xmlString);
        //iterates the sequence numbers
        xmlSequenceNumber++;
        byte[] encodedXML = encoder.encode();

        BinaryMessageEncoder binaryMessageEncoder = new BinaryMessageEncoder(MessageType.XMLMESSAGE, System.currentTimeMillis(), messageNumber, (short)encodedXML.length, encodedXML);
        //iterates the message number
        messageNumber++;

        addMessageToBufferToSend(binaryMessageEncoder.getFullMessage());
    }

    /**
     * Used to give the mocOutput information about boat location to be made into a message and sent
     * @param sourceID id of the boat
     * @param lat latitude of boat
     * @param lon longitude of boat
     * @param heading heading of boat
     * @param speed speed of boat
     */
    public synchronized void parseBoatLocation(int sourceID, double lat, double lon, double heading, double speed){

        BoatLocation boatLocation = new BoatLocation(sourceID, lat, lon, boatLocationSequenceNumber, heading, speed);
        //iterates the sequence number
        boatLocationSequenceNumber++;

        //encodeds the messages
        byte[] encodedBoatLoc = RaceVisionByteEncoder.boatLocation(boatLocation);

        //encodeds the full message with header
        BinaryMessageEncoder binaryMessageEncoder = new BinaryMessageEncoder(MessageType.BOATLOCATION, System.currentTimeMillis(), messageNumber, (short)encodedBoatLoc.length,
                encodedBoatLoc);

        //iterates the message number
        messageNumber++;

        addMessageToBufferToSend(binaryMessageEncoder.getFullMessage());
    }

    /**
     * Parse the race status data and add it to the buffer to be sent
     * @param raceStatus race status to parses
     */
    public synchronized void parseRaceStatus(RaceStatus raceStatus){

        //iterates the sequence number
        raceStatusSequenceNumber++;

        //encodeds the messages
        byte[] encodedRaceStatus = RaceVisionByteEncoder.raceStatus(raceStatus);

        //encodeds the full message with header
        BinaryMessageEncoder binaryMessageEncoder = new BinaryMessageEncoder(MessageType.RACESTATUS, System.currentTimeMillis(), messageNumber, (short)encodedRaceStatus.length,
                encodedRaceStatus);

        //iterates the message number
        messageNumber++;

        addMessageToBufferToSend(binaryMessageEncoder.getFullMessage());

    }

    /**
     * Add a message to the buffer to be sent
     * @param messagesToSendBuffer message to add to the buffer
     */
    private synchronized void addMessageToBufferToSend(byte[] messagesToSendBuffer) {
        this.messagesToSendQueue.add(messagesToSendBuffer);
    }

    public void run() {

        try {
            while (true){
                System.out.println("Waiting for a connection...");//TEMP DEBUG REMOVE
                mockSocket = serverSocket.accept();

                outToVisualiser = new DataOutputStream(mockSocket.getOutputStream());


                while(boatsXml == null || regattaXml == null || raceXml == null) {

                }

                parseXMLString(raceXml, XMLMessage.XMLTypeRace);
                parseXMLString(regattaXml, XMLMessage.XMLTypeRegatta);
                parseXMLString(boatsXml, XMLMessage.XMLTypeBoat);


                while(true) {
                    try {
                        //Sends a heartbeat every so often.
                        if (timeSinceHeartbeat() >= heartbeatPeriod) {
                            outToVisualiser.write(heartbeat());
                            lastHeartbeatTime = System.currentTimeMillis();
                        }

                        //Checks the buffer to see if there is anything to send.
                        while (messagesToSendQueue.size() > 0) {
                            //Grabs message from head of queue.
                            byte[] binaryMessage = messagesToSendQueue.remove();

                            //sends the message to the visualiser
                            outToVisualiser.write(binaryMessage);

                        }
                    }catch(SocketException e){
                        break;
                    }

                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }



    public void setRaceXml(String raceXml) {
        this.raceXml = raceXml;
    }


    public void setRegattaXml(String regattaXml) {
        this.regattaXml = regattaXml;
    }


    public void setBoatsXml(String boatsXml) {
        this.boatsXml = boatsXml;
    }

    public static void main(String argv[]) throws Exception
    {
        MockOutput client = new MockOutput();
        client.run();
    }

}
