package fi.csc.microarray.client.dataimport.table;

import java.awt.Color;
import java.awt.Component;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Point;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.lang.reflect.InvocationTargetException;

import javax.swing.BorderFactory;
import javax.swing.JOptionPane;
import javax.swing.JTable;
import javax.swing.SwingConstants;
import javax.swing.SwingUtilities;
import javax.swing.ToolTipManager;
import javax.swing.UIManager;
import javax.swing.border.Border;
import javax.swing.table.AbstractTableModel;
import javax.swing.table.DefaultTableCellRenderer;
import javax.swing.table.TableColumn;

import org.apache.log4j.Logger;
import org.jdesktop.swingx.JXTable;
import org.jdesktop.swingx.table.DefaultTableColumnModelExt;
import org.jdesktop.swingx.table.TableColumnModelExt;

import fi.csc.microarray.client.dataimport.ColumnType;
import fi.csc.microarray.client.dataimport.ColumnTypeManager;
import fi.csc.microarray.client.dataimport.ConversionModel;
import fi.csc.microarray.client.dataimport.ImportScreen;
import fi.csc.microarray.client.dataimport.events.ColumnTitlesChangedEvent;
import fi.csc.microarray.client.dataimport.events.ConversionModelChangeListener;
import fi.csc.microarray.client.dataimport.events.DecimalSeparatorChangedEvent;
import fi.csc.microarray.client.dataimport.events.DelimiterChangedEvent;
import fi.csc.microarray.client.dataimport.events.FooterChangedEvent;
import fi.csc.microarray.client.dataimport.events.HeaderChangedEvent;
import fi.csc.microarray.client.dataimport.events.InputFileChangedEvent;
import fi.csc.microarray.client.dataimport.events.TitleRowChangedEvent;

/**
 * Table to show imported data and color the cells depending on their 
 * status. The cell data can be header, footer, title or actual data.
 * Table also listens for mouse presses for marking column types.
 * 
 * @author mkoski
 *
 */
