/**
 * Open Visual Trace Route
 *
 * This library 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 3 of the License, or (at your option) any later version.

 * This library 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 library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 */
package org.leo.traceroute.ui.route;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Insets;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.MouseEvent;
import java.util.HashMap;
import java.util.Map;

import javax.swing.DefaultCellEditor;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JLabel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.ListSelectionModel;
import javax.swing.ToolTipManager;
import javax.swing.table.AbstractTableModel;
import javax.swing.table.DefaultTableCellRenderer;
import javax.swing.table.JTableHeader;
import javax.swing.table.TableColumn;

import org.leo.traceroute.core.ServiceFactory;
import org.leo.traceroute.core.network.DNSLookupService;
import org.leo.traceroute.core.route.ITraceRoute;
import org.leo.traceroute.core.route.RoutePoint;
import org.leo.traceroute.install.Env;
import org.leo.traceroute.install.Env.IConfigProvider;
import org.leo.traceroute.install.Env.OS;
import org.leo.traceroute.resources.CountryFlagManager.Resolution;
import org.leo.traceroute.resources.Resources;
import org.leo.traceroute.ui.whois.WhoIsPanel;

/**
 * RoutePanel $Id: RouteTablePanel.java 287 2016-10-30 20:35:16Z leolewis $
 *
 * @author Leo Lewis
 */
public class RouteTablePanel extends AbstractRoutePanel implements IConfigProvider {

	/**
	 * Column enum
	 */
	public enum Column {

		NUMBER("#", Integer.class, 28, true),
		COUNTRY_FLAG("", ImageIcon.class, 24, false),
		COUNTRY(Resources.getLabel("country"), String.class, 115, true),
		TOWN(Resources.getLabel("town"), String.class, 115, true),
		LAT(Resources.getLabel("lat"), Float.class, 95, true),
		LON(Resources.getLabel("lon"), Float.class, 95, true),
		IP(Resources.getLabel("ip"), String.class, 125, true),
		HOSTNAME(Resources.getLabel("hostname"), String.class, 190, true),
		LATENCY(Resources.getLabel("latency"), Integer.class, 35, true),
		DNS_LOOKUP(Resources.getLabel("dns.lookup"), Integer.class, 45, true),
		DISTANCE_TO_PREVIOUS(Resources.getLabel("distance.previous.node"), Integer.class, 45, true),
		WHO_IS(Resources.getLabel("whois"), String.class, 15, false),

		;

		/** Label */
		private final String _label;
		/** Class */
		private final Class<?> _class;
		/** width */
		private final int _width;
		/** if the column will be exported */
		private final boolean _export;

		/**
		 * Constructor
		 *
		 * @param label
		 * @param clazz
		 */
		private Column(final String label, final Class<?> clazz, final int width, final boolean export) {
			_label = label;
			_class = clazz;
			_width = width;
			_export = export;
		}

		/**
		 * Label of the column
		 *
		 * @return label
		 */
		public String getLabel() {
			return _label;
		}

		/**
		 * Return the value of the field width
		 *
		 * @return the value of width
		 */
		public int getWidth() {
			return _width;
		}

		/**
		 * Class of the column
		 *
		 * @return class
		 */
		public Class<?> getClazz() {
			return _class;
		}

		/**
		 * Return the value of the field export
		 * @return the value of export
		 */
		public boolean isExport() {
			return _export;
		}

		/**
		 * Get value for the given route point
		 *
		 * @param point
		 * @return value
		 */
		public Object getValue(final RoutePoint point) {
			switch (this) {
			case NUMBER:
				return point.getNumber();
			case COUNTRY_FLAG:
				return point.getCountryFlag(Resolution.R16);
			case COUNTRY:
				return point.getCountry();
			case TOWN:
				return point.getTown();
			case LAT:
				return point.getLat();
			case LON:
				return point.getLon();
			case IP:
				return point.getIp();
			case HOSTNAME:
				return point.getHostname();
			case LATENCY:
				return point.getLatency();
			case DNS_LOOKUP:
				return point.getDnsLookUpTime() == DNSLookupService.UNDEF ? "~" : point.getDnsLookUpTime();
			case DISTANCE_TO_PREVIOUS:
				return point.getDistanceToPrevious();
			case WHO_IS:
				return point.getIp();
			default:
				return null;
			}
		}
	}

