package OpenDis7Examples;

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

import edu.nps.moves.dis7.pdus.*;
import edu.nps.moves.dis7.enumerations.*;

/**
 * This is an application example that sends every type of PDU defined
 * by the IEEE Distributed Interactive Simulation (DIS) Protocol.
 * This example program is useful for testing standards compliance by applications, or 
 * producing a full set of PDUs. It also writes the generated PDUs to an XML file.
 * Adapted from OpenDIS library example package edu.nps.moves.examples
 *
 * @author brutzman
 * @author DMcG
 * @version $Id:$
 */
public class AllPduSender
{
    /** Default multicast group address we send on.
      * @see <a href="https://en.wikipedia.org/wiki/Multicast_address" target="_blank">https://en.wikipedia.org/wiki/Multicast_address</a> */
    public static final String DEFAULT_MULTICAST_ADDRESS = "239.1.2.3"; // PduRecorder "225.4.5.6"; //
	
    /** Default multicast port used, matches Wireshark DIS capture default
      * @see <a href="https://en.wikipedia.org/wiki/Port_(computer_networking)" target="_blank">https://en.wikipedia.org/wiki/Port_(computer_networking)</a> */
    public static final int    DEFAULT_MULTICAST_PORT    = 3000;
	
    /** Duration in milliseconds, set to 0 to avoid pausing between PDU sends */
    private long               THREAD_SLEEP_INTERVAL = 0;
	
    /** Number of complete loops to perform.
     * Putting any upper limit on # packets sent avoids possibility of non-terminating infinite loops that continue sending packets. */
    private int                SEND_LOOPS_TO_PERFORM = 1;
    
    private static InetAddress multicastInetAddress;
    private static int         port;

    /** Object constructor
     * @param newMulticastAddress address of interest
     * @param newMulticastPort port of interest */
    public AllPduSender(String newMulticastAddress, int newMulticastPort)
    {
        this.port = DEFAULT_MULTICAST_PORT;
        try
		{
            multicastInetAddress = InetAddress.getByName(newMulticastAddress);
            if (!multicastInetAddress.isMulticastAddress())
			{
                System.out.println("Not a multicast address: " + newMulticastAddress);
            }
            this.port = newMulticastPort;
        } 
		catch (UnknownHostException e) {
            System.out.println("Unable to open socket: " + e);
        }
    }

