/*
 * Copyright 2011 JBoss, by Red Hat, Inc
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *  
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.jboss.errai.tools.monitoring;

import org.jboss.errai.bus.client.api.Message;
import org.jboss.errai.bus.client.framework.RoutingFlag;
import org.jboss.errai.bus.server.util.ServerBusTools;

import javax.swing.*;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.table.AbstractTableModel;
import javax.swing.table.DefaultTableColumnModel;
import java.awt.*;
import java.awt.event.*;
import java.sql.Date;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Map;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;

import static java.lang.System.currentTimeMillis;
import static javax.swing.SwingUtilities.invokeLater;

/**
 * This represents the actual individual service monitoring windows.
 */
public class ServiceActivityMonitor extends JFrame implements Attachable {
  private JTable activityTable;
  private JTable detailsTable;
  private ActivityMonitorTableModel tableModel;
  private MessageDetailsTableModel detailsModel;

  protected String busId;
  protected String service;
  protected ServerMonitorPanel serverMonitor;

  private boolean lockable;
  private boolean scrollLock = true;
  private int lastScrollAmount;

  protected ActivityProcessor.Handle handle;

  private ObjectExplorer explorer;

  protected WindowListener defaultWindowListener;

  public ServiceActivityMonitor(final ServerMonitorPanel serverMonitor, final String busId, final String service) {
    this.serverMonitor = serverMonitor;
    this.busId = busId;
    this.service = service;

    updateTitle(null);

    tableModel = new ActivityMonitorTableModel();

    activityTable = new JTable(tableModel);


    activityTable.setDefaultRenderer(Message.class, new MessageCellRenderer());
    activityTable.setAutoResizeMode(JTable.AUTO_RESIZE_LAST_COLUMN);

    detailsModel = new MessageDetailsTableModel();

    detailsTable = new JTable(detailsModel);
    activityTable.setAutoResizeMode(JTable.AUTO_RESIZE_LAST_COLUMN);

    activityTable.getSelectionModel().addListSelectionListener(new ListSelectionListener() {
      public void valueChanged(ListSelectionEvent e) {
        detailsModel.clear();

        Message m = UiHelper.uglyReEncode((String) tableModel.getValueAt(activityTable.getSelectedRow(), 1));
        if (m == null) return;

        for (Map.Entry<String, Object> entry : m.getParts().entrySet()) {
          detailsModel.addPart(entry.getKey(), entry.getValue());
        }

        detailsModel.fireTableRowsUpdated(0, m.getParts().size() - 1);
        detailsModel.fireTableDataChanged();
      }
    });

    detailsTable.getSelectionModel().addListSelectionListener(new ListSelectionListener() {
      public void valueChanged(ListSelectionEvent e) {
        invokeLater(new Runnable() {
          public void run() {
            if (detailsTable.getSelectedRow() == -1 &&
                detailsTable.getSelectedRow() >= detailsModel.getRowCount()) return;

            Object v = detailsModel.getValueAt(detailsTable.getSelectedRow(), 1);

            explorer.setRoot(v);
            explorer.buildTree();
          }
        });
      }
    });

    final JScrollPane activityScroll;
    final JSplitPane bottomSplit = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT,
        new JScrollPane(detailsTable), new JScrollPane(explorer = new ObjectExplorer()));
    bottomSplit.setDividerLocation(300);


