package org.jgroups.protocols;

import org.jgroups.Address;
import org.jgroups.Event;
import org.jgroups.Message;
import org.jgroups.PhysicalAddress;
import org.jgroups.annotations.Experimental;
import org.jgroups.annotations.Property;
import org.jgroups.stack.*;
import org.jgroups.util.Buffer;
import org.jgroups.util.ExposedByteArrayOutputStream;
import org.jgroups.util.ExposedDataOutputStream;
import org.jgroups.util.Util;

import java.io.DataInputStream;
import java.net.DatagramSocket;
import java.net.InetSocketAddress;
import java.net.UnknownHostException;
import java.net.SocketException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

/**
 * Replacement for UDP. Instead of sending packets via UDP, a TCP connection is opened to a Router
 * (using the RouterStub client-side stub), the IP address/port of which was given using channel
 * properties <code>router_host</code> and <code>router_port</code>. All outgoing traffic is sent
 * via this TCP socket to the Router which distributes it to all connected TUNNELs in this group.
 * Incoming traffic received from Router will simply be passed up the stack.
 * 
 * <p>
 * A TUNNEL layer can be used to penetrate a firewall, most firewalls allow creating TCP connections
 * to the outside world, however, they do not permit outside hosts to initiate a TCP connection to a
 * host inside the firewall. Therefore, the connection created by the inside host is reused by
 * Router to send traffic from an outside host to a host inside the firewall.
 * 
 * @author Bela Ban
 * @author Vladimir Blagojevic
 */
@Experimental
public class TUNNEL extends TP {

    /*
    * ----------------------------------------- Properties
    * --------------------------------------------------
    */

    @Property(description = "Interval in msec to attempt connecting back to router in case of torn connection. Default is 5000 msec")
    private long reconnect_interval = 5000;

    @Property(description="Should TCP no delay flag be turned on")
    boolean tcp_nodelay=false;

    /*
    * --------------------------------------------- Fields
    * ------------------------------------------------------
    */

   private final List<InetSocketAddress> gossip_router_hosts = new ArrayList<InetSocketAddress>();

   private TUNNELPolicy tunnel_policy = new DefaultTUNNELPolicy();

   private DatagramSocket sock;
   
   private volatile RouterStubManager stubManager;

   public TUNNEL() {
   }

    public boolean supportsMulticasting() {
        return false;
    }

    @Property(description="A comma-separated list of GossipRouter hosts, e.g. HostA[12001],HostB[12001]")
   public void setGossipRouterHosts(String hosts) throws UnknownHostException {
      gossip_router_hosts.clear();
      // if we get passed value of List<SocketAddress>#toString() we have to strip []
      if (hosts.startsWith("[") && hosts.endsWith("]")) {
         hosts = hosts.substring(1, hosts.length() - 1);
      }
      gossip_router_hosts.addAll(Util.parseCommaDelimitedHosts2(hosts, 1));
   }

   public String toString() {
      return "TUNNEL";
   }

   public long getReconnectInterval() {
      return reconnect_interval;
   }

   public void setReconnectInterval(long reconnect_interval) {
      this.reconnect_interval = reconnect_interval;
   }

   /*------------------------------ Protocol interface ------------------------------ */

   public synchronized void setTUNNELPolicy(TUNNELPolicy policy) {
      if (policy == null)
         throw new IllegalArgumentException("Tunnel policy has to be non null");
      tunnel_policy = policy;
   }

    public void init() throws Exception {
        super.init();
        if (timer == null)
            throw new Exception("timer cannot be retrieved from protocol stack");
      
        // TODO [JGRP-1194] - Revisit implementation of TUNNEL and shared transport
        if (isSingleton())
            throw new Exception("TUNNEL and shared transport mode are not supported!");

        if (log.isDebugEnabled())
            log.debug("GossipRouters are:" + gossip_router_hosts.toString());
        
        stubManager = RouterStubManager.emptyGossipClientStubManager(this);
        sock = getSocketFactory().createDatagramSocket("jgroups.tunnel.ucast_sock", bind_port, bind_addr);
        
        // loopback turned on is mandatory
        loopback = true;
    }
    
    public void destroy() {        
        stubManager.destroyStubs();
        super.destroy();
    }

   private void disconnectStub(String group, Address addr) {
      stubManager.disconnectStubs();
   }

