package org.contikios.cooja.serialsocket;

/*
 * Copyright (c) 2014, TU Braunschweig.
 * Copyright (c) 2010, 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.
 *
 */

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.GridLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.text.NumberFormat;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import java.util.Observable;
import java.util.Observer;
import java.util.logging.Level;

import javax.swing.BorderFactory;
import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JFormattedTextField;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JSeparator;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
import javax.swing.border.EtchedBorder;
import javax.swing.text.NumberFormatter;

import org.apache.log4j.Logger;
import org.jdom.Element;

import org.contikios.cooja.ClassDescription;
import org.contikios.cooja.Cooja;
import org.contikios.cooja.Mote;
import org.contikios.cooja.MotePlugin;
import org.contikios.cooja.PluginType;
import org.contikios.cooja.Simulation;
import org.contikios.cooja.VisPlugin;
import org.contikios.cooja.interfaces.SerialPort;

/**
 * Socket to simulated serial port forwarder. Server version.
 * 
 * @author Fredrik Osterlind
 * @author Enrico Jorns
 */
@ClassDescription("Serial Socket (SERVER)")
@PluginType(PluginType.MOTE_PLUGIN)
public class SerialSocketServer extends VisPlugin implements MotePlugin {
  private static final long serialVersionUID = 1L;
  private static final Logger logger = Logger.getLogger(SerialSocketServer.class);

  private final static int STATUSBAR_WIDTH = 350;

  private static final Color COLOR_NEUTRAL = Color.DARK_GRAY;
  private static final Color COLOR_POSITIVE = new Color(0, 161, 83);
  private static final Color COLOR_NEGATIVE = Color.RED;
  
  private final int SERVER_DEFAULT_PORT;

  private final SerialPort serialPort;
  private Observer serialDataObserver;

  private JLabel socketToMoteLabel;
  private JLabel moteToSocketLabel;
  private JLabel socketStatusLabel;
  private JFormattedTextField listenPortField;
  private JButton serverStartButton;

  private int inBytes = 0, outBytes = 0;

  private ServerSocket serverSocket;
  private Socket clientSocket;

  private Mote mote;
  private Simulation simulation;