    /** Begin operations
     * @return number of PDUs received, -1 if exception occurs */
    @SuppressWarnings("SleepWhileInLoop")
    public int run()
	{
		System.out.println("OpenDis7Examples.AllPduSender started...");
        if (SEND_LOOPS_TO_PERFORM != 1)
        {
            float  waitIntervalSeconds = ((float)THREAD_SLEEP_INTERVAL / 1000);
            float  loopIntervalSeconds = ((float)THREAD_SLEEP_INTERVAL / 1000) * 72; // 72 PDUs
            float totalDurationSeconds = loopIntervalSeconds * SEND_LOOPS_TO_PERFORM ;
            System.out.println("... THREAD_SLEEP_INTERVAL = " + THREAD_SLEEP_INTERVAL + " milliseconds = "  + waitIntervalSeconds + " seconds");
            System.out.print  ("... running for ");
            if (SEND_LOOPS_TO_PERFORM > 1)
                System.out.print  (SEND_LOOPS_TO_PERFORM + " loops, ");
            if (THREAD_SLEEP_INTERVAL > 0)
                System.out.println("expected loop interval = " + loopIntervalSeconds + " seconds, total duration = " + 
                        totalDurationSeconds + " seconds = " + (totalDurationSeconds/60.0) + " minutes");
        }
           
        System.out.println("Generate list of all PDU types and note issues, if any...");
        List<Pdu> generatedPdusList = new ArrayList<>();
            
        for (int i = 0; i < SEND_LOOPS_TO_PERFORM; i++) {
            
            try {
                
                // Loop through all the enumerated PDU types, create a PDU for each type,
                // add that PDU to generatedPdusList, then send each one
                for (DisPduType pduTypeValue : DisPduType.values()) 
                {
    //                System.out.println("PDU " + pdu.getValue() + " " + pdu.name() + " " + pdu.getDescription()); // diagnostic

                    Pdu aPdu = null; // edu.​nps.​moves7.​dis.PDU superclass for all PDUs, in preparation for custom assignment

                    try {
                        switch (pduTypeValue) // using enumeration values from edu.​nps.​moves.​dis7.​enumerations.​DisPduType
                        {
                            // each case value is DisPduType
                            case OTHER: // 0
                                System.out.println ("*** Note: DisPduType." + pduTypeValue.name() + "=" + pduTypeValue.getValue() + " not supported"); // TODO why was this received?
                                break; // nothing to send

                            case ENTITY_STATE: // 1 
                                aPdu = new EntityStatePdu();

                                EntityStatePdu espdu = (EntityStatePdu) aPdu;
                                EntityMarking entityMarking = new EntityMarking ();
                                entityMarking.setCharacters("AllPduSender".getBytes());   //entityMarking.setCharacters(Byte.valueOf("0")); // 11 characters max?

                                espdu.setMarking(entityMarking);
                                Vector3Double espduLocation = new Vector3Double();
                                espduLocation.setX(1.0);
                                espduLocation.setY(2.0);
                                espduLocation.setZ(3.0);
                                espdu.setEntityLocation(espduLocation);
                                // it is important to identify questions as you think of them
                                // TODO how to set azimuth, i.e. course direction over ground?
                                break;

                            case FIRE: // 2
                                aPdu = new FirePdu();
                                break;

                            case DETONATION: // 3
                                aPdu = new DetonationPdu();
                                break;

                            case COLLISION: // 4
                                aPdu = new CollisionPdu();
                                break;

                            case SERVICE_REQUEST: // 5
                                aPdu = new ServiceRequestPdu();
                                break;

                            case RESUPPLY_OFFER: // 6
                                aPdu = new ResupplyOfferPdu();
                                break;

                            case RESUPPLY_RECEIVED: // 7
                                aPdu = new ResupplyReceivedPdu();
                                break;

                            case RESUPPLY_CANCEL: //8
                                aPdu = new ResupplyCancelPdu();
                                break;

                            case REPAIR_COMPLETE: // 9
                                aPdu = new RepairCompletePdu();
                                break;

                            case REPAIR_RESPONSE: // 10
                                aPdu = new RepairResponsePdu();
                                break;

                            case CREATE_ENTITY: // 11
                                aPdu = new CreateEntityPdu();
                                break;

                            case REMOVE_ENTITY: // 12
                                aPdu = new RemoveEntityPdu();
                                break;

                            case START_RESUME: // 13
                                aPdu = new StartResumePdu();
                                break;

                            case STOP_FREEZE: // 14
                                aPdu = new StopFreezePdu();
                                break;

                            case ACKNOWLEDGE: // 15
                                aPdu = new AcknowledgePdu();
                                break;

                            case ACTION_REQUEST: // 16
                                aPdu = new ActionRequestPdu();
                                break;

                            case ACTION_RESPONSE: // 17
                                aPdu = new ActionResponsePdu();
                                break;

                            case DATA_QUERY: // 18
                                aPdu = new DataQueryPdu();
                                break;

                            case SET_DATA: // 19
                                aPdu = new SetDataPdu();
                                break;

                            case DATA: // 20
                                aPdu = new DataPdu();
                                break;

                            case EVENT_REPORT: // 21
                                aPdu = new EventReportPdu();
                                break;

                            case ELECTROMAGNETIC_EMISSION: // 23
                                aPdu = new ElectromagneticEmissionPdu();
                                break;

                            case DESIGNATOR: // 24
                                aPdu = new DesignatorPdu();
                                break;

                            case TRANSMITTER: // 25
                                aPdu = new TransmitterPdu();
                                break;

                            case SIGNAL: // 26
                                aPdu = new SignalPdu();
                                break;

                            case RECEIVER: // 27
                                aPdu = new ReceiverPdu();
                                break;

                            case IDENTIFICATION_FRIEND_OR_FOE: // 28
                                aPdu = new IdentificationFriendOrFoePdu();
                                break;

                            case UNDERWATER_ACOUSTIC: // 29
                                aPdu = new UnderwaterAcousticPdu();
                                break;

                            case SUPPLEMENTAL_EMISSION_ENTITY_STATE: // 30
                                aPdu = new SupplementalEmissionEntityStatePdu();
                                break;

                            case INTERCOM_SIGNAL: // 31
                                aPdu = new IntercomSignalPdu();
                                break;

                            case INTERCOM_CONTROL: // 32
                                aPdu = new IntercomControlPdu();
                                break;

                            case AGGREGATE_STATE: // 33
                                aPdu = new AggregateStatePdu();
                                break;

                            case ISGROUPOF: // 34
                                aPdu = new IsGroupOfPdu();
                                break;

                            case TRANSFER_OWNERSHIP: // 35
                                aPdu = new TransferOwnershipPdu();
                                break;

                            case ISPARTOF: // 36
                                aPdu = new IsPartOfPdu();
                                break;

                            case MINEFIELD_STATE: // 37
                                aPdu = new MinefieldStatePdu();
                                break;

                            case MINEFIELD_QUERY: // 38
                                aPdu = new MinefieldQueryPdu();
                                break;

                            case MINEFIELD_DATA: // 39
                                aPdu = new MinefieldDataPdu();
                                break;

                            case MINEFIELD_RESPONSE_NACK: // 40
                                aPdu = new MinefieldResponseNACKPdu();
                                break;

                            case ENVIRONMENTAL_PROCESS: // 41
                                aPdu = new EnvironmentalProcessPdu();
                                break;

                            case GRIDDED_DATA: // 42
                                aPdu = new GriddedDataPdu();
                                break;

                            case POINT_OBJECT_STATE: // 43
                                aPdu = new PointObjectStatePdu();
                                break;

                            case LINEAR_OBJECT_STATE: // 44
                                aPdu = new LinearObjectStatePdu();
                                break;

                            case AREAL_OBJECT_STATE: // 45
                                aPdu = new ArealObjectStatePdu();
                                break;

                            case TIME_SPACE_POSITION_INFORMATION: // 46
                                aPdu = new TimeSpacePositionInformationPdu();
                                break;

                            case APPEARANCE: // 47
                                aPdu = new AppearancePdu();
                                break;

                            case ARTICULATED_PARTS: // 48
                                aPdu = new ArticulatedPartsPdu();
                                break;

                            case LIVE_ENTITY_FIRE: // 49
                                aPdu = new LiveEntityFirePdu();
                                break;

                            case LIVE_ENTITY_DETONATION: // 50
                                aPdu = new LiveEntityDetonationPdu();
                                break;

                            case CREATE_ENTITY_RELIABLE: // 51
                                aPdu = new CreateEntityReliablePdu();
                                break;

                            case REMOVE_ENTITY_RELIABLE: // 52
                                aPdu = new RemoveEntityReliablePdu();
                                break;

                            case START_RESUME_RELIABLE: // 53
                                aPdu = new StartResumeReliablePdu();
                                break;

                            case STOP_FREEZE_RELIABLE: // 54
                                aPdu = new StopFreezeReliablePdu();
                                break;

                            case ACKNOWLEDGE_RELIABLE: // 55
                                aPdu = new AcknowledgeReliablePdu();
                                break;

                            case ACTION_REQUEST_RELIABLE: // 56
                                aPdu = new ActionRequestReliablePdu();
                                break;

                            case ACTION_RESPONSE_RELIABLE: // 57
                                aPdu = new ActionResponseReliablePdu();
                                break;

                            case DATA_QUERY_RELIABLE: // 58
                                aPdu = new DataQueryReliablePdu();
                                break;

                            case SET_DATA_RELIABLE: // 59
                                aPdu = new SetDataReliablePdu();
                                break;

                            case DATA_RELIABLE: // 60
                                aPdu = new DataReliablePdu();
                                break;

                            case EVENT_REPORT_RELIABLE: // 61
                                aPdu = new EventReportReliablePdu();
                                break;

                            case COMMENT_RELIABLE: // 62
                                aPdu = new CommentReliablePdu();
                                break;

                            case RECORD_RELIABLE: // 63
                                aPdu = new RecordReliablePdu();
                                break;

                            case SET_RECORD_RELIABLE: // 64
                                aPdu = new SetRecordReliablePdu();
                                break;

                            case RECORD_QUERY_RELIABLE: // 65
                                aPdu = new RecordQueryReliablePdu();
                                break;

                            case COLLISION_ELASTIC: // 66
                                aPdu = new CollisionElasticPdu();
                                break;

                            case ENTITY_STATE_UPDATE: // 67
                                aPdu = new EntityStateUpdatePdu();
                                break;

                            case DIRECTED_ENERGY_FIRE: // 68
                                aPdu = new DirectedEnergyFirePdu();
                                break;

                            case ENTITY_DAMAGE_STATUS: // 69
                                aPdu = new EntityDamageStatusPdu();
                                break;

                            case INFORMATION_OPERATIONS_ACTION: // 70
                                aPdu = new InformationOperationsActionPdu();
                                break;

                            case INFORMATION_OPERATIONS_REPORT: // 71
                                aPdu = new InformationOperationsReportPdu();
                                break;

                            case ATTRIBUTE: // 72
                                aPdu = new AttributePdu();
                                break;

                            case COMMENT:
                                // aPdu = new CommentPdu(); // default for this switch logic

                                // see Garrett Loffelman and Pete Severson's code for OpenDis version 4 example
                                // https://gitlab.nps.edu/Savage/NetworkedGraphicsMV3500/tree/master/assignments/src/MV3500Cohort2018JulySeptember/projects/LoeffelmanSeverson

                                CommentPdu    newCommentPdu = new CommentPdu();
                                ArrayList<VariableDatum> payloadList = new ArrayList<>();
                                ArrayList<String> commentsList = new ArrayList<>();
                                commentsList.add("Hello CommentPDU");
                                commentsList.add("Here is a second line of text in this comment.");
                                if (!commentsList.isEmpty())
                                    System.out.println("Preparing CommentPDU:");

                                for (String comment : commentsList)
                                {
                                    VariableDatum newVariableDatum = new VariableDatum();
                                    newVariableDatum.setVariableDatumValue (comment.getBytes());               // conversion
                                    // TODO confirm whether method overloading is appropriate
//                                  newVariableDatum.setVariableDatumLength(comment.getBytes().length); 
                                    newVariableDatum.setVariableDatumLengthInBytes(comment.getBytes().length); // also available in bits, see spec and javadoc
                                    // alternatively, you do not need to set this and the marshaller will figure it out from the byte array
                                    // (see javadoc for VariableDatum.setVariableDatumLength())
                                    payloadList.add(newVariableDatum);
                                    System.out.println("   \"" + comment + "\"");
                                }
                                newCommentPdu.setVariableDatums(payloadList);

                                aPdu = newCommentPdu; // hand off for sending
                                break;

                            default:
                                System.out.println("*** Warning: PDU " + pduTypeValue.getValue() + " " + pduTypeValue + " not supported, created or sent ");

                                // code generation block for this class follows:
    //                            System.out.println("                    case " + pdu + ": // " + pdu.getValue());
    //                            System.out.println("                        aPdu = new " + pdu.getDescription().replace(" ","").replace("-","").replace("/","") + 
    //                                                                                      "Pdu();");
    //                            System.out.println("                        break;");
    //                            System.out.println();
                        }
                        if (aPdu != null)
                        {
                            // leave aPdu timestamp at default value for consistency of output unit tests
                            generatedPdusList.add(aPdu); 
                        }   
                    }
                    catch (Exception e)
                    {
                        System.out.print("Exception thrown for PDU " + pduTypeValue.getValue() + " " + pduTypeValue);
                        System.out.print(Arrays.toString(e.getStackTrace()));
                        // continue looping
                    }
                }
                if (generatedPdusList.size() != 72) // TODO create an enumeration DISType.TOTAL_PDU_TYPES
                    System.out.println("Error: " + generatedPdusList.size() + " PDUs generated, but 72 PDUs expected.");

                // Send the PDUs we created
                System.out.println("Send the " + generatedPdusList.size() + " PDUs we created...");

                // =======================================================================
                // prior appproach
//                InetAddress localMulticastAddress = InetAddress.getByName(DEFAULT_MULTICAST_ADDRESS);
//                MulticastSocket multicastSocket = new MulticastSocket(DEFAULT_MULTICAST_PORT);
//                if (!localMulticastAddress.isMulticastAddress())
//                {
//                    throw new RuntimeException("*** Error: sending to multicast address, but destination address " + localMulticastAddress.toString() + "is not multicast");
//                }
//                multicastSocket.joinGroup(localMulticastAddress); // deprecated
                // =======================================================================
                // updated approach using NetworkInterface
                NetworkInterface networkInterface = NetworkInterface.getByInetAddress(multicastInetAddress);
                if (networkInterface != null)
                    System.out.println("networkInterface=" + networkInterface.getDisplayName()); // typically null if loopback
                SocketAddress localMulticastSocketAddress = new InetSocketAddress(multicastInetAddress, DEFAULT_MULTICAST_PORT);
                MulticastSocket multicastSocket = new MulticastSocket(DEFAULT_MULTICAST_PORT);
                multicastSocket.joinGroup(localMulticastSocketAddress, networkInterface);
                // =======================================================================

                byte[] buffer;
                Pdu aPdu;
                DatagramPacket packet;

                for (int idx = 0; idx < generatedPdusList.size(); idx++)
                {
                    // careful here!  keep object instantiations inside of loop to avoid endless array and packet growth
                    ByteArrayOutputStream baos = new ByteArrayOutputStream();
                    DataOutputStream       dos = new DataOutputStream(baos);
                
                    aPdu = generatedPdusList.get(idx);
                    try 
                    {
                        aPdu.marshal(dos); // dos is DataOutputStream connected to ByteArrayOutputStrea

                        buffer = baos.toByteArray();
                        packet = new DatagramPacket(buffer, buffer.length, multicastInetAddress, DEFAULT_MULTICAST_PORT);
                        multicastSocket.send(packet);
                        
                        DisPduType disPduType= aPdu.getPduType();
//                      disPduType.setTRACE(true);
                        String currentIndexPadded        = String.format("%2s",   (idx + 1));
                        String currentPduTypeValuePadded = String.format("%2s",   disPduType.getValue());
                        String currentPduTypePadded      = String.format("%-49s", aPdu.getPduType().toString()); // - indicates right padding of whitespace
                        String packetLengthPadded        = String.format("%3s",   packet.getLength());
                        
                        System.out.println ("Sent packet #" + currentIndexPadded + ", " + 
//                          currentPduTypeValuePadded + " " + 
                            currentPduTypePadded + 
                            "(packet.getLength()=" + packetLengthPadded + ")" + // diagnostic, beware of ever-growing packet size!
                            " of type " + aPdu.getClass().getSimpleName());

                        Thread.sleep(THREAD_SLEEP_INTERVAL); // pause for debugging, if zero this process still yields
                    } 
                    catch (Exception ex) {
                        System.out.println("Marshaling error" + ex);
                    }
                }
            }
            catch (IOException e)
            {
                System.out.println(e);
                return -1; // error condition
            }
        } // end repetion loop
            
        // TODO write the PDUs out to an XML file.
        //PduContainer container = new PduContainer();
        //container.setPdus(generatedPdus);
        //container.marshallToXml("examplePdus.xml");
        return generatedPdusList.size();
    }

    /**
     * Program invocation, execution starts here
     * @param args command-line arguments
     */
    public static void main(String args[])
	{
        AllPduSender allPduSender;
        int totalSentPdus;
            
        if (args.length == 2)
        {
            System.out.println("Usage:   AllPduSender <multicast group address> <port>");
            System.out.println("Actual:  AllPduSender  " + multicastInetAddress.getHostAddress() + "   " + port);
            allPduSender = new AllPduSender(args[0], Integer.parseInt(args[1]));
        } 
        else
        {
            System.out.println("Usage:   AllPduSender <multicast group address> <port>");
            System.out.println("Default: AllPduSender  " + DEFAULT_MULTICAST_ADDRESS + "   " + DEFAULT_MULTICAST_PORT);
            allPduSender = new AllPduSender(DEFAULT_MULTICAST_ADDRESS, DEFAULT_MULTICAST_PORT);
        }
        totalSentPdus = allPduSender.run(); // do it to it
        System.out.println("OpenDis7Examples.AllPduSender complete, sent " + totalSentPdus + " PDUs total.");
    }
}