   public Object handleDownEvent(Event evt) {
      Object retEvent = super.handleDownEvent(evt);
      switch (evt.getType()) {
         case Event.CONNECT:
         case Event.CONNECT_WITH_STATE_TRANSFER:
         case Event.CONNECT_USE_FLUSH:
         case Event.CONNECT_WITH_STATE_TRANSFER_USE_FLUSH:
             String group=(String)evt.getArg();
             Address local=local_addr;

             if(stubManager != null)
                stubManager.destroyStubs();
             stubManager = new TUNNELStubManager(this,group,local,getReconnectInterval());
             for (InetSocketAddress gr : gossip_router_hosts) {
                 RouterStub stub = stubManager.createAndRegisterStub(gr.getHostName(), gr.getPort(), bind_addr);
                 stub.setTcpNoDelay(tcp_nodelay);           
              }  
             PhysicalAddress physical_addr=(PhysicalAddress)down(new Event(Event.GET_PHYSICAL_ADDRESS, local));
             List<PhysicalAddress> physical_addrs=Arrays.asList(physical_addr);
             String logical_name=org.jgroups.util.UUID.get(local);
             List<RouterStub> stubs = stubManager.getStubs();
             tunnel_policy.connect(stubs, group, local, logical_name, physical_addrs);
            break;

         case Event.DISCONNECT:
             local = local_addr;
             group = channel_name;
             disconnectStub(group,local);
            break;
      }
      return retEvent;
   }
    private class TUNNELStubManager extends RouterStubManager {

        TUNNELStubManager(Protocol owner, String channelName, Address logicalAddress, long interval) {            
            super(owner, channelName, logicalAddress, interval);
        }

        public void connectionStatusChange(RouterStub stub, RouterStub.ConnectionStatus newState) {
            super.connectionStatusChange(stub, newState);
            if (newState == RouterStub.ConnectionStatus.CONNECTED) {               
                StubReceiver stubReceiver = new StubReceiver(stub);
                stub.setReceiver(stubReceiver);
                Thread t = global_thread_factory.newThread(stubReceiver, "TUNNEL receiver for " + stub.toString());
                stubReceiver.setThread(t);
                t.setDaemon(true);
                t.start();
            } 
        }
    }

    public class StubReceiver implements Runnable {

        private Thread runner;        
        private final RouterStub stub;
       
        public StubReceiver(RouterStub stub) {
            this.stub = stub;          
        }

        public synchronized void setThread(Thread t) {
            runner = t;
        }

        public synchronized Thread getThread() {
            return runner;
        }

        public void run() {
            final DataInputStream input = stub.getInputStream();
            mainloop:
            while (!Thread.currentThread().isInterrupted()) {
                try {                                        
                    GossipData msg = new GossipData();
                    msg.readFrom(input);
                    switch (msg.getType()) {
                        case GossipRouter.DISCONNECT_OK:                            
                            break mainloop;
                        case GossipRouter.MESSAGE:
                            byte[] data = msg.getBuffer();
                            receive(null/* src will be read from data */, data, 0, data.length);
                            break;
                        case GossipRouter.SUSPECT:
                            final Address suspect = Util.readAddress(input);
                            log.debug("Firing suspect event " + suspect + " at " + local_addr);
                            if(suspect != null) {
                                // https://jira.jboss.org/jira/browse/JGRP-902                               
                                Thread thread = getThreadFactory().newThread(new Runnable() {
                                    public void run() {
                                        fireSuspectEvent(suspect);
                                    }
                                }, "StubReceiver-suspect");
                                thread.start();
                            }
                            break;
                    }
                }catch (SocketException ioe) {
                    break;
                }catch (Exception ioe) {     
                    if(!stub.isConnected())
                        break;
                }
            }
        }

        private void fireSuspectEvent(Address suspect) {
            up(new Event(Event.SUSPECT, suspect));            
        }
    }


