/*
 * Copyright 2008-2019 by Emeric Vernat
 *
 *     This file is part of Java Melody.
 *
 * 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 net.bull.javamelody.swing.table; // NOPMD

import java.awt.Component;
import java.awt.Container;
import java.awt.Event;
import java.awt.Point;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;

import javax.swing.ImageIcon;
import javax.swing.JCheckBox;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.JViewport;
import javax.swing.ListSelectionModel;
import javax.swing.ScrollPaneConstants;
import javax.swing.table.JTableHeader;
import javax.swing.table.TableCellRenderer;
import javax.swing.table.TableColumn;
import javax.swing.table.TableColumnModel;
import javax.swing.table.TableModel;
import javax.swing.text.JTextComponent;

/**
 * Composant Table basique servant de base à MListTable.
 *
 * @author Emeric Vernat
 */
public class MBasicTable extends JTable {
	private static final int ADJUST_COLUMN_WIDTHS_MAX_ROWS = 50;

	private static final long serialVersionUID = 1L;

	// Singleton statique pour renderers par défaut des cellules.
	@SuppressWarnings("all")
	private static final Map<Class<?>, TableCellRenderer> DEFAULT_RENDERERS = new HashMap<>(25);

	@SuppressWarnings("all")
	private static final KeyHandler KEY_HANDLER = new KeyHandler();

	/**
	 * KeyAdapter.
	 *
	 * @author Emeric Vernat
	 */
	private static class KeyHandler extends KeyAdapter {
		/**
		 * Constructeur.
		 */
		KeyHandler() {
			super();
		}

		@Override
		public void keyPressed(final KeyEvent event) {
			if (event.getSource() instanceof MBasicTable) {
				((MBasicTable) event.getSource()).keyPressed(event);
			}
		}
	}

	private static class TableHeader extends JTableHeader {
		private static final long serialVersionUID = 1L;

		TableHeader(TableColumnModel columnModel) {
			super(columnModel);
		}

		@Override
		public String getToolTipText(MouseEvent e) {
			final Point p = e.getPoint();
			final int index = columnModel.getColumnIndexAtX(p.x);
			return String.valueOf(columnModel.getColumn(index).getHeaderValue());
		}
	}

	/**
	 * Constructeur.
	 *
	 * @param dataModel
	 *           Modèle pour les données (par exemple, MTableModel)
	 */
	public MBasicTable(final TableModel dataModel) {
		super(dataModel);
		setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
		// setAutoCreateColumnsFromModel(false);
		// par défaut, on laisse AUTO_RESIZE_SUBSEQUENT_COLUMNS
		// setAutoResizeMode(AUTO_RESIZE_OFF);
		// setDragEnabled(true);
		addKeyListener(KEY_HANDLER);
	}

	/**
	 * Adapte les largeurs des colonnes selon les données de cette table. <br/>
	 * Pour chaque colonne la taille préférée est déterminée selon la valeur (et le renderer) du header et selon la valeur (et le renderer) de chaque cellule.
	 */
	public void adjustColumnWidths() {
		if (ADJUST_COLUMN_WIDTHS_MAX_ROWS > 0) {
			TableColumn tableColumn;
			TableCellRenderer renderer;
			Object value;
			Component component;
			final int columnCount = getColumnCount();
			final int rowCount = Math.min(getRowCount(), ADJUST_COLUMN_WIDTHS_MAX_ROWS);
			int columnWidth;
			final int maxWidth = 250; // taille ajustée maximum (15 minimum par défaut)

			// Boucle sur chaque colonne et chaque ligne.
			// Trouve le max de la largeur du header et de chaque cellule
			// et fixe la largeur de la colonne en fonction.
			for (int colNum = 0; colNum < columnCount; colNum++) {
				tableColumn = columnModel.getColumn(colNum);
				if (tableColumn.getMaxWidth() <= 0) {
					continue; // colonne invisible
				}

				renderer = getTableHeader().getDefaultRenderer();
				value = tableColumn.getHeaderValue();
				component = renderer.getTableCellRendererComponent(this, value, false, false, -1,
						colNum);
				columnWidth = component.getPreferredSize().width + 10;
				renderer = getCellRenderer(-1, colNum);

				for (int rowNum = 0; rowNum < rowCount; rowNum++) {
					value = getValueAt(rowNum, colNum);
					component = renderer.getTableCellRendererComponent(this, value, false, false,
							rowNum, colNum);
					columnWidth = Math.max(columnWidth, component.getPreferredSize().width);
				}
				columnWidth = Math.min(maxWidth, columnWidth);

				tableColumn.setPreferredWidth(columnWidth + getIntercellSpacing().width);
			}
		}
	}

