/**
 * Copyright (c) 2007-2012, Swedish Institute of Computer Science.
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * 3. Neither the name of the Institute nor the names of its contributors
 *    may be used to endorse or promote products derived from this software
 *    without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE INSTITUTE AND CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED.  IN NO EVENT SHALL THE INSTITUTE OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 *
 * This file is part of MSPSim.
 *
 * -----------------------------------------------------------------
 *
 * NetworkConnection
 *
 * Author  : Joakim Eriksson
 * Created : 31 mar 2008
 */
package se.sics.mspsim.util;

import java.io.DataInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.ArrayList;

import se.sics.mspsim.chip.PacketListener;

/**
 * @author joakim
 *
 */
public class NetworkConnection implements Runnable {

  private final static boolean DEBUG = false;
  private final static int DEFAULT_PORT = 4711;

  private ServerSocket serverSocket = null;
  private SendThread sendThread = null;
  private ConnectionThread[] connections = null;
  private PacketListener packetListener;

  public NetworkConnection() {
    if (connect(DEFAULT_PORT)) {
      System.out.println("NetworkConnection: Connected to network...");
    } else {
      setupServer(DEFAULT_PORT);
      System.out.println("NetworkConnection: Setup network server...");
    }
    sendThread = new SendThread();
  }
  
  public synchronized void addPacketListener(PacketListener listener) {
      packetListener = PacketListener.Proxy.INSTANCE.add(packetListener, listener);
  }

  public synchronized void removePacketListener(PacketListener listener) {
      packetListener = PacketListener.Proxy.INSTANCE.remove(packetListener, listener);
  }

  private void setupServer(int port) {
    try {
      serverSocket = new ServerSocket(port);
      if (DEBUG) System.out.println("NetworkConnection: setup of server socket finished... ");
      new Thread(this).start();
    } catch (IOException e) {
      e.printStackTrace();
    }
  }

  public void run() {
    System.out.println("NetworkConnection: Accepting new connections...");
    while (true) {
      try {
        Socket s = serverSocket.accept();
        if (DEBUG) System.out.println("NetworkConnection: New connection from " + s.getRemoteSocketAddress());
        connections = ArrayUtils.add(ConnectionThread.class, connections, new ConnectionThread(s));
      } catch (IOException e) {
        e.printStackTrace();
      }
    }
  }
  
  // Data incoming from the network!!! - forward to radio and if server, to
  // all other nodes
  private void dataReceived(byte[] data, ConnectionThread source) {
    PacketListener listener = this.packetListener;
    if (listener != null) {
      // Send this data to the transmitter in this node!
      listener.transmissionStarted();      
      listener.transmissionEnded(data);
    }

    // And if this is the server, propagate to the others
    if (serverSocket != null) {
      dataSent(data, source);
    }
  }

  // Data was sent from the radio in the node (or other node) and should
  // be sent out to other nodes!!!
  public void dataSent(byte[] receivedData) {
    dataSent(receivedData, null);
  }

  // Data was sent either from radio, or came from another "radio" -
  // and if so it should be propagated to all others.
  public void dataSent(byte[] receivedData, ConnectionThread source) {
    if (connections != null && sendThread != null) {
      sendThread.send(receivedData, source);
    }
  }

  private void printPacket(String prefix, byte[] data) {
    System.out.print("NetworkConnection: " + prefix);
    for (int i = 0, len = data.length; i < len; i++) {
      System.out.print(' ' + Utils.hex8(data[i]));
    }
    System.out.println();
  }
  
  private boolean connect(int port) {
    try {
      Socket socket = new Socket("127.0.0.1", port);
      connections = ArrayUtils.add(ConnectionThread.class, connections, new ConnectionThread(socket));
    } catch (UnknownHostException e) {
      return false;
    } catch (IOException e) {
      return false;
    }    
    return true;
  }

  private static class SendEvent {
    public final byte[] data;
    public final ConnectionThread source;
    public SendEvent(byte[] data, ConnectionThread source) {
      this.data = data;
      this.source = source;
    }
  }

  class SendThread implements Runnable {

    private ArrayList<SendEvent> queue = new ArrayList<SendEvent>();

    public SendThread() {
      new Thread(this).start();
    }

    public synchronized void send(byte[] receivedData, ConnectionThread source) {
      queue.add(new SendEvent(receivedData, source));
      notifyAll();
    }

    public synchronized SendEvent getNext() throws InterruptedException {
      while (queue.isEmpty()) {
        wait();
      }
      return queue.remove(0);
    }

    private void sendPacket(SendEvent event) {
      ConnectionThread[] cthr = connections;
      if (cthr != null) {
        for (int i = 0; i < cthr.length; i++) {
          if (cthr[i].isClosed()) {
            connections = ArrayUtils.remove(connections, cthr[i]);
            // Do not write back to the source
          } else if (cthr[i] != event.source){
            try {
              cthr[i].output.write(event.data, 0, event.data.length);
              cthr[i].output.flush();
            } catch (IOException e) {
              e.printStackTrace();
              cthr[i].close();
            }
          }
        }
        if (DEBUG) {
//              System.out.println("NetworkConnection: wrote " + receivedData.length + " bytes");
          printPacket("sent", event.data);
        }
      }
    }

    public void run() {
      try {
        SendEvent event;
        do {
          event = getNext();
          if (event != null) {
            sendPacket(event);
          }
        } while (event != null);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    }
  }

  class ConnectionThread implements Runnable {
    Socket socket;
    DataInputStream input;
    OutputStream output;
    
    public ConnectionThread(Socket socket) throws IOException {
      this.socket = socket;
      input = new DataInputStream(socket.getInputStream());
      output = socket.getOutputStream();
      new Thread(this).start();
    }
    
    public void close() {
      try {
        input.close();
        socket.close();
      } catch (IOException e) {
        e.printStackTrace();
      }
      socket = null;
    }
    
    public boolean isClosed() {
      return socket == null;
    }
    
    public void run() {
      if (DEBUG) System.out.println("NetworkConnection: Started connection thread...");
      try {
        while (socket != null) {
          int len = input.read();
          if (len > 0) {
            byte[] buffer = new byte[len + 1];
            buffer[0] = (byte) (len & 0xff);
            input.readFully(buffer, 1, len);
            if (DEBUG) {
//              System.out.println("NetworkConnection: Read packet with " + len + " bytes");
              printPacket("read", buffer);
            }
            dataReceived(buffer, this);
          }
        }
      } catch (IOException e) {
        e.printStackTrace();
        close();
      }
    }
  }
}