  public SerialSocketServer(Mote mote, Simulation simulation, final Cooja gui) {
    super("Serial Socket (SERVER) (" + mote + ")", gui, false);
    this.mote = mote;
    this.simulation = simulation;

    updateTimer.start();

    SERVER_DEFAULT_PORT = 60000 + mote.getID();

    /* GUI components */
    if (Cooja.isVisualized()) {

      setResizable(false);
      setLayout(new BorderLayout());

      // --- Server Port setup
      
      GridBagConstraints c = new GridBagConstraints();
      JPanel socketPanel = new JPanel(new GridBagLayout());
      socketPanel.setBorder(BorderFactory.createEmptyBorder(2, 2, 2, 2));
      
      JLabel label = new JLabel("Listen port: ");
      c.gridx = 0;
      c.gridy = 0;
      c.weightx = 0.1;
      c.anchor = GridBagConstraints.EAST;
      socketPanel.add(label, c);
      
      NumberFormat nf = NumberFormat.getIntegerInstance();
      nf.setGroupingUsed(false);
      listenPortField = new JFormattedTextField(new NumberFormatter(nf));
      listenPortField.setColumns(5);
      listenPortField.setText(String.valueOf(SERVER_DEFAULT_PORT));
      c.gridx++;
      c.weightx = 0.0;
      socketPanel.add(listenPortField, c);

      serverStartButton = new JButton("Start") { // Button for label toggeling
        private final String altString = "Stop";
        
        @Override
        public Dimension getPreferredSize() {
          String origText = getText();
          Dimension origDim = super.getPreferredSize();
          setText(altString);
          Dimension altDim = super.getPreferredSize();
          setText(origText);
          return new Dimension(Math.max(origDim.width, altDim.width), origDim.height);
        }
      };
      c.gridx++;
      c.weightx = 0.1;
      c.anchor = GridBagConstraints.EAST;
      socketPanel.add(serverStartButton, c);

      c.gridx = 0;
      c.gridy++;
      c.gridwidth = GridBagConstraints.REMAINDER;
      c.fill = GridBagConstraints.HORIZONTAL;
      socketPanel.add(new JSeparator(JSeparator.HORIZONTAL), c);
      
      add(BorderLayout.NORTH, socketPanel);
      
      // --- Incoming / outgoing info

      JPanel connectionInfoPanel = new JPanel(new GridLayout(0, 2));
      connectionInfoPanel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
      c = new GridBagConstraints();

      label = new JLabel("socket -> mote: ");
      label.setHorizontalAlignment(JLabel.RIGHT);
      c.gridx = 0;
      c.gridy = 0;
      c.anchor = GridBagConstraints.EAST;
      connectionInfoPanel.add(label);

      socketToMoteLabel = new JLabel("0 bytes");
      c.gridx++;
      c.anchor = GridBagConstraints.WEST;
      connectionInfoPanel.add(socketToMoteLabel);

      label = new JLabel("mote -> socket: ");
      label.setHorizontalAlignment(JLabel.RIGHT);
      c.gridx = 0;
      c.gridy++;
      c.anchor = GridBagConstraints.EAST;
      connectionInfoPanel.add(label);

      moteToSocketLabel = new JLabel("0 bytes");
      c.gridx++;
      c.anchor = GridBagConstraints.WEST;
      connectionInfoPanel.add(moteToSocketLabel);

      add(BorderLayout.CENTER, connectionInfoPanel);

      // --- Status bar
      
      JPanel statusBarPanel = new JPanel(new BorderLayout()) {
        @Override
        public Dimension getPreferredSize() {
          Dimension d = super.getPreferredSize();
          return new Dimension(STATUSBAR_WIDTH, d.height);
        }
      };
      statusBarPanel.setLayout(new BoxLayout(statusBarPanel, BoxLayout.LINE_AXIS));
      statusBarPanel.setBorder(BorderFactory.createEtchedBorder(EtchedBorder.RAISED));
      label = new JLabel("Status: ");
      statusBarPanel.add(label);
      
      socketStatusLabel = new JLabel("Idle");
      socketStatusLabel.setForeground(Color.DARK_GRAY);
      statusBarPanel.add(socketStatusLabel);
      
      add(BorderLayout.SOUTH, statusBarPanel);

      serverStartButton.addActionListener(new ActionListener() {

        @Override
        public void actionPerformed(ActionEvent e) {
          if (e.getActionCommand().equals("Start")) {
            try {
              listenPortField.commitEdit();
            } catch (ParseException ex) {
              java.util.logging.Logger.getLogger(SerialSocketClient.class.getName()).log(Level.SEVERE, null, ex);
            }
            startServer(((Long) listenPortField.getValue()).intValue());
          } else {
            stopServer();
          }
        }
      });
      
      pack();
    }

    /* Mote serial port */
    serialPort = (SerialPort) mote.getInterfaces().getLog();
    if (serialPort == null) {
      throw new RuntimeException("No mote serial port");
    }

    if (Cooja.isVisualized()) {
      // gui updates for server status updates
      addServerListener(new ServerListener() {

        @Override
        public void onServerStarted(final int port) {
          SwingUtilities.invokeLater(new Runnable() {

            @Override
            public void run() {
              System.out.println("onServerStarted");
              socketStatusLabel.setForeground(COLOR_NEUTRAL);
              socketStatusLabel.setText("Listening on port " + String.valueOf(port));
              listenPortField.setEnabled(false);
              serverStartButton.setText("Stop");
            }
          });
        }

        @Override
        public void onClientConnected(final Socket client) {
          SwingUtilities.invokeLater(new Runnable() {

            @Override
            public void run() {
              socketStatusLabel.setForeground(COLOR_POSITIVE);
              socketStatusLabel.setText("Client "
                      + client.getInetAddress() + ":" + client.getPort()
                      + " connected.");
            }
          });
        }

        @Override
        public void onClientDisconnected() {
          SwingUtilities.invokeLater(new Runnable() {

            @Override
            public void run() {
              // XXX check why needed
              if (serverSocket != null) {
                socketStatusLabel.setForeground(COLOR_NEUTRAL);
                socketStatusLabel.setText("Listening on port " + String.valueOf(serverSocket.getLocalPort()));
              }
            }
          });
        }

        @Override
        public void onServerStopped() {
          SwingUtilities.invokeLater(new Runnable() {

            @Override
            public void run() {
              listenPortField.setEnabled(true);
              serverStartButton.setText("Start");
              socketStatusLabel.setForeground(COLOR_NEUTRAL);
              socketStatusLabel.setText("Idle");
            }
          });
        }

        @Override
        public void onServerError(final String msg) {
          SwingUtilities.invokeLater(new Runnable() {

            @Override
            public void run() {
              socketStatusLabel.setForeground(COLOR_NEGATIVE);
              socketStatusLabel.setText(msg);
            }
          });
        }

      });
    }

  }

