package org.jacorb.orb.miop;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.MulticastSocket;
import java.net.SocketException;
import org.jacorb.config.Configurable;
import org.jacorb.config.Configuration;
import org.jacorb.config.ConfigurationException;
import org.jacorb.orb.CDROutputStream;
import org.omg.ETF.Profile;
import org.omg.MIOP.PacketHeader_1_0;
import org.omg.MIOP.PacketHeader_1_0Helper;


/**
 * A client side one way miop connection. Send data via multicast sockets.
 *
 * @author Alysson Neves Bessani
 * @author Nick Cross
 * @version 1.0
 * @see MIOPConnection MIOPFactories
 */
public class ClientMIOPConnection extends MIOPConnection implements Configurable
{
   private static short nextMessage = 0;

   private MulticastSocket socket;
   private DatagramPacket packet;

   /** connection's configuration parameters */
   private short packetDataMaxSize;

   public ClientMIOPConnection ()
   {
      super ();
   }


   /**
    * Connect with the specified profile.
    *
    * @param profile a group MIOP profile.
    * @param timeout unused.
    */
   @Override
   public void connect (Profile profile, long timeout)
   {
      if (is_connected ())
      {
         close ();
      }

      this.profile = (MIOPProfile)profile;

      try
      {
         socket = new MulticastSocket ();
      }
      catch (IOException ioe)
      {
         throw new RuntimeException ("Error while creating and setting multicast socket " + profile);
      }
   }


   /**
    * Test if this connection is connected.
    *
    * @return true if connected and false otherwise.
    */
   @Override
   public boolean is_connected ()
   {
      return (socket != null) && socket.isConnected ();
   }


   /**
    * Close this connection (if it is opened).
    */
   @Override
   public void close ()
   {
      if (socket != null)
      {
         socket.close ();
      }
   }


   @Override
   public void flush ()
   {
      super.flush();

      byte data[] = ((ByteArrayOutputStream)out_stream).toByteArray ();
      ((ByteArrayOutputStream)out_stream).reset();

      int length = data.length;
      int offset = 0;

      // handle the size of the last packet that is rest of the division between
      // the message's size and the maximum packet's size
      short lastPacketSize = (short)(length % packetDataMaxSize);

      // number of packages filled: the message size divided by the maximum size of a package
      int numberOfFullPackets = length / packetDataMaxSize;

      // number of packages: number of packages filled + (1 or 0)
      int numberOfPackets = numberOfFullPackets + ((lastPacketSize > 0) ? 1 : 0);

      // creates an id
      byte[] messageId = generateNewId ();

      // Here is the case (very rare) when size of data to be sent fit to
      // a collection of full packets without debris. But in any case we need
      // a final packet.
      if (lastPacketSize == 0)
      {
         numberOfFullPackets--;
         lastPacketSize = packetDataMaxSize;
      }

      PacketHeader_1_0 header;

      // Send all the packets except the last
      //
      int i = 0; // is out because it will be used later
      for (; i < numberOfFullPackets; i++)
      {
         header = new PacketHeader_1_0 (MulticastUtil.MAGIC, MulticastUtil.HDR_VERSION,
                  MulticastUtil.BIG_ENDIAN, packetDataMaxSize, i, numberOfPackets,
                  messageId);

         sendMIOPPacket (header, data, offset + i * packetDataMaxSize,
                         packetDataMaxSize);
      }

      // now the last ...
      header = new PacketHeader_1_0 (MulticastUtil.MAGIC, MulticastUtil.HDR_VERSION,
               (byte)(MulticastUtil.BIG_ENDIAN | MulticastUtil.STOP_FLAG), lastPacketSize, i,
               numberOfPackets, messageId);

      sendMIOPPacket (header, data, offset + i * packetDataMaxSize, lastPacketSize);
   }


   /**
    * Send a MIOP packet given the header and the bytes to send.
    *
    * @param header the MIOP packet header.
    * @param data the packet data array.
    * @param offset the data array offset.
    * @param length the amount of data to send.
    */
   private void sendMIOPPacket (PacketHeader_1_0 header, byte[] data, int offset, int length)
   {
      CDROutputStream os = new CDROutputStream (orb);

      PacketHeader_1_0Helper.write (os, header);
      os.check (length, MulticastUtil.BOUNDARY);
      os.write_octet_array (data, offset, length);

      try
      {
         byte[] buffer = os.getBufferCopy ();

         if (packet == null)
         {
            packet = new DatagramPacket
            (
               buffer,
               0,
               buffer.length,
               ((MIOPProfile)profile).getGroupInetAddress (),
               ((MIOPProfile)profile).getUIPMCProfile ().the_port
            );
         }
         else
         {
            packet.setData (buffer, 0, buffer.length);
         }
         socket.send (packet);
      }
      catch (IOException se)
      {
         if (logger.isDebugEnabled())
         {
             logger.debug("Transport to " + connection_info +
                          ": stream closed " + se.getMessage() );
         }
         throw to_COMM_FAILURE (se);
      }
   }


   /**
    * Generates an Id for a message.
    *
    * @return the 12 byte Id
    */
   private byte[] generateNewId ()
   {
      byte[] id = new byte[MulticastUtil.ID_SIZE];
      byte serverId[] = orb.getServerId ();

      // copy the key to the current host
      System.arraycopy (serverId, 0, id, 0, serverId.length);

      // number of message
      synchronized (ClientMIOPConnection.class)
      {
         id[10] = (byte)((nextMessage >>  8) & 0xFF);
         id[11] = (byte)(nextMessage & 0xFF);

         nextMessage++;
      }

      return id;
   }

   /**
    * @see org.jacorb.orb.etf.ConnectionBase#getTimeout()
    */
   @Override
   protected int getTimeout ()
   {
       try
       {
           return socket.getSoTimeout();
       }
       catch (SocketException se)
       {
           throw to_COMM_FAILURE (se);
       }
   }

   /**
    * @see org.jacorb.orb.etf.ConnectionBase#setTimeout(int)
    */
   @Override
   protected void setTimeout (int timeout)
   {
       if (socket != null)
       {
           try
           {
               if (logger.isInfoEnabled())
               {
                   logger.info ("Socket timeout set to " + timeout + " ms");
               }
               socket.setSoTimeout(timeout);
           }
           catch( SocketException se )
           {
               if (logger.isInfoEnabled())
               {
                   logger.info("SocketException", se);
               }
           }
       }
   }

   @Override
   public void configure (Configuration config) throws ConfigurationException
   {
       super.configure (config);

       int packetMax = config.getAttributeAsInteger ("jacorb.miop.packet_max_size",
                                                     MulticastUtil.PACKET_MAX_SIZE);
       packetDataMaxSize = (short)(packetMax - MulticastUtil.PACKET_DATA_MAX_SIZE);
   }
}
