/*
* Copyright (c) 2009 Nokia Corporation and/or its subsidiary(-ies).
* All rights reserved.
* This component and the accompanying materials are made available
* under the terms of the License "Eclipse Public License v1.0"
* which accompanies this distribution, and is available
* at the URL "http://www.eclipse.org/legal/epl-v10.html".
*
* Initial Contributors:
* Nokia Corporation - initial contribution.
*
* Contributors:
*
* Description: 
*
*/
package com.nokia.cdt.internal.debug.launch.ui;

import java.io.File;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import org.eclipse.core.runtime.Path;
import org.eclipse.jface.dialogs.IDialogSettings;
import org.eclipse.jface.viewers.CheckStateChangedEvent;
import org.eclipse.jface.viewers.CheckboxTableViewer;
import org.eclipse.jface.viewers.DoubleClickEvent;
import org.eclipse.jface.viewers.ICheckStateListener;
import org.eclipse.jface.viewers.IDoubleClickListener;
import org.eclipse.jface.viewers.ISelectionChangedListener;
import org.eclipse.jface.viewers.IStructuredContentProvider;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.ITableLabelProvider;
import org.eclipse.jface.viewers.LabelProvider;
import org.eclipse.jface.viewers.SelectionChangedEvent;
import org.eclipse.jface.viewers.TableLayout;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.jface.viewers.ViewerSorter;
import org.eclipse.jface.window.Window;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.ControlAdapter;
import org.eclipse.swt.events.ControlEvent;
import org.eclipse.swt.events.KeyAdapter;
import org.eclipse.swt.events.KeyEvent;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.graphics.Font;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Table;
import org.eclipse.swt.widgets.TableColumn;

import com.nokia.cdt.internal.debug.launch.LaunchPlugin;
import com.nokia.cpp.internal.api.utils.core.Pair;

/**
 * A composite that displays files in a table. Files can be 
 * added, removed, and edited..
 */
public class FilesBlock {
	
	FileTransferTab fLaunchTab;
	/**
	 * This block's control
	 */
	private Composite fControl;
	
	/**
	 * Files being displayed
	 */
	private List<FileToTransfer> fFiles = new ArrayList<FileToTransfer>(); 
	
	/**
	 * The main list control
	 */ 
	private CheckboxTableViewer fFileList;
	
	// Action buttons
	private Button fAddButton;
	private Button fRemoveButton;
	private Button fEditButton;
	private Button fSelectAllButton;
	private Button fDeSelectAllButton;
	
	// column weights
	private float fWeight1 = .1F;
	private float fWeight2 = .45F;
	
	// ignore column re-sizing when the table is being resized
	private boolean fResizingTable = false; 
	
	// index of column used for sorting
	private int fSortColumn = 1;
	
	// cache of file info: we expect this to remain relatively static while
	// any UI using FilesBlock is up.
	private static Map<File, Pair<Long, Boolean>> fFileInfoCache = new LinkedHashMap<File, Pair<Long,Boolean>>();
	private static long fFileInfoCacheFlushTime = 0;
	private static final int CACHE_CHECK_QUANTUM = 60 * 1000;

	/** 
	 * Content provider to show a list of files to be transferred
	 */ 
	class FilesContentProvider implements IStructuredContentProvider {	
	
		public Object[] getElements(Object input) {
			return fFiles.toArray();
		}

		public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
		}