	/** Index to column */
	private final Map<Integer, Column> _indexToColumn = new HashMap<>();
	{
		int i = 0;
		for (final Column column : Column.values()) {
			_indexToColumn.put(i++, column);
		}
	}

	/** Width */
	public static final int WIDTH = 550;

	/**  */
	private static final long serialVersionUID = 733606227606524464L;

	/** Table model */
	private RouteTableModel _model;

	/** Table */
	private JTable _table;

	/** Search is pending */
	private volatile boolean _searching = false;

	/** Focus adjusting */
	private volatile boolean _focusAdjusting = false;

	/** If the dns lookup function is enable for the current route */
	private volatile boolean _dnsLookup = false;

	/**
	 * Constructor
	 *
	 * @param route
	 * @param networkInterfaceChooser
	 */
	public RouteTablePanel(final ServiceFactory services) {
		super(services);
		init();
	}

	/**
	 * Initialization of the panel
	 *
	 * @param networkInterfaceChooser
	 */
	@SuppressWarnings("serial")
	private void init() {
		// the table of the route points
		_model = new RouteTableModel(_route);
		_table = new JTable(_model) {
			@Override
			protected JTableHeader createDefaultTableHeader() {
				return new JTableHeader(columnModel) {
					@Override
					public String getToolTipText(final MouseEvent e) {
						final Point p = e.getPoint();
						final int index = columnModel.getColumnIndexAtX(p.x);
						final int realIndex = columnModel.getColumn(index).getModelIndex();
						return _model.getColumnName(realIndex);
					}
				};
			}
		};
		final JScrollPane scrollPane = new JScrollPane(_table);
		_table.setFillsViewportHeight(true);
		add(scrollPane, BorderLayout.CENTER);
		_table.setShowGrid(false);
		_table.setPreferredScrollableViewportSize(new Dimension(WIDTH, 250));
		_table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
		_table.setAutoCreateRowSorter(true);
		_table.getTableHeader().setReorderingAllowed(true);
		// on selection change, notify the route to focus on the selection
		_table.getSelectionModel().addListSelectionListener(e -> {
			if (!_focusAdjusting) {
				try {
					_focusAdjusting = true;
					if (!e.getValueIsAdjusting() && _table.getSelectedRow() != -1) {
						final int index = _table.convertRowIndexToModel(_table.getSelectedRow());
						if (index >= 0 && index < _route.getRoute().size()) {
							_route.focus(_route.getRoute().get(index), true);
						}
					}
				} finally {
					_focusAdjusting = false;
				}
			}
		});
		// special renderer
		for (int colIndex = 0; colIndex < _table.getColumnCount(); colIndex++) {
			final TableColumn col = _table.getColumnModel().getColumn(colIndex);
			col.setCellRenderer(new RouteCellRenderer());
			_table.getColumnModel().getColumn(colIndex).setPreferredWidth(_indexToColumn.get(colIndex).getWidth());
			if (_indexToColumn.get(colIndex) == Column.WHO_IS) {
				col.setCellEditor(new ButtonCellEditor());
			}
		}
		ToolTipManager.sharedInstance().registerComponent(_table);
		Env.INSTANCE.registerConfigProvider(this);
	}

	@Override
	public String name() {
		return getClass().getSimpleName();
	}

	/**
	 * @see org.leo.traceroute.install.Env.IConfigProvider#load(java.util.Map)
	 */
	@Override
	public void load(final Map<String, String> config) {
		for (int colIndex = 0; colIndex < _table.getColumnCount(); colIndex++) {
			final Column column = _indexToColumn.get(colIndex);
			final String width = config.get(column.name());
			if (width != null) {
				_table.getColumnModel().getColumn(colIndex).setPreferredWidth(Integer.parseInt(width));
			}
		}
	}

