/*
 * JBoss, Home of Professional Open Source
 * Copyright 2005, JBoss Inc., and individual contributors as indicated
 * by the @authors tag. See the copyright.txt in the distribution for a
 * full listing of individual contributors.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package org.jboss.remoting.transport.socket;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;

import org.jboss.remoting.InvokerLocator;
import org.jboss.remoting.ServerInvoker;
import org.jboss.remoting.transport.PortUtil;

/**
 * SocketServerInvoker is the server-side of a SOCKET based transport
 *
 * @author <a href="mailto:jhaynie@vocalocity.net">Jeff Haynie</a>
 * @version $Revision: 1.4.2.4 $
 */
public class SocketServerInvoker extends ServerInvoker
{
   private static final boolean DEBUG = Boolean.getBoolean("jboss.remoting.socket.debug");
   private InetAddress addr;
   private int port;
   private ServerSocket socket;
   private boolean started = false;
   private Listener listener;
   private List clients = Collections.synchronizedList(new ArrayList());
   static int clientCount = 0;

   public SocketServerInvoker(InvokerLocator locator)
   {
      super(locator);
      try
      {
         setup();
      }
      catch (Exception ex)
      {
         throw new RuntimeException(ex.getMessage());
      }
   }

   public InetAddress getAddress()
   {
      return addr;
   }

   public int getPort()
   {
      return port;
   }

   protected void setup()
      throws Exception
   {
      this.addr = InetAddress.getByName(locator.getHost());
      this.port = locator.getPort();
      if (this.port <= 0)
      {
         this.port = PortUtil.findFreePort();
         // re-write locator since the port is different
         this.locator = new InvokerLocator(locator.getProtocol(), locator.getHost(), this.port, locator.getPath(), locator.getParameters());
      }
   }

   protected void finalize() throws Throwable
   {
      stop();
      super.finalize();
   }


   public synchronized void start() throws IOException
   {
      if (!started)
      {
         if (log.isInfoEnabled())
         {
            log.info("Starting Socket Transport at: " + this.locator);
         }
         this.socket = new ServerSocket(this.port, 100, this.addr);
         this.port = socket.getLocalPort();
         this.started = true;
         this.listener = new Listener();
         this.listener.start();
      }
      super.start();
   }

   public synchronized void stop()
   {
      if (started)
      {
         if (log.isInfoEnabled())
         {
            log.info("Stopping Socket Transport at: " + this.locator);
         }
         this.started = false;
         this.listener.interrupt();
         try
         {
            this.listener.join(500);
         }
         catch (Throwable ig)
         {
         }
         if (clients.isEmpty() == false)
         {
            // make a copy
            Iterator iter = new ArrayList(clients).iterator();
            while (iter.hasNext())
            {
               Client client = (Client) iter.next();
               if (log.isDebugEnabled()) log.debug("Client: " + client.getName() + " is connected during stop, interrupting him...");
               client.interrupt();
               iter.remove();
            }
         }
         clients.clear();
         try
         {
            this.socket.close();
         }
         catch (Throwable ig)
         {
         }
         this.socket = null;
         this.listener = null;
      }
      super.stop();
   }

   /**
    * returns true if the transport is bi-directional in nature, for example,
    * SOAP in unidirectional and SOCKETs are bi-directional (unless behind a firewall
    * for example).
    *
    * @return
    */
   public boolean isTransportBiDirectional()
   {
      return true;
   }

   private final class Client extends Thread
   {
      Socket socket;
      InputStream input;
      OutputStream output;

      Client(Socket socket)
         throws IOException
      {
         super("SocketServerInvoker-Client [" + (++clientCount) + "] " + socket);
         setDaemon(true);
         this.socket = socket;
         this.input = new BufferedInputStream(socket.getInputStream());
         this.output = new BufferedOutputStream(socket.getOutputStream());
         clients.add(this);
      }

      public void run()
      {
         byte lengthbuf[] = new byte[20];
         byte buff[] = new byte[4096];
         ByteArrayOutputStream bos = new ByteArrayOutputStream();
         while (started)
         {
            // read in the length
            try
            {
               // read in the length header
               int x = input.read(lengthbuf);
               if (x < 0)
               {
                  // socket is closed
                  break;
               }
               // calcuate the length of the invocation data buffer
               int buflen = SocketUtil.len(lengthbuf);
               int count = 0;

               if (DEBUG)
               {
                  System.err.println("++ read invocation length header: " + buflen);
               }
               if (buflen <= 0)
               {
                  continue;
               }
               // read in the invocation buffer
               while (count < buflen)
               {
                  int c = input.read(buff);
                  if (DEBUG)
                  {
                     System.err.println("++ read invocation buffer size: " + c + " bytes");
                  }
                  if (c < 0)
                  {
                     // socket is closed
                     break;
                  }
                  bos.write(buff, 0, c);
                  count += c;
               }
               if (!started)
               {
                  if (log.isDebugEnabled()) log.debug("after read, we're disconnected: " + this.getName() + ", stopping invocation");
                  // check in case during read we stopped
                  break;
               }
               bos.flush();
               if (DEBUG)
               {
                  System.err.println("++ read invocation buffer - total size is " + count + " bytes");
               }
               // call transport on the subclass, get the result to handback
               byte data[] = SocketServerInvoker.this.invoke(bos.toByteArray());
               if (DEBUG)
               {
                  System.err.println("++ returning invocation buffer size: " + data.length + " bytes");
               }
               // write the length of the data we're writing
               output.write(SocketUtil.pad(data.length, 20));
               // write out the result data
               output.write(data);
               output.flush();
            }
            catch (SocketException se)
            {
               if (se.getMessage().indexOf("Connection reset") > -1)
               {
                  break;
               }
               if (!started)
               {
                  break;
               }
               //FIXME
               se.printStackTrace();
            }
            catch (IOException io)
            {
               if (!started)
               {
                  break;
               }
               //FIXME
               io.printStackTrace();
            }
            finally
            {
               bos.reset();
            }
         }
         if (output != null)
         {
            try
            {
               output.close();
            }
            catch (Exception ig)
            {
            }
         }
         if (socket != null)
         {
            try
            {
               socket.close();
            }
            catch (Exception ig)
            {
            }
         }
         socket = null;
         output = null;
         clients.remove(this);
         if (log.isDebugEnabled())
         {
            log.debug("Client disconnected: " + this.getName());
         }
      }
   }

   private final class Listener extends Thread
   {
      public Listener()
      {
         super("SocketServerInvoker-Listener");
         setDaemon(false);
      }

      public void run()
      {
         while (started)
         {
            try
            {
               // blocks until a new client arrives
               Socket client = socket.accept();
               if (client != null)
               {
                  // make this a thread pool task
                  new Client(client).start();
               }
            }
            catch (Exception ex)
            {
               if (started)
               {
                  ex.printStackTrace();
               }
               else
               {
                  break;
               }
            }
         }
         try
         {
            socket.close();
         }
         catch (Exception ig)
         {
         }
         socket = null;
      }
   }
}