    protected void send(Message msg, Address dest, boolean multicast) throws Exception {

        // we don't currently support message bundling in TUNNEL
        TpHeader hdr=(TpHeader)msg.getHeader(this.id);
        if(hdr == null)
            throw new Exception("message " + msg + " doesn't have a transport header, cannot route it");
        String group=hdr.channel_name;

        ExposedByteArrayOutputStream out_stream=new ExposedByteArrayOutputStream((int)(msg.size() + 50));
        ExposedDataOutputStream dos=new ExposedDataOutputStream(out_stream);

        writeMessage(msg, dos, multicast);
        Buffer buf=new Buffer(out_stream.getRawBuffer(), 0, out_stream.size());

        if(stats) {
            num_msgs_sent++;
            num_bytes_sent+=buf.getLength();
        }
        List<RouterStub> stubs = stubManager.getStubs();
        if(multicast) {
            tunnel_policy.sendToAllMembers(stubs, group, buf.getBuf(), buf.getOffset(), buf.getLength());
        }
        else {
            tunnel_policy.sendToSingleMember(stubs, group, dest, buf.getBuf(), buf.getOffset(), buf.getLength());
        }
    }


    public void sendMulticast(byte[] data, int offset, int length) throws Exception {
        throw new UnsupportedOperationException("sendMulticast() should not get called on TUNNEL");
    }

   public void sendUnicast(PhysicalAddress dest, byte[] data, int offset, int length) throws Exception {
       throw new UnsupportedOperationException("sendUnicast() should not get called on TUNNEL");
   }

   public String getInfo() {
       List<RouterStub> stubs = stubManager.getStubs();
      if (stubs.isEmpty())
         return stubs.toString();
      else
         return "RouterStubs not yet initialized";
   }

   protected PhysicalAddress getPhysicalAddress() {
      return sock != null ? new IpAddress(bind_addr, sock.getLocalPort()) : null;
   }

   public interface TUNNELPolicy {
      public void connect(List<RouterStub> stubs, String group, Address addr, String logical_name, List<PhysicalAddress> phys_addrs);

      public void sendToAllMembers(List<RouterStub> stubs, String group, byte[] data, int offset, int length) throws Exception;

       public void sendToSingleMember(List<RouterStub> stubs, String group, Address dest, byte[] data, int offset,
               int length) throws Exception;
   }

   private class DefaultTUNNELPolicy implements TUNNELPolicy {

      public void sendToAllMembers(List<RouterStub> stubs, String group, byte[] data, int offset, int length)
               throws Exception {
         boolean sent = false;
          if(stubs.size() > 1)
              Collections.shuffle(stubs);  // todo: why is this needed ?
         for (RouterStub stub : stubs) {
            try {
                if(!stub.isConnected())
                    continue;
                stub.sendToAllMembers(group, data, offset, length);
               if (log.isTraceEnabled())
                  log.trace("sent a message to all members, GR used " + stub.getGossipRouterAddress());
               sent = true;
               break;
            } catch (Exception e) {
                if (log.isWarnEnabled())
                    log.warn("failed sending a message to all members, GR used " + stub.getGossipRouterAddress());
            }
         }
         if (!sent)
            throw new Exception("None of the available stubs " + stubs + " accepted a multicast message");
      }

      public void sendToSingleMember(List<RouterStub> stubs, String group, Address dest, byte[] data, int offset, int length) throws Exception {
         boolean sent = false;
          if(stubs.size() > 1)
              Collections.shuffle(stubs); 
         for (RouterStub stub : stubs) {
            try {
                if(!stub.isConnected())
                    continue;
                stub.sendToMember(group, dest, data, offset, length);
               if (log.isDebugEnabled())
                  log.debug("sent a message to " + dest + ", GR used " + stub.getGossipRouterAddress());
               sent = true;
               break;
            } catch (Exception e) {
                if (log.isWarnEnabled()) {
                    log.warn("failed sending a message to " + dest + ", GR used " + stub.getGossipRouterAddress());
                }
            }
         }
         if (!sent)
            throw new Exception("None of the available stubs " + stubs
                     + " accepted a message for dest " + dest);
      }

       public void connect(List<RouterStub> stubs, String group, Address addr, String logical_name, List<PhysicalAddress> phys_addrs) {
           for (RouterStub stub : stubs) {
               try {
                   stub.connect(group, addr, logical_name, phys_addrs);
               }
               catch (Exception e) {
                   if (log.isWarnEnabled())
                       log.warn("Failed connecting to GossipRouter at " + stub.getGossipRouterAddress());
                   stubManager.startReconnecting(stub);
               }
           }
       }
   }
}