	@Override
	protected void configureEnclosingScrollPane() {
		// Si cette table est la viewportView d'un JScrollPane (la situation habituelle),
		// configure ce ScrollPane en positionnant la barre verticale à "always"
		// (et en installant le tableHeader comme columnHeaderView).
		super.configureEnclosingScrollPane();

		final Container parent = getParent();
		if (parent instanceof JViewport && parent.getParent() instanceof JScrollPane) {
			final JScrollPane scrollPane = (JScrollPane) parent.getParent();
			if (scrollPane.getVerticalScrollBar() != null) {
				scrollPane.getVerticalScrollBar().setFocusable(false);
			}
			if (scrollPane.getHorizontalScrollBar() != null) {
				scrollPane.getHorizontalScrollBar().setFocusable(false);
			}

			final JViewport viewport = scrollPane.getViewport();
			if (viewport == null || viewport.getView() != this) {
				return;
			}

			scrollPane.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS);
		}
	}

	@SuppressWarnings("unchecked")
	@Override
	protected void createDefaultRenderers() {
		final Map<Class<?>, TableCellRenderer> map = getDefaultTableCellRenderers();
		super.defaultRenderersByColumnClass = new Hashtable<Class<?>, TableCellRenderer>(
				map.size());
		super.defaultRenderersByColumnClass.putAll(map);
	}

	@Override
	protected JTableHeader createDefaultTableHeader() {
		return new TableHeader(columnModel);
	}

	/**
	 * Retourne la map par défaut des renderers de cellules avec pour clé le type des valeurs.
	 *
	 * @return Map
	 */
	private Map<Class<?>, TableCellRenderer> getDefaultTableCellRenderers() {
		if (DEFAULT_RENDERERS.isEmpty()) {
			DEFAULT_RENDERERS.put(Boolean.class, new MBooleanTableCellRenderer());
			DEFAULT_RENDERERS.put(Double.class, new MDoubleTableCellRenderer());
			DEFAULT_RENDERERS.put(Float.class, new MDoubleTableCellRenderer());
			DEFAULT_RENDERERS.put(java.math.BigDecimal.class, new MDoubleTableCellRenderer());
			DEFAULT_RENDERERS.put(Integer.class, new MIntegerTableCellRenderer());
			DEFAULT_RENDERERS.put(Long.class, new MIntegerTableCellRenderer());
			DEFAULT_RENDERERS.put(java.math.BigInteger.class, new MIntegerTableCellRenderer());
			DEFAULT_RENDERERS.put(GregorianCalendar.class, new MDateTableCellRenderer());
			DEFAULT_RENDERERS.put(Date.class, new MDateTableCellRenderer());
			DEFAULT_RENDERERS.put(java.sql.Date.class, new MDateTableCellRenderer());
			DEFAULT_RENDERERS.put(java.sql.Timestamp.class, new MDateTableCellRenderer());
			DEFAULT_RENDERERS.put(ArrayList.class, new MListTableCellRenderer()); // NOPMD
			DEFAULT_RENDERERS.put(HashMap.class, new MListTableCellRenderer()); // NOPMD
			DEFAULT_RENDERERS.put(Hashtable.class, new MListTableCellRenderer()); // NOPMD
			DEFAULT_RENDERERS.put(Object.class, new MDefaultTableCellRenderer());
			DEFAULT_RENDERERS.put(String.class, new MDefaultTableCellRenderer());
			DEFAULT_RENDERERS.put(ImageIcon.class, new MImageIconTableCellRenderer());
			DEFAULT_RENDERERS.put(Component.class, new MComponentTableCellRenderer());
		}
		return DEFAULT_RENDERERS;
	}

	/** {@inheritDoc} */
	@Override
	public TableCellRenderer getDefaultRenderer(final Class<?> columnClass) {
		// si c'est une instance de Collection on prend le renderer de ArrayList
		// (car par ex., le résultat de Collections.unmodifiableList est une interface de List
		// sans classe "connue" donc son renderer par défaut n'est pas paramétrable)
		if (columnClass != null && Collection.class.isAssignableFrom(columnClass)) {
			return super.getDefaultRenderer(ArrayList.class); // NOPMD
		}
		return super.getDefaultRenderer(columnClass);
	}

	/** {@inheritDoc} */
	@Override
	public String getToolTipText(final MouseEvent event) {
		final int row = rowAtPoint(event.getPoint());
		final int column = columnAtPoint(event.getPoint());
		if (row != -1 && column != -1) {
			String tip = super.getToolTipText(event);
			if (tip == null) {
				tip = getTextAt(row, column);
				if (tip == null || tip.length() == 0) { // NOPMD
					tip = super.getToolTipText();
				}
			}
			return tip;
		}
		return super.getToolTipText();
	}

	/**
	 * Renvoie le texte affiché à la position demandée.
	 *
	 * @return String
	 * @param row
	 *           int
	 * @param column
	 *           int
	 */
	public String getTextAt(final int row, final int column) {
		final Object value = getValueAt(row, column);

		String text = "";
		if (value != null) {
			final TableCellRenderer renderer = getCellRenderer(row, column);
			final Component rendererComponent = renderer.getTableCellRendererComponent(this, value,
					false, false, row, column);
			if (rendererComponent instanceof JLabel) {
				text = ((JLabel) rendererComponent).getText();
				text = getToolTipTextIfNoText(text, rendererComponent);
			} else if (rendererComponent instanceof JTextComponent) {
				text = ((JTextComponent) rendererComponent).getText();
				text = getToolTipTextIfNoText(text, rendererComponent);
			} else if (rendererComponent instanceof JCheckBox) {
				text = ((JCheckBox) rendererComponent).isSelected() ? "vrai" : "faux";
			} else {
				text = value.toString();
			}
		}
		return text;
	}

	private String getToolTipTextIfNoText(final String text, final Component rendererComponent) {
		if (text == null || text.length() == 0) {
			String toolTipText = ((JComponent) rendererComponent).getToolTipText();
			if (toolTipText == null) {
				toolTipText = "";
			}
			return toolTipText;
		}
		return text;
	}

	/**
	 * Gestion des événements clavier sur cette table.
	 *
	 * @param event
	 *           KeyEvent
	 */
	protected void keyPressed(final KeyEvent event) {
		final int keyCode = event.getKeyCode();
		final int modifiers = event.getModifiers();
		if ((modifiers & Event.CTRL_MASK) != 0 && keyCode == KeyEvent.VK_ADD) {
			adjustColumnWidths();
		}
		// else if (modifiers == 0)
		// {
		// final int selectedColumn = getSelectedColumn() != -1 ? getSelectedColumn() : 0;
		// final int selectedRow = getSelectedRow() != -1 ? getSelectedRow() : 0;
		// final int rowCount = getRowCount();
		// if (isCellEditable(selectedRow, selectedColumn) || rowCount == 0)
		// {
		// return;
		// }
		// final String keyChar = String.valueOf(event.getKeyChar());
		// String text;
		// for (int i = selectedRow + 1; i < rowCount; i++)
		// {
		// text = getTextAt(i, selectedColumn);
		// if (text != null && text.regionMatches(true, 0, keyChar, 0, 1))
		// {
		// setRowSelectionInterval(i, i);
		// setColumnSelectionInterval(selectedColumn, selectedColumn);
		// scrollRectToVisible(getCellRect(i, selectedColumn, true));
		// return;
		// }
		// }
		// for (int i = 0; i <= selectedRow; i++)
		// {
		// text = getTextAt(i, selectedColumn);
		// if (text != null && text.regionMatches(true, 0, keyChar, 0, 1))
		// {
		// setRowSelectionInterval(i, i);
		// setColumnSelectionInterval(selectedColumn, selectedColumn);
		// scrollRectToVisible(getCellRect(i, selectedColumn, true));
		// return;
		// }
		// }
		// }
	}
}