  private List<ServerListener> listeners = new LinkedList<>();
  
  public interface ServerListener {
    void onServerStarted(int port);
    void onClientConnected(Socket client);
    void onClientDisconnected();
    void onServerStopped();
    void onServerError(String msg);
  }
  
  private void addServerListener(ServerListener listener) {
    listeners.add(listener);
  }
  
  public void notifyServerStarted(int port) {
    for (ServerListener listener : listeners) {
      listener.onServerStarted(port);
    }
  }
  
  public void notifyClientConnected(Socket client) {
    for (ServerListener listener : listeners) {
      listener.onClientConnected(client);
    }
  }

  public void notifyClientDisconnected() {
    for (ServerListener listener : listeners) {
      listener.onClientDisconnected();
    }
  }
  
  public void notifyServerStopped() {
    for (ServerListener listener : listeners) {
      listener.onServerStopped();
    }
  }
  
  public void notifyServerError(String msg) {
    for (ServerListener listener : listeners) {
      listener.onServerError(msg);
    }
  }
  
  /**
   * Start server ..
   * @param port 
   */
  public void startServer(int port) {
    try {
      serverSocket = new ServerSocket(port);
      logger.info("Listening on port: " + port);
      notifyServerStarted(port);
    } catch (IOException ex) {
      logger.error(ex.getMessage());
      notifyServerError(ex.getMessage());
      return;
    }

    new Thread() {
      private Thread incomingDataHandler;
      @Override
      public void run() {
        while (!serverSocket.isClosed()) {
          try {
            // wait for next client
            Socket candidateSocket = serverSocket.accept();

            // reject connection if already one client connected
            if (clientSocket != null && !clientSocket.isClosed()) {
              logger.info("Refused connection of client " + candidateSocket.getInetAddress());
              candidateSocket.close();
              continue;
            }

            clientSocket = candidateSocket;

            /* Start handler for data input from socket */
            incomingDataHandler = new Thread(new IncomingDataHandler());
            incomingDataHandler.start();

            /* Observe serial port for outgoing data */
            serialDataObserver = new SerialDataObserver();
            serialPort.addSerialDataObserver(serialDataObserver);

            inBytes = outBytes = 0;

            logger.info("Client connected: " + clientSocket.getInetAddress());
            notifyClientConnected(clientSocket);

          } catch (IOException e) {
            logger.info("Listening thread shut down: " + e.getMessage());
            try {
              serverSocket.close();
            } catch (IOException ex) {
              logger.error(ex);
            }
          }
        }
        cleanupClient();
        if (incomingDataHandler != null) {
          // Wait for reader thread to terminate
          try {
            incomingDataHandler.join(500);
          } catch (InterruptedException ex) {
            logger.warn(ex);
          }
        }
        notifyServerStopped();
      }
    }.start();
  }

  /**
   * Stops server by closing server listen socket.
   */
  public void stopServer() {
    try {
      serverSocket.close();
    } catch (IOException ex) {
      logger.error(ex);
    }
  }

  /* Forward data: virtual port -> mote */
  private class IncomingDataHandler implements Runnable {

    DataInputStream in;