	/**
	 * @see org.leo.traceroute.install.Env.IConfigProvider#save()
	 */
	@Override
	public Map<String, String> save() {
		final Map<String, String> widths = new HashMap<>();
		for (int colIndex = 0; colIndex < _table.getColumnCount(); colIndex++) {
			final Column column = _indexToColumn.get(colIndex);
			final int w = _table.getColumnModel().getColumn(colIndex).getPreferredWidth();
			widths.put(column.name(), String.valueOf(w));
		}
		return widths;
	}

	/**
	 * @see org.leo.traceroute.core.RouteListener#newRoute(boolean)
	 */
	@Override
	public void newRoute(final boolean dnsLookup) {
		_searching = true;
		_dnsLookup = dnsLookup;
		_model.fireTableDataChanged();
	}

	/**
	 * @see org.leo.traceroute.core.RouteListener#routePointAdded(org.leo.traceroute.core.RoutePoint)
	 */
	@Override
	public void routePointAdded(final RoutePoint p) {
		_model.fireTableDataChanged();
	}

	/**
	 * @see org.leo.traceroute.core.RouteListener#done()
	 */
	@Override
	public void routeDone(final long tracerouteTime, final long lengthInKm) {
		traceRouteEnded();
	}

	/**
	 * @see org.leo.traceroute.core.RouteListener#error(org.leo.traceroute.core.RouteException)
	 */
	@Override
	public void error(final Exception exception, final Object origin) {
		traceRouteEnded();
	}

	/**
	 * @see org.leo.traceroute.core.RouteListener#cancelled()
	 */
	@Override
	public void routeCancelled() {
		traceRouteEnded();
	}

	/**
	 * @see org.leo.traceroute.core.RouteListener#timeout()
	 */
	@Override
	public void routeTimeout() {
		traceRouteEnded();
	}

	/**
	 * @see org.leo.traceroute.core.route.IRouteListener#maxHops()
	 */
	@Override
	public void maxHops() {
		traceRouteEnded();
	}

	/**
	 * Trace root ended
	 *
	 * @param complete complete or cancel/error
	 * @param label label to show in the progress bar
	 */
	private void traceRouteEnded() {
		_searching = false;
		_model.fireTableDataChanged();
	}

	/**
	 * @see org.leo.traceroute.core.IRouteListener#focusRoute(org.leo.traceroute.core.RoutePoint, boolean, boolean)
	 */
	@Override
	public void focusRoute(final RoutePoint point, final boolean isTracing, final boolean animation) {
		if (!_focusAdjusting) {
			try {
				_focusAdjusting = true;
				final int index = _route.getRoute().indexOf(point);
				_table.getSelectionModel().setSelectionInterval(index, index);
				_table.scrollRectToVisible(new Rectangle(_table.getCellRect(index, 0, true)));
			} finally {
				_focusAdjusting = false;
			}
		}
	}

	/**
	 * Dispose the panel
	 */
	@Override
	public void dispose() {
		super.dispose();
		_model.dispose();
		Env.INSTANCE.unregisterConfigProvider(this);
	}

	/**
	 * RouteTableModel
	 *
	 * @author Leo Lewis
	 */
	private class RouteTableModel extends AbstractTableModel {

		/**  */
		private static final long serialVersionUID = 760575517959483510L;

		/** Route (Data model) */
		private final ITraceRoute _route;

		/**
		 * Constructor
		 *
		 * @param route
		 */
		public RouteTableModel(final ITraceRoute route) {
			_route = route;
		}

		/**
		 * @see javax.swing.table.TableModel#getColumnCount()
		 */
		@Override
		public int getColumnCount() {
			return Column.values().length;
		}

		/**
		 * @see javax.swing.table.TableModel#getRowCount()
		 */
		@Override
		public int getRowCount() {
			return _route.getRoute().size();
		}

		/**
		 * @see javax.swing.table.TableModel#getValueAt(int, int)
		 */
		@Override
		public Object getValueAt(final int row, final int col) {
			final RoutePoint point = _route.getRoute().get(row);
			return _indexToColumn.get(col).getValue(point);
		}

		@Override
		public Class<?> getColumnClass(final int column) {
			return _indexToColumn.get(column).getClazz();
		}