public class ImportPreviewTable extends JXTable implements MouseMotionListener, MouseListener, 
		ConversionModelChangeListener{
	

	/**
	 * Logger for this class
	 */
	private static final Logger logger = Logger.getLogger(ImportPreviewTable.class);
	private static final int COLUMN_WIDTH = 120;
	
	/**
	 * Table model for imported data. 
	 * @author mkoski
	 *
	 */
	class ImportPreviewTableModel extends AbstractTableModel {

		private String[] columnTitles;
		private Object[][] arrayData;

		public ImportPreviewTableModel(Object[][] arrayData, String[] columnTitles) {
			this.arrayData = arrayData;
			this.columnTitles = columnTitles;
		}

		public int getRowCount() {
			return arrayData.length;
		}

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

		public String getColumnName(int columnIndex) {
			return columnTitles[columnIndex];
		}

		public Class<?> getColumnClass(int columnIndex) {
			Object firstValue = this.getValueAt(0, columnIndex);
			if (firstValue != null) {
				return firstValue.getClass();
			} else {
				return null;
			}
		}

		/**
		 * Gets value at given row and column. The column count may vary because 
		 * of the headers and footers. If value in given row and column is not set 
		 * the method returns empty string. Otherwise the value of cell is returned
		 * 
		 * @param int row
		 * @param int col
		 * @return value of the cell or empty string if there is no value set
		 */
		public Object getValueAt(int row, int col) {
			if (row >= 0 && row < arrayData.length &&
					col >= 0 && col < arrayData[row].length) {
				return arrayData[row][col];
			} else {
				return "";
			}
		}
	}

	class RowNumberRenderer extends DefaultTableCellRenderer{

		@Override
		public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
			super.getTableCellRendererComponent(table, value, isSelected, hasFocus,
					row, column);

			// This removes the borders when a cell is focused
			if (hasFocus) {
				Border border = BorderFactory.createEmptyBorder(1,1,1,1);
				setBorder(border);
			}

			this.setBackground(UIManager.getColor("TableHeader.background"));
			this.setHorizontalAlignment(SwingConstants.CENTER);

			return this;
		}
	}


	/**
	 * Renderer which adds posibility to change the color of 
	 * rows which has been marked to header or footer
	 * 
	 * @author mkoski
	 *
	 */
	class ImportCellRenderer extends DefaultTableCellRenderer{

		private int row;
		private int column;
		
		@Override
		public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
			super.getTableCellRendererComponent(table, value, isSelected, hasFocus,
					row, column);
			this.row = row;
			this.column = column;
			
			// This removes the borders when a cell is focused
			if (hasFocus) {
				Border border = BorderFactory.createEmptyBorder(1,1,1,1);
				setBorder(border);
			}

			// Highlights
			if(highlightRow && row == mouseOverRow){
				setBackground(new Color(250,250,250));
			} else if(highlightColumn && column == mouseOverColumn){
				setBackground(new Color(250,250,250));
			} else {
				setBackground(UIManager.getColor("Table.background"));
			}

			return this;
		}

		@Override
		protected void paintComponent(Graphics g) {
			
			// Header and footer coloring
			if(screen.getCurrentStep() == ImportScreen.Step.FIRST){
				//Default
				this.setForeground(UIManager.getColor("Table.foreground"));
				
				if (header >= row) {
					this.setForeground(Color.LIGHT_GRAY);
				} 
				
				if(footer <= row){
					this.setForeground(Color.LIGHT_GRAY);
				} 
				
				if(titles == row){
					// This is the column title row
					this.setBackground(Color.LIGHT_GRAY);
					this.setForeground(UIManager.getColor("Table.foreground"));
				} 				
			}
			
			// Do data trimming
			this.setText(screen.getDataTrimmer().doTrimming(this.getText(), column));
			
			// Column coloring on the second step
			if(screen.getCurrentStep() == ImportScreen.Step.SECOND &&	
					columnTypeManager.getColumnCount() > column &&
					columnTypeManager.getColumnType(column) != null 
					&& columnTypeManager.getColumnType(column).getColor() != null){
				
				// Default
				this.setForeground(UIManager.getColor("Table.foreground"));
					
				this.setBackground(columnTypeManager.getColumnType(column).getColor());
				
			}
			
			super.paintComponent(g);
		}
	}
	private static final String HEADER_TOOLTIP = "Click to the row where the header ends";
	private static final String FOOTER_TOOLTIP = "Click to the row where the footer starts";
	private static final String TITLE_TOOLTIP = "Click to the column title row";
	private static final String NO_TOOL_DIALOG_TITLE = "Select a marking tool before clicking the table";
	private static final String NO_TOOL_DIALOG_TEXT = "No tool selected";
	private ConversionModel conversionModel;
	private ImportScreen screen;
	private TableInternalFrame tableFrame;
	private int header;
	private int footer;
	private boolean highlightRow;
	private boolean highlightColumn;
	private int mouseOverRow;
	private int mouseOverColumn;
	private int titles;
	private ColumnTypeManager columnTypeManager;
	private int defaultInitialDelay;
	private int defaultDismissDelay;
	private Point lastDragPoint;

	public ImportPreviewTable(ImportScreen importScreen, TableInternalFrame tableFrame){
		super();
		this.screen = importScreen;
		this.conversionModel = importScreen.getConversionModel();
		this.tableFrame = tableFrame;
		
		this.columnTypeManager = screen.getColumnTypeManager();
		this.setAutoResizeMode(JXTable.AUTO_RESIZE_OFF);
		this.setSortable(false);
		this.setTableHeader(new EditableHeader(this.getColumnModel()));
		this.getTableHeader().setReorderingAllowed(false);
		this.getTableHeader().setEnabled(false);
		
		this.defaultInitialDelay = ToolTipManager.sharedInstance().getInitialDelay();
		this.defaultDismissDelay = ToolTipManager.sharedInstance().getDismissDelay();
		
		// Renderers for data, headers and footers
		setDefaultRenderer(String.class, new ImportCellRenderer());
		setDefaultRenderer(Double.class, new ImportCellRenderer());		

		// Renderer for row numbers
		setDefaultRenderer(Integer.class, new RowNumberRenderer());		

		// Mouse listeners
		this.addMouseListener(this);
		this.addMouseMotionListener(this);
	}
	
	public void createHeaderRenderers(){
		logger.debug("Check the header class of.."+this.getTableHeader()+"..");
		if(!(this.getTableHeader() instanceof EditableHeader)){
			logger.debug("..not editable header, will put a new one");
			this.setTableHeader(new EditableHeader(this.getColumnModel()));
		}
		
		for(int i = 1; i < this.getColumnCount(); i++){
			
			if(!(this.getColumnModel().getColumn(i).getHeaderRenderer() instanceof
					PanelTableHeaderRenderer)){
				
				PanelTableHeaderRenderer renderer = new PanelTableHeaderRenderer(screen,i);

				this.getColumnModel().getColumn(i).setHeaderRenderer(
						renderer);
				
				if(!(this.getColumnModel().getColumn(i) instanceof EditableHeaderTableColumn)){
					((EditableHeader)this.getTableHeader()).recreateTableColumn(columnModel);					
				}

				((EditableHeaderTableColumn)this.getColumnModel().getColumn(i)).
				setHeaderEditor(new PanelTableHeaderEditor(renderer));
				
				this.getColumnModel().getColumn(i).setMinWidth(COLUMN_WIDTH);
			}
			
		}		
		
		screen.getTableFrame().updateAllChipCountComboBoxes();
		screen.getTableFrame().updateAllColumnTitleLabels();	
		this.updateTableHeader();
	}
	
	public PanelTableHeaderRenderer getHeaderRenderer(int columnIndex){
		return (PanelTableHeaderRenderer)this.getColumnModel().getColumn(columnIndex).getHeaderRenderer();
	}

	/**
	 * Sets data to table. This method updates swing components and it made thread safe.
	 * 
	 * @param arrayData
	 * @param columnTitles
	 */
	public void setData(Object[][] arrayData, String[] columnTitles) {
		
		/**
		 * Runnable helper class to set data to table. This is done 
		 * in it's own runnable class because the data must be set 
		 * before column headers can be created. That's why the 
		 * SwingUtilities.invokeAndWait is used because it 
		 * blocks the application until the <code>setModel</code> 
		 * is done.
		 * 
		 * @author mkoski
		 *
		 */
		class SetModelRunnable implements Runnable{

			private Object[][] arrayData;
			private String[] columnTitles;
			
			public SetModelRunnable(Object[][] arrayData, String[] columnTitles) {
				this.arrayData = arrayData;
				this.columnTitles = columnTitles;
			}
			
			public void run() {
				ImportPreviewTable.this.setModel(new ImportPreviewTableModel(arrayData, columnTitles));
				if(screen.getCurrentStep() == ImportScreen.Step.FIRST){
					screen.getTableFrame().addCornerComponents();
				}
			}
			
		}
		
		/**
		 * Makes the rest of table initializing
		 * 
		 * @author mkoski
		 *
		 */
		class InitializeTableRunnable implements Runnable{
			
			public void run(){
				// Take custom renderers in use because previous line sets default 
				// renderers (null), but only for second step 
				if(screen.getCurrentStep() == ImportScreen.Step.SECOND){
					screen.getColumnTypeManager().setColumnCount(getColumnCount());
					createHeaderRenderers();
				}
				
				// Initialize the table's and spinner's footer and header values if needed
				if(screen.getConversionModel().hasColumnTitles() 
						|| screen.getConversionModel().hasFooter()
						|| screen.getConversionModel().hasHeader()){
					// Footer, header or title already set. 
					// Do not initialize the values
				} else {
					initializeHeaderAndFooter();
					tableFrame.resetSpinners();
				}

				// Sets row number column width
				updateRowNumberColumnWidth();
			}
		}
		
		// Set data to table
		try {
			SwingUtilities.invokeAndWait(new SetModelRunnable(arrayData, columnTitles));
		} 
		
		catch (InterruptedException e) { } 
		catch (InvocationTargetException e) { }
		
		// Do other initializations
		SwingUtilities.invokeLater(new InitializeTableRunnable());
	}

	private void updateRowNumberColumnWidth() {	
		if(this.getRowCount()>0 && this.getColumnCount()>0){
			String str = this.getValueAt(this.getRowCount()-1, 0).toString();
			FontMetrics fm = this.getFontMetrics(this.getFont());

			// The number does not fit without a bit extra space (+8)
			int width = SwingUtilities.computeStringWidth(fm, str) + 8;

			this.getColumnModel().getColumn(0).setMaxWidth(width);
			this.getColumnModel().getColumn(0).setMinWidth(width);
			logger.debug("Set row number column width to " + width);
		}
	}

	/**
	 * Updates the cell highlighting according to mouse point
	 * 
	 * @param e Mouse event
	 */
	private void updateHighlight(MouseEvent e){

		int oldMouseOverRow = mouseOverRow;
		int oldMouseOverColumn = mouseOverColumn;

		mouseOverRow = this.rowAtPoint(e.getPoint());
		mouseOverColumn = this.columnAtPoint(e.getPoint());

		// If mouse is off the component, set both to -1 when no highlight
		// is painted
		if(mouseOverRow < 0 || mouseOverColumn < 0){
			mouseOverRow = -1;
			mouseOverColumn = -1;
		}

		// Repainting the table is important to update the highlighted row or column
		// The repainting is a slow operation, so it shouldn't be done everytime 
		// the mouse moves
		if((highlightRow && oldMouseOverRow != mouseOverRow)
				|| (highlightColumn && oldMouseOverColumn != mouseOverColumn)){
			repaint();
		}
	}
	
	/**
	 * Updates table header. Gets the header value from table if cell header row 
	 * is selected. Otherwise the header value is just column numbers.
	 *
	 */
	private void updateTableHeader(){
		TableColumnModelExt model = new DefaultTableColumnModelExt();
		Object[] columnTitles = conversionModel.getColumnTitles();
		
		for(int column = 0; column < columnTitles.length; column++){
			
			if(column >= this.getColumnCount()){
				/*
				 * The TitleRowChangeEvent is fired after data chopping is done. 
				 * This means that this method may be called before the table is updated. 
				 * In this case, if the column count is changed, ArrayIndexOutOfBounds exception 
				 * may occure.
				 *  
				 * So, let's break the loop and wait that this method is called by updateTable 
				 * method a bit later.
				 */
				break;
			}
			TableColumn newColumn = this.getColumnModel().getColumn(column);
			
			if(column >= 1){ 
				
				//Now text to the upper left corner cell
				if(newColumn.getHeaderRenderer() instanceof PanelTableHeaderRenderer){
					//For custom header of second step
					((PanelTableHeaderRenderer)newColumn.getHeaderRenderer()).
					setTitleText(columnTitles[column].toString());
				} else {
					//For step 1				
					newColumn.setHeaderValue(columnTitles[column]);
				}
			}
			
			model.addColumn(newColumn);
		} 
		this.setColumnModel(model);
		
		logger.debug("Table header updated");
	}

	public void setColumnHighlight(boolean enabled){
		highlightColumn = enabled;
		repaint();
	}

	public void setRowHighlight(boolean enabled){
		highlightRow = enabled;
		repaint();
	}

	public void mouseMoved(MouseEvent e) {
		updateHighlight(e);
	}

	public void mouseClicked(MouseEvent e) {
		// Do nothing		
	}

	public void mouseEntered(MouseEvent e) {
		this.setTooltipDelays();
	}

	public void mouseExited(MouseEvent e) {
		updateHighlight(e);
		this.resetTooltipDelays();
	}

	public void mouseDragged(MouseEvent e) {
		if(e.getSource() == this && screen.getCurrentStep() == ImportScreen.Step.FIRST){
			if(tableFrame.isInHeaderMarkingMode()){
				markHeaderAtPoint(e.getPoint());
			} else if(tableFrame.isInFooterMarkingMode()){
				markFooterAtPoint(e.getPoint());
			} else if(tableFrame.isInTitleMarkingMode()){
				markTitleAtPoint(e.getPoint(), false);
			}
		} else {
			markColumnsBetweenPoints(lastDragPoint, e.getPoint());
			if(this.columnAtPoint(e.getPoint()) == -1){
				// User dragged mouse out of the table. Do not set new last point
				return;
			} else {
				lastDragPoint = e.getPoint();
			}
		}
		updateHighlight(e);
	}

	/**
	 * Marks column between two points. This is useful when marking columns 
	 * using mouse drag
	 * 
	 * @param start start point
	 * @param end end point
	 */
	private void markColumnsBetweenPoints(Point start, Point end) {
		int startColumn = this.columnAtPoint(start);
		int endColumn = this.columnAtPoint(end);
		
		if(endColumn < 0){
			// endColumn can get value -1 if user drags mouse out of the table
			endColumn = this.getColumnCount() -2;
		}
		
		int bigger;
		int smaller;
		
		if(startColumn > endColumn){
			bigger = startColumn;
			smaller = endColumn;
		} else {
			bigger = endColumn;
			smaller = startColumn;
		}
		
		for(int column = smaller; column <= bigger; column++){
			markColumn(column);
		}
		
	}

	public void mousePressed(MouseEvent e) {
		// Mark drag start point just in case user starts dragging
		lastDragPoint = e.getPoint();
		
		// Listeners for first step
		if(e.getSource() == this && screen.getCurrentStep() == ImportScreen.Step.FIRST){
			
			// Mark header
			if(tableFrame.isInHeaderMarkingMode()){
				markHeaderAtPoint(e.getPoint());
			} 
			
			// Mark footer
			else if(tableFrame.isInFooterMarkingMode()){
				markFooterAtPoint(e.getPoint());
			} 
			
			// Mark title row
			else if(tableFrame.isInTitleMarkingMode()){
				markTitleAtPoint(e.getPoint(), true);
			} 
			
			else {
				JOptionPane.showMessageDialog(screen.getFrame(), NO_TOOL_DIALOG_TITLE, NO_TOOL_DIALOG_TEXT, JOptionPane.WARNING_MESSAGE);
			}
		} 
		
		// Listeners for second step
		else if(e.getSource() == this && screen.getCurrentStep() == ImportScreen.Step.SECOND){			
			markColumnAtPoint(e.getPoint());
		}
	}
	
	private void markHeaderAtPoint(Point p){
		
		// Set header end row to both table and spinner
		int headerEnd = this.rowAtPoint(p);
		conversionModel.setHeaderEndsRow(headerEnd);
	}
	
	private void markFooterAtPoint(Point p){
		
		// Set footer start row to both table and spinner
		int footerStart = this.rowAtPoint(p);
		conversionModel.setFooterBeginsRow(footerStart);
	}
	
	private void markTitleAtPoint(Point p, boolean removeIfSelected){
		// Sets column title row
		int columnTitleRow = this.rowAtPoint(p);
		
		// Removes title row if the current title row is clicked
		if(titles == columnTitleRow && removeIfSelected){
			conversionModel.setColumnTitleLine(-1);
		} else {
			conversionModel.setColumnTitleLine(columnTitleRow);
		}
		this.repaint();
	}
	
	private void markColumnAtPoint(Point p){
		markColumn(this.columnAtPoint(p));
	}
	
	private void markColumn(int column){
		if(screen.getTableFrame().getSelectedColumnType() != null){
			ColumnType type = screen.getTableFrame().getSelectedColumnType();
			if(column >= 1 && column < screen.getColumnTypeManager().getColumnCount()){					
				
				// Set column type to the column type manager
				screen.getColumnTypeManager().setColumnType(column, type, conversionModel.getCleanColumnTitle(column));
				screen.getColumnTypeManager().setColumnChipNumber(column, screen.getColumnTypeManager().getNextChipNumber(type));
			} else {
				logger.debug("ColumnCount smaller than clicked column index: "+ screen.getColumnTypeManager().getColumnCount());
			}
		}
	}

	public void mouseReleased(MouseEvent e) {
		this.repaint();	
	}

	/**
	 * Sets row number to end the header.
	 * 
	 * @param header Row number to end the header
	 */
	private void setHeaderEndRow(int header){
		this.header = header;
		logger.debug("Header end row set to " + header);
		this.repaint();
	}

	/**
	 * Sets row number to start the footer. Notice that row numbers in the table 
	 * starts from 0, so the value is smaller by one than it is in spinners
	 * 
	 * @param header Row number to start the footer
	 */
	private void setFooterStartRow(int footer){
		if(footer < 0){
			footer = this.getRowCount() +1;
		}
		this.footer = footer;
		logger.debug("Footer start row set to " + footer);
		this.repaint();
	}
	
	private void setColumnTitleRow(int columnTitleRow) {
		this.titles = columnTitleRow;
		this.updateTableHeader();
		this.repaint();
	}

	/**
	 * Resets the header, footer and title rows
	 *
	 */
	public void initializeHeaderAndFooter(){
		// Reset value of this table
		this.header = -1;
		this.footer = this.getRowCount();
		this.titles = -1;
	}
		
	public void decimalSeparatorChanged(DecimalSeparatorChangedEvent e) {
		// Do nothing
	}

	public void delimiterChanged(DelimiterChangedEvent e) {
		// Do nothing
	}

	public void footerChanged(FooterChangedEvent e) {
		this.setFooterStartRow(e.getNewValue());
	}

	public void headerChanged(HeaderChangedEvent e) {
		this.setHeaderEndRow(e.getNewValue());
	}

	public void titleRowChanged(TitleRowChangedEvent e) {
		this.setColumnTitleRow(e.getNewValue());
	}
	
	/**
	 * Makes tooltip to follow the mouse pointer
	 */
	@Override
	public Point getToolTipLocation(MouseEvent event) {
		Point tooltipPoint = new Point(event.getX() + 12, event.getY() + 14);
		return tooltipPoint;
	}
	
	/**
	 * Gets the right tooltip depending on the selected tool and step
	 */
	@Override
	public String getToolTipText() {
		if(screen.getCurrentStep() == ImportScreen.Step.FIRST){
			if(screen.getTableFrame().isInFooterMarkingMode()){
				return FOOTER_TOOLTIP;
			} 
			else if(screen.getTableFrame().isInHeaderMarkingMode()){
				return HEADER_TOOLTIP;
			} 
			else if(screen.getTableFrame().isInTitleMarkingMode()){
				return TITLE_TOOLTIP;
			}
			else {
				return null;
			}
		} else {
			if(screen.getTableFrame().getSelectedColumnType() == null){
				return null;
			} 
			
			else if(screen.getTableFrame().getSelectedColumnType().equals(ColumnType.UNUSED_LABEL)){
				return "Mark as unused";
			}
			
			else {
				return "Select " + screen.getTableFrame().getSelectedColumnType().toString().toLowerCase();
			}
		}
	}
	
	/**
	 * Sets tooltip delays back to default
	 *
	 */
	public void resetTooltipDelays(){
		ToolTipManager.sharedInstance().setInitialDelay(this.defaultInitialDelay);
		ToolTipManager.sharedInstance().setDismissDelay(this.defaultDismissDelay);
	}
	
	/**
	 * Sets tooltips to appear immediately
	 */
	public void setTooltipDelays(){
		ToolTipManager.sharedInstance().setInitialDelay(0);
		ToolTipManager.sharedInstance().setDismissDelay(Integer.MAX_VALUE);
	}

	public void columnTitlesChanged(ColumnTitlesChangedEvent e) {
		// Do nothing
	}

	public void inputFileChanged(InputFileChangedEvent e) {
		// Do nothing
	}
}