    @Override
    public void run() {
      int numRead = 0;
      byte[] data = new byte[1024];
      try {
        in = new DataInputStream(clientSocket.getInputStream());
      } catch (IOException ex) {
        logger.error(ex);
        return;
      }

      logger.info("Forwarder: socket -> serial port");
      while (numRead >= 0) {
        final int finalNumRead = numRead;
        final byte[] finalData = data;
        /* We are not on the simulation thread */
        simulation.invokeSimulationThread(new Runnable() {

          @Override
          public void run() {
            for (int i = 0; i < finalNumRead; i++) {
              serialPort.writeByte(finalData[i]);
            }
            inBytes += finalNumRead;
          }
        });

        try {
          numRead = in.read(data);
        } catch (IOException e) {
          logger.info(e.getMessage());
          numRead = -1;
        }
      }
      logger.info("End of Stream");
      cleanupClient();
    }
  }

  private class SerialDataObserver implements Observer {
    
    DataOutputStream out;

    public SerialDataObserver() {
      try {
        out = new DataOutputStream(clientSocket.getOutputStream());
      } catch (IOException ex) {
        logger.error(ex);
        out = null;
      }
    }

    @Override
    public void update(Observable obs, Object obj) {
      try {
        if (out == null) {
          /*logger.debug("out is null");*/
          return;
        }

        out.write(serialPort.getLastSerialData());
        out.flush();

        outBytes++;
      } catch (IOException ex) {
        logger.error(ex);
        cleanupClient();
      }
    }

  }

  @Override
  public Collection<Element> getConfigXML() {
    List<Element> config = new ArrayList<>();
    Element element;
    
    // XXX isVisualized guards?

    element = new Element("port");
    if (serverSocket == null || !serverSocket.isBound()) {
      try {
        listenPortField.commitEdit();
        element.setText(String.valueOf((Long) listenPortField.getValue()));
      } catch (ParseException ex) {
        logger.error(ex.getMessage());
        listenPortField.setText("null");
      }
    } else {
      element.setText(String.valueOf(serverSocket.getLocalPort()));
    }
    config.add(element);

    element = new Element("bound");
    if (serverSocket == null) {
      element.setText(String.valueOf(false));
    } else {
      element.setText(String.valueOf(!serverSocket.isClosed()));
    }
    config.add(element);

    return config;
  }

  @Override
  public boolean setConfigXML(Collection<Element> configXML, boolean visAvailable) {
    Integer port = null;
    boolean bound = false;
    
    for (Element element : configXML) {
      switch (element.getName()) {
        case "port":
          port = Integer.parseInt(element.getText());
          break;
        case "bound":
          bound = Boolean.parseBoolean(element.getText());
          break;
        default:
          logger.warn("Unknwon config element: " + element.getName());
          break;
      }
    }
    if (Cooja.isVisualized()) {
      if (port != null) {
        listenPortField.setText(String.valueOf(port));
      }
      if (bound) {
        serverStartButton.doClick();
      }
    } else {
      // if bound and all set up, start client
      if (port != null) {
        startServer(port);
      } else {
        logger.error("Server not started due to incomplete configuration");
      }
    }

    return true;
  }

  private void cleanupClient() {
    try {
      if (clientSocket != null) {
        clientSocket.close();
        clientSocket = null;
      }
    } catch (IOException e1) {
      logger.error(e1.getMessage());
    }

    serialPort.deleteSerialDataObserver(serialDataObserver);

    notifyClientDisconnected();
  }

  private boolean closed = false;

  @Override
  public void closePlugin() {
    closed = true;
    cleanupClient();
    try {
      if (serverSocket != null) {
        serverSocket.close();
      }
    } catch (IOException ex) {
      logger.error(ex);
    }
  }

  @Override
  public Mote getMote() {
    return mote;
  }

  private static final int UPDATE_INTERVAL = 150;
  private Timer updateTimer = new Timer(UPDATE_INTERVAL, new ActionListener() {
    @Override
	  public void actionPerformed(ActionEvent e) {
		  if (closed) {
			  updateTimer.stop();
			  return;
		  }
		  
		  socketToMoteLabel.setText(inBytes + " bytes");
		  moteToSocketLabel.setText(outBytes + " bytes");
	  }
  });
}