		/**
		 * @see javax.swing.table.AbstractTableModel#getColumnName(int)
		 */
		@Override
		public String getColumnName(final int column) {
			return _indexToColumn.get(column).getLabel();
		}

		/**
		 * @see javax.swing.table.AbstractTableModel#isCellEditable(int, int)
		 */
		@Override
		public boolean isCellEditable(final int row, final int column) {
			if (_indexToColumn.get(column) == Column.WHO_IS) {
				final RoutePoint point = _route.getRoute().get(row);
				return point != null && !point.isUnknown();
			}
			return false;
		}

		/**
		 * Dispose the model
		 */
		public void dispose() {

		}
	}

	/**
	 * RouteCellRenderer
	 *
	 * @author Leo Lewis
	 */
	private class RouteCellRenderer extends DefaultTableCellRenderer {

		/**  */
		private static final long serialVersionUID = 1622508565550177571L;

		/**
		 * @see javax.swing.table.DefaultTableCellRenderer#getTableCellRendererComponent(javax.swing.JTable,
		 *      java.lang.Object, boolean, boolean, int, int)
		 */
		@Override
		public Component getTableCellRendererComponent(final JTable table, final Object value, final boolean isSelected, final boolean hasFocus, final int row,
				final int column) {
			final Component c = super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
			if (c instanceof JLabel) {
				final JLabel label = (JLabel) c;
				// bg selection color
				if (isSelected) {
					label.setBackground(new Color(200, 200, 255));
				} else {
					// otherwise, alternate bg color
					if (row % 2 == 0) {
						label.setBackground(new Color(245, 245, 245));
					} else {
						label.setBackground(new Color(254, 254, 254));
					}
				}
				final Column col = _indexToColumn.get(column);
				if (col == Column.COUNTRY_FLAG) {
					label.setText("");
					label.setIcon((ImageIcon) value);
				} else if (col == Column.WHO_IS) {
					final JButton button = new JButton("?");
					button.setMargin(new Insets(0, 0, 0, 0));
					if (Env.INSTANCE.getOs() == OS.win) {
						button.setBorder(null);
					}
					final RoutePoint point = _route.getRoute().get(row);
					button.setToolTipText(Column.WHO_IS.getLabel());
					button.setPreferredSize(new Dimension(Column.WHO_IS.getWidth(), c.getHeight()));
					button.setEnabled(!_searching && point != null && !point.isUnknown());
					return button;
				} else {
					if ((col == Column.LATENCY || col == Column.DNS_LOOKUP) && value.equals(0l)) {
						if (!_dnsLookup && col == Column.DNS_LOOKUP) {
							label.setText("");
						} else {
							label.setText("<1");
						}
					}
				}
				label.setToolTipText(label.getText());
			}
			c.setFont(Env.INSTANCE.getFont());
			return c;
		}
	}

	/**
	 * ButtonCellEditor
	 *
	 * @author Leo Lewis
	 */
	public class ButtonCellEditor extends DefaultCellEditor {

		/**  */
		private static final long serialVersionUID = 5338833733700590170L;

		public ButtonCellEditor() {
			super(new JCheckBox());

		}

		@Override
		public Component getTableCellEditorComponent(final JTable table, final Object value, final boolean isSelected, final int row, final int column) {
			final Component c = super.getTableCellEditorComponent(table, value, isSelected, row, column);
			final JButton button = new JButton("?");
			button.setMargin(new Insets(0, 0, 0, 0));
			button.setToolTipText(Column.WHO_IS.getLabel());
			button.setPreferredSize(new Dimension(Column.WHO_IS.getWidth(), c.getHeight()));
			button.setMaximumSize(button.getPreferredSize());
			if (Env.INSTANCE.getOs() == OS.win) {
				button.setBorder(null);
			}
			button.setEnabled(!_searching);
			button.addActionListener(e -> {
				final RoutePoint point = _route.getRoute().get(_table.convertRowIndexToModel(row));
				WhoIsPanel.showWhoIsDialog(RouteTablePanel.this, _services, point);
				if (table.isEditing()) {
					table.getCellEditor().stopCellEditing();
				}
				_whois.clear();
			});
			return button;
		}

	}
}