    final JSplitPane splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT,
        activityScroll = new JScrollPane(activityTable),
        bottomSplit);


    activityTable.addKeyListener(new KeyListener() {
      public void keyTyped(KeyEvent e) {
        if (!Character.isWhitespace(e.getKeyChar()) || e.getKeyChar() == '\n') {
          searchDialog.setAlwaysOnTop(true);
          searchDialog.setLocationRelativeTo(ServiceActivityMonitor.this);
          if (e.getKeyChar() != '\n') searchDialog.keyTyped(e);
          searchDialog.setVisible(true);
        }
      }

      public void keyPressed(KeyEvent e) {
      }

      public void keyReleased(KeyEvent e) {
      }
    });

    KeyStroke escKeyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0);

    getLayeredPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW)
        .put(escKeyStroke, "esc-pressed");

    getLayeredPane().getActionMap().put("esc-pressed", new AbstractAction() {
      public void actionPerformed(ActionEvent e) {
        if (tableModel.isFiltered()) {
          invokeLater(new Runnable() {
            public void run() {
              activityTable.clearSelection();
              tableModel.setFilterTerm(null);
              activityTable.setModel(tableModel);
              tableModel.fireTableDataChanged();
              updateTitle(null);
            }
          });

        }
      }
    });


    splitPane.setDividerLocation(150);

    final JScrollBar vertScroll = activityScroll.getVerticalScrollBar();
    vertScroll.addMouseListener(new MouseListener() {
      public void mouseClicked(MouseEvent e) {
      }

      public void mousePressed(MouseEvent e) {
        lockable = true;
      }

      public void mouseReleased(MouseEvent e) {
        lockable = false;
      }

      public void mouseEntered(MouseEvent e) {
      }

      public void mouseExited(MouseEvent e) {
      }
    });

    vertScroll.addMouseWheelListener(new MouseWheelListener() {
      public void mouseWheelMoved(MouseWheelEvent e) {
        lastScrollAmount = e.getScrollAmount();
      }
    });

    vertScroll.addAdjustmentListener(new AdjustmentListener() {
      public void adjustmentValueChanged(AdjustmentEvent e) {
        if (lockable || lastScrollAmount != 0) {
          scrollLock = (e.getValue() == e.getAdjustable().getMaximum() - e.getAdjustable().getVisibleAmount());
          return;
        }

        lastScrollAmount = 0;

        if (scrollLock) e.getAdjustable().setValue(e.getAdjustable().getMaximum());
      }
    });

    getContentPane().add(splitPane);

    Point point = serverMonitor.getMainMonitorGUI().getLocation();
    setLocation(point.x + 20, point.y + 20);
    setSize(500, 300);

    DefaultTableColumnModel defaultColumn = (DefaultTableColumnModel) activityTable.getColumnModel();

    defaultColumn.getColumn(0).setResizable(false);
    defaultColumn.getColumn(0).setPreferredWidth(120);
    defaultColumn.getColumn(0).setMaxWidth(120);

    defaultColumn = (DefaultTableColumnModel) detailsTable.getColumnModel();
    defaultColumn.getColumn(0).setPreferredWidth(120);
    defaultColumn.getColumn(0).setMaxWidth(250);

    setVisible(true);

    setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);

    addWindowListener(defaultWindowListener = new WindowListener() {
      public void windowOpened(WindowEvent e) {
      }

      public void windowClosing(WindowEvent e) {
      }

      public void windowClosed(WindowEvent e) {
        handle.dispose();
        serverMonitor.stopMonitor(service);
      }

      public void windowIconified(WindowEvent e) {
      }

      public void windowDeiconified(WindowEvent e) {
      }

      public void windowActivated(WindowEvent e) {
      }

      public void windowDeactivated(WindowEvent e) {
      }
    });
  }

  public void updateTitle(String s) {
    if (s == null) setTitle(service + "@" + busId);
    else setTitle(service + "@" + busId + ": " + s);
  }

  public class ActivityLogEntry {
    private long time;
    private String message;

    public ActivityLogEntry(long time, String message) {
      this.time = time;
      this.message = message;
    }

    public long getTime() {
      return time;
    }

    public void setTime(long time) {
      this.time = time;
    }

    public String getMessage() {
      return message;
    }

    public void setMessage(String message) {
      this.message = message;
    }
  }

  public class ActivityMonitorTableModel extends AbstractTableModel {
    private ArrayList<ActivityLogEntry> messages = new ArrayList<ActivityLogEntry>();
    private ArrayList<ActivityLogEntry> filteredMessages = new ArrayList<ActivityLogEntry>();

    private volatile Pattern filter = null;

    private final String[] COLS
        = {"Time", "Message Contents"};

    private final Class[] TYPES
        = {String.class, String.class};

    private DateFormat formatter = new SimpleDateFormat("hh:mm:ss.SSS");

    @Override
    public String getColumnName(int column) {
      return COLS[column];
    }

    public int getRowCount() {
      return filter == null ? messages.size() : filteredMessages.size();
    }

    public int getColumnCount() {
      return COLS.length;
    }

    public Object getValueAt(int rowIndex, int columnIndex) {
      if (rowIndex == -1) return null;
      if (filter == null) {
        switch (columnIndex) {
          case 0:
            return formatter.format(new Date(messages.get(rowIndex).getTime()));
          case 1:
            return messages.get(rowIndex).getMessage();
        }
      }
      else {
        switch (columnIndex) {
          case 0:
            return formatter.format(new Date(filteredMessages.get(rowIndex).getTime()));
          case 1:
            return filteredMessages.get(rowIndex).getMessage();
        }
      }

      return null;
    }

    @Override
    public Class<?> getColumnClass(int columnIndex) {
      return TYPES[columnIndex];
    }

    public void addMessage(long time, String message) {
      ActivityLogEntry entry = new ActivityLogEntry(time, message);
      messages.add(entry);
      if (filter != null) {
        if (filter.matcher(message).find()) {
          filteredMessages.add(entry);
          fireTableRowsInserted(filteredMessages.size() - 1, filteredMessages.size() - 1);
        }

      }
      else {
        fireTableRowsInserted(messages.size() - 1, messages.size() - 1);
      }
    }

    public void setFilterTerm(String filterTerm) {
      if (filterTerm != null && filterTerm.length() != 0) {
        Pattern filter = Pattern.compile(filterTerm);
        this.filteredMessages.clear();
        for (ActivityLogEntry entry : messages) {
          if (filter.matcher(entry.getMessage()).find()) {
            filteredMessages.add(entry);
          }
        }
        this.filter = filter;

      }
      else {
        this.filter = null;
        this.filteredMessages.clear();
      }

    }

    public boolean isFiltered() {
      return this.filter != null;
    }
  }

  public class MessageDetailsTableModel extends AbstractTableModel {
    private ArrayList<String> fields = new ArrayList<String>();
    private ArrayList<Object> values = new ArrayList<Object>();

    private final String[] COLS = {"Message Part", "Value"};
    private final Class[] TYPES = {String.class, Object.class};

    @Override
    public String getColumnName(int column) {
      return COLS[column];
    }

    @Override
    public Class<?> getColumnClass(int columnIndex) {
      return TYPES[columnIndex];
    }

    public int getRowCount() {
      return fields.size();
    }

    public int getColumnCount() {
      return COLS.length;
    }

    public Object getValueAt(int rowIndex, int columnIndex) {
      if (rowIndex != -1) {
        switch (columnIndex) {
          case 0:
            if (fields.size() > rowIndex)
              return fields.get(rowIndex);
          case 1:
            if (values.size() > rowIndex)
              return values.get(rowIndex);
        }
      }
      return null;
    }

    public void addPart(String field, Object value) {
      fields.add(field);
      values.add(value);
    }

    public void clear() {
      fields.clear();
      values.clear();
    }
  }

  class SearchDialog extends JFrame {
    JTextField searchField;
    KeyEvent preEvent;

    public SearchDialog() {
      setUndecorated(true);

      setSize(300, 30);
      searchField = new JTextField();

      getContentPane().add(searchField);


      KeyStroke escKeyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0);

      getLayeredPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW)
          .put(escKeyStroke, "esc-pressed");

      getLayeredPane().getActionMap().put("esc-pressed", new AbstractAction() {
        public void actionPerformed(ActionEvent e) {
          searchField.setText("");
          preEvent = null;
          setVisible(false);
        }
      });

      searchField.addKeyListener(new KeyListener() {
        public void keyTyped(KeyEvent e) {
          if (e.getKeyChar() == '\n') {
            final String val = searchField.getText();
            searchField.setText("");

            invokeLater(new Runnable() {
              public void run() {
                setVisible(false);
                activityTable.clearSelection();
                try {
                  tableModel.setFilterTerm(val);
                }
                catch (PatternSyntaxException e) {
                  JOptionPane.showMessageDialog(new JFrame(), e.getMessage(), "Dialog",
                      JOptionPane.ERROR_MESSAGE);
                  return;
                }

                activityTable.setModel(tableModel);
                tableModel.fireTableDataChanged();
                updateTitle("Filtering[\"" + val + "\"]");
              }
            });

          }
        }

        public void keyPressed(KeyEvent e) {
        }

        public void keyReleased(KeyEvent e) {
        }
      });

      searchField.addFocusListener(new FocusListener() {
        public void focusGained(FocusEvent e) {
          if (preEvent != null) {
            searchField.setText(new String(new char[]{preEvent.getKeyChar()}));
            searchField.setCaretPosition(1);
            preEvent = null;
          }
        }

        public void focusLost(FocusEvent e) {
          searchField.setText("");
          preEvent = null;
          setVisible(false);
        }
      });

    }

    public void keyTyped(KeyEvent s) {
      preEvent = s;
    }


  }

  private SearchDialog searchDialog = new SearchDialog();


  public void notifyMessage(long time, Message message) {
    /*
    * This is a huge hack to get the display of the messages consistent with what the payload does when
    * encoded. And no it's not particularly efficient.  But since inbus messages are not encoded by JSON and
    * it would be wacky to make the monitoring API such that we had to scan for one or the other, this
    * is much more consistent from an API point-of-view.
    */
    tableModel.addMessage(time, ServerBusTools.encodeMessage(message));
  }

  public void attach(ActivityProcessor proc) {
    handle = proc.registerEvent(EventType.MESSAGE, new MessageMonitor() {
      public void monitorEvent(MessageEvent event) {
        Message m = (Message) event.getContents();
        // if the message is sent to the currently monitored bus (or is global) and the subject matches, then notify
        if ((event.getToBus().equals(busId) || !m.isFlagSet(RoutingFlag.NonGlobalRouting)) && service.equals(event.getSubject())) {
          notifyMessage(event.getTime(), (Message) event.getContents());
        }
      }
    });

    /**
     * When this monitor is attached send a request to replay the messages for this subject@bus.
     */
    proc.notifyEvent(currentTimeMillis(), EventType.REPLAY_MESSAGES, SubEventType.NONE, busId, busId, service, null, null, false);
  }
}