		public void dispose() {
		}
	
	}
	
	/**
	 * Label provider for files to transfer table.
	 */
	class FilesLabelProvider extends LabelProvider implements ITableLabelProvider {

		/**
		 * @see ITableLabelProvider#getColumnText(Object, int)
		 */
		public String getColumnText(Object element, int columnIndex) {
			if (element instanceof FileToTransfer) {
				FileToTransfer file = (FileToTransfer)element;
				switch(columnIndex) {
					case 0:
						return ""; //$NON-NLS-1$
					case 1:
						return file.getHostPath();
					case 2: 
						return file.getTargetPath();						
				}
			}
			return element.toString();
		}

		/**
		 * @see ITableLabelProvider#getColumnImage(Object, int)
		 */
		public Image getColumnImage(Object element, int columnIndex) {
			if (columnIndex == 1 && element instanceof FileToTransfer) {
				// add warning icon for any host files that don't exist
				FileToTransfer file = (FileToTransfer)element;
				File hostFile = new Path(file.getHostPath()).toFile();
				if (!fileExists(hostFile)) {
					return LaunchPlugin.getImageDescriptor("icons/Launch/etool16/warning_obj.gif").createImage();
				}
			}
			return null;
		}

	}
	
	FilesBlock(FileTransferTab launchTab) {
		fLaunchTab = launchTab;
		fFileInfoCacheFlushTime = System.currentTimeMillis();
	}

	/**
	 * Creates this block's control in the given control.
	 * 
	 * @param ancestor containing control
	 */
	public void createControl(Composite ancestor) {
		
		Composite parent= new Composite(ancestor, SWT.NULL);
		GridLayout layout= new GridLayout();
		layout.numColumns= 2;
		layout.marginHeight = 0;
		layout.marginWidth = 0;
		parent.setLayout(layout);
		Font font = ancestor.getFont();
		parent.setFont(font);	
		fControl = parent;	
		
		GridData data;

		Table table= new Table(parent, SWT.CHECK | SWT.BORDER | SWT.MULTI | SWT.FULL_SELECTION);
		data= new GridData(GridData.FILL_BOTH);
		table.setLayoutData(data);
		table.setFont(font);
				
		table.setHeaderVisible(true);
		table.setLinesVisible(true);		

		TableLayout tableLayout= new TableLayout();
		table.setLayout(tableLayout);

		TableColumn column1= new TableColumn(table, SWT.NULL);
		column1.setText(Messages.getString("FileTransferTab.2")); //$NON-NLS-1$
		column1.addSelectionListener(new SelectionAdapter() {
			public void widgetSelected(SelectionEvent e) {
				sortByEnabled();
			}
		});
	
		TableColumn column2= new TableColumn(table, SWT.NULL);
		column2.setText(Messages.getString("FileTransferTab.3")); //$NON-NLS-1$
		column2.addSelectionListener(new SelectionAdapter() {
			public void widgetSelected(SelectionEvent e) {
				sortByHostPath();
			}
		});
		
		TableColumn column3= new TableColumn(table, SWT.NULL);
		column3.setText(Messages.getString("FileTransferTab.4")); //$NON-NLS-1$
		column3.addSelectionListener(new SelectionAdapter() {
			public void widgetSelected(SelectionEvent e) {
				sortByTargetPath();
			}
		});
		
		fFileList= new CheckboxTableViewer(table);			
		fFileList.setLabelProvider(new FilesLabelProvider());
		fFileList.setContentProvider(new FilesContentProvider());

		fFileList.addSelectionChangedListener(new ISelectionChangedListener() {
			public void selectionChanged(SelectionChangedEvent evt) {
				enableButtons();
			}
		});
		
		fFileList.addDoubleClickListener(new IDoubleClickListener() {
			public void doubleClick(DoubleClickEvent e) {
				if (!fFileList.getSelection().isEmpty()) {
					editFile();
				}
			}
		});
		
		fFileList.addCheckStateListener(new ICheckStateListener() {
			public void checkStateChanged(CheckStateChangedEvent e) {
				FileToTransfer file = (FileToTransfer)e.getElement();
				file.setEnabled(e.getChecked());
				fLaunchTab.dataChanged();
			}
		});
		
		table.addKeyListener(new KeyAdapter() {
			public void keyPressed(KeyEvent event) {
				if (event.character == SWT.DEL && event.stateMask == 0) {
					removeFiles();
				}
			}
		});	
		
		Composite buttons= new Composite(parent, SWT.NULL);
		buttons.setLayoutData(new GridData(GridData.VERTICAL_ALIGN_BEGINNING));
		layout= new GridLayout();
		layout.marginHeight= 0;
		layout.marginWidth= 0;
		buttons.setLayout(layout);
		buttons.setFont(font);
		
		fAddButton = createPushButton(buttons, Messages.getString("FileTransferTab.5")); //$NON-NLS-1$
		fAddButton.addListener(SWT.Selection, new Listener() {
			public void handleEvent(Event evt) {
				addFile();
			}
		});
		
		fEditButton= createPushButton(buttons, Messages.getString("FileTransferTab.6")); //$NON-NLS-1$
		fEditButton.addListener(SWT.Selection, new Listener() {
			public void handleEvent(Event evt) {
				editFile();
			}
		});
		
		fRemoveButton= createPushButton(buttons, Messages.getString("FileTransferTab.7")); //$NON-NLS-1$
		fRemoveButton.addListener(SWT.Selection, new Listener() {
			public void handleEvent(Event evt) {
				removeFiles();
			}
		});

		fSelectAllButton= createPushButton(buttons, Messages.getString("FileTransferTab.10")); //$NON-NLS-1$
		fSelectAllButton.addListener(SWT.Selection, new Listener() {
			public void handleEvent(Event evt) {
				for (int i = 0; i < fFiles.size(); i++) {
					FileToTransfer file = (FileToTransfer)fFiles.get(i);
					file.setEnabled(true);
					fFileList.setChecked(file, true);
				}
				fLaunchTab.dataChanged();
			}
		});

		fDeSelectAllButton= createPushButton(buttons, Messages.getString("FileTransferTab.11")); //$NON-NLS-1$
		fDeSelectAllButton.addListener(SWT.Selection, new Listener() {
			public void handleEvent(Event evt) {
				for (int i = 0; i < fFiles.size(); i++) {
					FileToTransfer file = (FileToTransfer)fFiles.get(i);
					file.setEnabled(false);
					fFileList.setChecked(file, false);
				}
				fLaunchTab.dataChanged();
			}
		});

		configureTableResizing(parent, buttons, table, column1, column2, column3);
		
		enableButtons();
		fAddButton.setEnabled(true);
	}
	
	/**
	 * Sorts by enabled.
	 */
	private void sortByEnabled() {
		fFileList.setSorter(new ViewerSorter() {
			public int compare(Viewer viewer, Object e1, Object e2) {
				if ((e1 instanceof FileToTransfer) && (e2 instanceof FileToTransfer)) {
					FileToTransfer left= (FileToTransfer)e1;
					FileToTransfer right= (FileToTransfer)e2;
					if (left.getEnabled() == right.getEnabled()) {
						return 0;
					} else if (left.getEnabled()) {
						return 1;
					}
					else
						return -1;
				}
				return super.compare(viewer, e1, e2);
			}
			
			public boolean isSorterProperty(Object element, String property) {
				return true;
			}
		});		
		fSortColumn = 1;		
	}
	
	/**
	 * Sorts by host path.
	 */
	private void sortByHostPath() {
		fFileList.setSorter(new ViewerSorter() {
			public int compare(Viewer viewer, Object e1, Object e2) {
				if ((e1 instanceof FileToTransfer) && (e2 instanceof FileToTransfer)) {
					FileToTransfer left= (FileToTransfer)e1;
					FileToTransfer right= (FileToTransfer)e2;
					return left.getHostPath().compareToIgnoreCase(right.getHostPath());
				}
				return super.compare(viewer, e1, e2);
			}
			
			public boolean isSorterProperty(Object element, String property) {
				return true;
			}
		});	
		fSortColumn = 3;			
	}
	
	/**
	 * Sorts by target path.
	 */
	private void sortByTargetPath() {
		fFileList.setSorter(new ViewerSorter() {
			public int compare(Viewer viewer, Object e1, Object e2) {
				if ((e1 instanceof FileToTransfer) && (e2 instanceof FileToTransfer)) {
					FileToTransfer left= (FileToTransfer)e1;
					FileToTransfer right= (FileToTransfer)e2;
					return left.getTargetPath().compareToIgnoreCase(right.getTargetPath());
				}
				return super.compare(viewer, e1, e2);
			}
			
			public boolean isSorterProperty(Object element, String property) {
				return true;
			}
		});		
		fSortColumn = 2;		
	}
		
	private void enableButtons() {
		int selectionCount= ((IStructuredSelection)fFileList.getSelection()).size();
		fEditButton.setEnabled(selectionCount == 1);
		fRemoveButton.setEnabled(selectionCount > 0);
	}	
	
	protected Button createPushButton(Composite parent, String label) {
		Button button = new Button(parent, SWT.PUSH);
		button.setFont(parent.getFont());
		if (label != null) {
			button.setText(label);
		}
		GridData gd = new GridData(GridData.HORIZONTAL_ALIGN_FILL);
		button.setLayoutData(gd);	
		return button;
	}
	
	/**
	 * Correctly resizes the table so no phantom columns appear
	 */
	protected void configureTableResizing(final Composite parent, final Composite buttons, final Table table, final TableColumn column1, final TableColumn column2, final TableColumn column3) {
		parent.addControlListener(new ControlAdapter() {
			public void controlResized(ControlEvent e) {
				resizeTable(parent, buttons, table, column1, column2, column3);
			}
		}); 
		table.addListener(SWT.Paint, new Listener() {
			public void handleEvent(Event event) {
				table.removeListener(SWT.Paint, this);
				resizeTable(parent, buttons, table, column1, column2, column3);
			}
		});
		column1.addControlListener(new ControlAdapter() {
			public void controlResized(ControlEvent e) {
				if (column1.getWidth() > 0 && !fResizingTable) {
					fWeight1 = getColumnWeight(0);
				}
			}
		});
		column2.addControlListener(new ControlAdapter() {
			public void controlResized(ControlEvent e) {
				if (column2.getWidth() > 0 && !fResizingTable) {
					fWeight2 = getColumnWeight(1);
				}
			}
		});
	}	

	private void resizeTable(Composite parent, Composite buttons, Table table, TableColumn column1, TableColumn column2, TableColumn column3) {
		fResizingTable = true;
		int parentWidth = -1;
		int parentHeight = -1;
		if (parent.isVisible()) {
			Rectangle area = parent.getClientArea();
			parentWidth = area.width;
			parentHeight = area.height;
		} else {
			Point parentSize = parent.computeSize(SWT.DEFAULT, SWT.DEFAULT);
			parentWidth = parentSize.x;
			parentHeight = parentSize.y;
		}
		Point preferredSize = table.computeSize(SWT.DEFAULT, SWT.DEFAULT);
		int width = parentWidth - 2 * table.getBorderWidth();
		if (preferredSize.y > parentHeight) {
			// Subtract the scrollbar width from the total column width
			// if a vertical scrollbar will be required
			Point vBarSize = table.getVerticalBar().getSize();
			width -= vBarSize.x;
		}
		width-= buttons.getSize().x;
		Point oldSize = table.getSize();
		if (oldSize.x > width) {
			// table is getting smaller so make the columns
			// smaller first and then resize the table to
			// match the client area width
			column1.setWidth(Math.round(width * fWeight1));
			column2.setWidth(Math.round(width * fWeight2));
			column3.setWidth(width - (column1.getWidth() + column2.getWidth()));
			table.setSize(width, parentHeight);
		} else {
			// table is getting bigger so make the table
			// bigger first and then make the columns wider
			// to match the client area width
			table.setSize(width, parentHeight);
			column1.setWidth(Math.round(width * fWeight1));
			column2.setWidth(Math.round(width * fWeight2));
			column3.setWidth(width - (column1.getWidth() + column2.getWidth()));
		 }
		 fResizingTable = false;		
	}
	
	/**
	 * Returns this block's control
	 * 
	 * @return control
	 */
	public Control getControl() {
		return fControl;
	}
	
	/**
	 * Sets the files to be displayed in this block
	 * 
	 * @param files files to be displayed
	 */
	protected void setFiles(List<FileToTransfer> files) {
		fFiles.clear();
		for (int i = 0; i < files.size(); i++) {
			FileToTransfer file = (FileToTransfer)files.get(i);
			fFiles.add(file);
		}
		fFileList.setInput(fFiles);
		fFileList.refresh();

		for (int i = 0; i < fFiles.size(); i++) {
			FileToTransfer file = (FileToTransfer)fFiles.get(i);
			fFileList.setChecked(file, file.getEnabled());
		}
	}
	
	/**
	 * Returns the files currently being displayed in this block
	 * 
	 * @return files currently being displayed in this block
	 */
	public FileToTransfer[] getFiles() {
		return (FileToTransfer[])fFiles.toArray(new FileToTransfer[fFiles.size()]);
	}
	
	/**
	 * Bring up a dialog that lets the user create a new file to transfer.
	 */
	private void addFile() {
		FileToTransfer file = new FileToTransfer();
		AddEditFileToTransferDialog dialog= new AddEditFileToTransferDialog(getShell(), file);
		dialog.setTitle(Messages.getString("FileTransferTab.8")); //$NON-NLS-1$
		if (dialog.open() != Window.OK) {
			return;
		}
		fFiles.add(file);
		fFileList.refresh();
		fFileList.setChecked(file, file.getEnabled());
		fLaunchTab.dataChanged();
	}
		
	private void editFile() {
		IStructuredSelection selection= (IStructuredSelection)fFileList.getSelection();
		FileToTransfer file= (FileToTransfer)selection.getFirstElement();
		if (file == null) {
			return;
		}
		AddEditFileToTransferDialog dialog= new AddEditFileToTransferDialog(getShell(), file);
		dialog.setTitle(Messages.getString("FileTransferTab.9")); //$NON-NLS-1$
		if (dialog.open() != Window.OK) {
			return;
		}
		fFileList.refresh(file);
		fLaunchTab.dataChanged();
	}
	
	private void removeFiles() {
		IStructuredSelection selection= (IStructuredSelection)fFileList.getSelection();
		FileToTransfer[] files = new FileToTransfer[selection.size()];
		Iterator<?> iter = selection.iterator();
		int i = 0;
		while (iter.hasNext()) {
			files[i] = (FileToTransfer)iter.next();
			i++;
		}
		removeFiles(files);
	}
	
	/**
	 * Removes the given files from the table.
	 * 
	 * @param files
	 */
	public void removeFiles(FileToTransfer[] files) {
		for (int i = 0; i < files.length; i++) {
			fFiles.remove(files[i]);
		}
		fFileList.refresh();
		fLaunchTab.dataChanged();
	}
	
	protected Shell getShell() {
		return getControl().getShell();
	}

	/**
	 * Persist table settings into the give dialog store, prefixed
	 * with the given key.
	 * 
	 * @param settings dialog store
	 * @param qualifier key qualifier
	 */
	public void saveColumnSettings(IDialogSettings settings, String qualifier) {
		for (int i = 0; i < 2; i++) {
			//persist the first 2 column weights
			settings.put(qualifier + ".column" + i, getColumnWeight(i));	 //$NON-NLS-1$
		}
		settings.put(qualifier + ".sortColumn", fSortColumn); //$NON-NLS-1$
	}
	
	private float getColumnWeight(int col) {
		Table table = fFileList.getTable();
		int tableWidth = table.getSize().x;
		int columnWidth= table.getColumn(col).getWidth();
		if (tableWidth > columnWidth) {
			return ((float)columnWidth) / tableWidth;
		}
		return 1/3F;
	}
	
	/**
	 * Restore table settings from the given dialog store using the
	 * given key.
	 * 
	 * @param settings dialog settings store
	 * @param qualifier key to restore settings from
	 */
	public void restoreColumnSettings(IDialogSettings settings, String qualifier) {
		fWeight1 = restoreColumnWeight(settings, qualifier, 0);
		fWeight2 = restoreColumnWeight(settings, qualifier, 1);
		fFileList.getTable().layout(true);
		try {
			fSortColumn = settings.getInt(qualifier + ".sortColumn"); //$NON-NLS-1$
		} catch (NumberFormatException e) {
			fSortColumn = 1;
		}
		switch (fSortColumn) {
			case 1:
				sortByEnabled();
				break;
			case 2:
				sortByHostPath();
				break;
			case 3:
				sortByTargetPath();
				break;
		}
	}
	
	private float restoreColumnWeight(IDialogSettings settings, String qualifier, int col) {		
		try {
			return settings.getFloat(qualifier + ".column" + col); //$NON-NLS-1$
		} catch (NumberFormatException e) {
			return 1/3F;
		}

	}

	Viewer getViewer() {
		return fFileList;
	}
	
	boolean fileExists(File file) {
		Pair<Long, Boolean> entry = fFileInfoCache.get(file);
		
		// recheck the status occasionally, either after time passes or
		// whenever the UI is recreated (as in constructor)
		if (System.currentTimeMillis() >= fFileInfoCacheFlushTime) {
			// remove entries not checked at all in a while
			long deadLine = System.currentTimeMillis() - CACHE_CHECK_QUANTUM;
			Iterator<Pair<Long, Boolean>> iter = fFileInfoCache.values().iterator();
			while (iter.hasNext()) {
				Pair<Long, Boolean> ientry = iter.next();
				if (ientry.first < deadLine) {
					iter.remove();
				}
			}
			fFileInfoCacheFlushTime = System.currentTimeMillis() + CACHE_CHECK_QUANTUM;
		}
		
		if (entry == null) {
			//System.out.println("Checking " + file);
			entry = new Pair<Long, Boolean>(System.currentTimeMillis(), file.exists());
			fFileInfoCache.put(file, entry);
		} else {
			//System.out.println("Not checking " + file);
		}
		return entry.second;
	}
}
