/*******************************************************************************
 * Copyright (c) 2015, 2015 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     Bruno Medeiros - initial API and implementation
 *******************************************************************************/
package melnorme.lang.ide.ui.views;


import static melnorme.utilbox.core.Assert.AssertNamespace.assertNotNull;
import static melnorme.utilbox.core.Assert.AssertNamespace.assertTrue;
import static melnorme.utilbox.core.CoreUtil.array;
import melnorme.lang.ide.ui.LangUIPlugin;
import melnorme.util.swt.jface.TreeViewerExt;
import melnorme.util.swt.jface.TreeViewerUtil;
import melnorme.utilbox.misc.ArrayUtil;

import org.eclipse.jface.action.IMenuManager;
import org.eclipse.jface.dialogs.Dialog;
import org.eclipse.jface.dialogs.IDialogSettings;
import org.eclipse.jface.dialogs.PopupDialog;
import org.eclipse.jface.text.IInformationControl;
import org.eclipse.jface.text.IInformationControlExtension;
import org.eclipse.jface.text.IInformationControlExtension2;
import org.eclipse.jface.viewers.DelegatingStyledCellLabelProvider;
import org.eclipse.jface.viewers.DelegatingStyledCellLabelProvider.IStyledLabelProvider;
import org.eclipse.jface.viewers.IBaseLabelProvider;
import org.eclipse.jface.viewers.ILabelProvider;
import org.eclipse.jface.viewers.ITreeContentProvider;
import org.eclipse.jface.viewers.TreeViewer;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.jface.viewers.ViewerFilter;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.DisposeEvent;
import org.eclipse.swt.events.DisposeListener;
import org.eclipse.swt.events.FocusListener;
import org.eclipse.swt.events.KeyAdapter;
import org.eclipse.swt.events.KeyEvent;
import org.eclipse.swt.events.KeyListener;
import org.eclipse.swt.events.ModifyEvent;
import org.eclipse.swt.events.ModifyListener;
import org.eclipse.swt.events.MouseAdapter;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Text;
import org.eclipse.swt.widgets.Tree;
import org.eclipse.swt.widgets.TreeItem;

public abstract class AbstractFilteredTreePopupControl extends PopupDialog implements
	IInformationControl, 
	IInformationControlExtension,
	IInformationControlExtension2, 
	DisposeListener 
{
	
	protected final int treeStyle;
	
	protected TreeElementsFilter treeElementsFilter;
	
	protected Text filterText;
	protected TreeViewerExt treeViewer;
	
	protected Composite fViewMenuButtonComposite;
	
	protected String filteringString = "";
	
	
	public AbstractFilteredTreePopupControl(Shell parent, int shellStyle, int treeStyle) {
		super(parent, shellStyle, true, true, false, true, true, null, null);
		this.treeStyle = treeStyle;
		
		// Title and info text must be set to some value to have the controls created, so set empty values.
		// These will be updated later.
		if(hasHeader()) {
			setTitleText("");
		}
		setInfoText("");
		
		create();
		
		setInfoText("Outline"); // TODO: Use info text for more element information, or key-toggle info
	}
	
	protected Text getFilterText() {
		return filterText;
	}
	
	protected TreeViewer getTreeViewer() {
		return treeViewer;
	}
	
	@Override
	public void dispose() {
		close();
	}
	
	protected boolean hasHeader() {
		return false; // Subclasses may reimplement
	}
	
	/* ----------------- Shell creation (JDT based) ----------------- */
	
	@Override
	public void setVisible(boolean visible) {
		if(visible) {
			open();
		} else {
			saveDialogBounds(getShell());
			getShell().setVisible(false);
		}
	}
	
	@Override
	public void setSizeConstraints(int maxWidth, int maxHeight) {
	}
	
	@Override
	public Point computeSizeHint() {
		// return the shell's size - note that it already has the persisted size if persisting is enabled.
		return getShell().getSize();
	}
	
	@Override
	public void setLocation(Point location) {
		/*
		 * If the location is persisted, it gets managed by PopupDialog - fine. Otherwise, the location is
		 * computed in Window#getInitialLocation, which will center it in the parent shell / main
		 * monitor, which is wrong for two reasons:
		 * - we want to center over the editor / subject control, not the parent shell
		 * - the center is computed via the initalSize, which may be also wrong since the size may
		 *   have been updated since via min/max sizing of AbstractInformationControlManager.
		 * In that case, override the location with the one computed by the manager. Note that
		 * the call to constrainShellSize in PopupDialog.open will still ensure that the shell is
		 * entirely visible.
		 */
		if (!getPersistLocation() || getDialogSettings() == null)
			getShell().setLocation(location);
	}
	
	@Override
	protected IDialogSettings getDialogSettings() {
		return LangUIPlugin.getDialogSettings(getDialogSettingsId());
	}
	
	protected abstract String getDialogSettingsId();
	
	@Override
	public boolean hasContents() {
		return treeViewer != null && treeViewer.getInput() != null;
	}
	
	@Override
	public void setSize(int width, int height) {
		getShell().setSize(width, height);
	}
	
	@Override
	public void addDisposeListener(DisposeListener listener) {
		getShell().addDisposeListener(listener);
	}
	
	@Override
	public void removeDisposeListener(DisposeListener listener) {
		getShell().removeDisposeListener(listener);
	}
	
	@Override
	public void setForegroundColor(Color foreground) {
		applyForegroundColor(foreground, getContents());
	}
	
	@Override
	public void setBackgroundColor(Color background) {
		applyBackgroundColor(background, getContents());
	}
	
	@Override
	public boolean isFocusControl() {
		return getShell().getDisplay().getActiveShell() == getShell();
	}
	
	@Override
	public void setFocus() {
		getShell().forceFocus();
		filterText.setFocus();
	}
	
	@Override
	public void addFocusListener(FocusListener listener) {
		getShell().addFocusListener(listener);
	}
	
	@Override
	public void removeFocusListener(FocusListener listener) {
		getShell().removeFocusListener(listener);
	}
	
	@Override
	public void widgetDisposed(DisposeEvent event) {
		treeViewer = null;
		filterText = null;
	}
	
	/* ----------------- Create controls ----------------- */
	
	@Override
	protected Control createTitleMenuArea(Composite parent) {
		fViewMenuButtonComposite= (Composite) super.createTitleMenuArea(parent);
		
		// If there is a header, then the filter text must be created underneath the title and menu area.
		if(hasHeader()) {
			filterText = createFilterText(parent);
		}
		
		return fViewMenuButtonComposite;
	}
	
	@Override
	protected Control createTitleControl(Composite parent) {
		if(hasHeader()) {
			return super.createTitleControl(parent);
		}
		filterText = createFilterText(parent);
		return filterText;
	}
	
	protected Text createFilterText(Composite parent) {
		filterText = new Text(parent, SWT.NONE);
		Dialog.applyDialogFont(filterText);
		
		filterText.setLayoutData(new GridData(GridData.FILL, GridData.CENTER, true, false));
		
		filterText.addKeyListener(new KeyListener() {
			@Override
			public void keyPressed(KeyEvent e) {
				if(e.keyCode == 0x0D || e.keyCode == SWT.KEYPAD_CR) {
					gotoSelectedElement();
				}
				if(e.keyCode == SWT.ARROW_DOWN) {
					treeViewer.getTree().setFocus();
				}
				if(e.keyCode == SWT.ARROW_UP) {
					treeViewer.getTree().setFocus();
				}
				if(e.character == SWT.ESC) {
					dispose();
				}
			}
			@Override
			public void keyReleased(KeyEvent e) {
			}
		});
		
		return filterText;
	}
	
	protected Tree createTree(Composite parent, int treeStyle) {
		final Tree tree = new Tree(parent, SWT.SINGLE | (treeStyle & ~SWT.MULTI));
		
		GridData gd = new GridData(SWT.FILL, SWT.FILL, true, true);
		gd.heightHint = tree.getItemHeight() * 12;
		tree.setLayoutData(gd);
		
		tree.addKeyListener(new KeyAdapter() {
			@Override
			public void keyPressed(KeyEvent e) {
				if(e.character == 0x1B) {
					dispose();
				}
			}
		});
		
		// Auto mouse click listeners
		tree.addSelectionListener(new SelectionAdapter() {
			@Override
			public void widgetDefaultSelected(SelectionEvent e) {
				gotoSelectedElement();
			}
		});
		tree.addMouseListener(new MouseAdapter() {
			@Override
			public void mouseUp(MouseEvent e) {
				if(e.button != 1)
					return;
				
				if(tree == e.getSource() && tree.getSelectionCount() == 1) {
					TreeItem selection = tree.getSelection()[0];
					
					if(selection.equals(tree.getItem(new Point(e.x, e.y)))) {
						gotoSelectedElement();
					}
				}
			}
		});
		
		return tree;
	}
	
	protected void createTreeViewer(Composite parent, int treeStyle) {
		Tree tree = createTree(parent, treeStyle);
		
		treeViewer = new TreeViewerFilterExt(tree);
		TreeViewerUtil.addTreeViewerMouseAutoScroller(treeViewer);
		
		treeElementsFilter = createTreeFilter();
		treeViewer.addFilter(treeElementsFilter);
		
		treeViewer.setAutoExpandLevel(TreeViewer.ALL_LEVELS);
	}
	
	@Override
	protected Control createDialogArea(Composite parent) {
		createTreeViewer(parent, treeStyle);
		
		filterText.addModifyListener(new ModifyListener() {
			@Override
			public void modifyText(ModifyEvent e) {
				String text = ((Text) e.widget).getText();
				setFilteringString(text);
			}
		});
		
		addDisposeListener(this);
		return treeViewer.getControl();
	}
	
	@Override
	protected void setTabOrder(Composite composite) {
		if (hasHeader()) {
			composite.setTabList(array(filterText, treeViewer.getTree()));
		} else {
			fViewMenuButtonComposite.setTabList(array(filterText));
			composite.setTabList(array(fViewMenuButtonComposite, treeViewer.getTree()));
		}
	}
	
	/* ----------------- input / selection ----------------- */
	
	@Override
	public void setInformation(String information) {
		// ignored, IInformationControlExtension2.setInput(Object) should be used instead.
	}
	
	@Override
	public void setInput(Object information) {
		doSetInput(information, information);
	}
	
	protected void doSetInput(Object newInput, Object selectedElement) {
		filterText.setText("");
		treeViewer.setInput(newInput);
		treeViewer.setSelectedElement(selectedElement);
	}
	
	protected Object getSelectedElement() {
		if(treeViewer == null)
			return null;
		
		return treeViewer.getSelectionFirstElement();
	}
	
	protected Object firstDirectlyFilteredInElement;
	
	protected void setFilteringString(String pattern) {
		this.filteringString = pattern;
		refreshViewerFiltering();
	}
	
	protected void refreshViewerFiltering() {
		treeViewer.getControl().setRedraw(false);
		treeViewer.refresh();
		treeViewer.setSelectedElement(firstDirectlyFilteredInElement);
		treeViewer.expandAll();
		treeViewer.getControl().setRedraw(true);
	}
	
	/* ----------------- Filter ----------------- */
	
	protected class TreeViewerFilterExt extends TreeViewerExt {
		public TreeViewerFilterExt(Tree tree) {
			super(tree);
		}
		
		@Override
		protected void internalInitializeTree(Control widget) {
			AbstractFilteredTreePopupControl.this.firstDirectlyFilteredInElement = null;
			super.internalInitializeTree(widget);
		}
		
		@Override
		protected void internalRefresh(Object element, boolean updateLabels) {
			if(element == getRoot()) {
				AbstractFilteredTreePopupControl.this.firstDirectlyFilteredInElement = null;
			}
			super.internalRefresh(element, updateLabels);
		}
	}
	
	protected TreeElementsFilter createTreeFilter() {
		return new TreeElementsFilter();
	}
	
	protected class TreeElementsFilter extends ViewerFilter {
		
		@Override
		public boolean select(Viewer viewer, Object parentElement, Object element) {
			assertTrue(viewer == treeViewer);
			return elementFilteredIn(element);
		}
		
		protected boolean elementFilteredIn(Object element) {
			if(elementDirectlyFilteredIn(element)) {
				if(firstDirectlyFilteredInElement == null) {
					firstDirectlyFilteredInElement = element;
				}
				return true;
			}
			
			return hasFilteredInChildren(element);
		}
		
		protected boolean hasFilteredInChildren(Object element) {
			Object[] children = ((ITreeContentProvider) treeViewer.getContentProvider()).getChildren(element);
			
			for(Object child : ArrayUtil.nullToEmpty(children)) {
				if(elementFilteredIn(child)) {
					return true;
				}
			}
			return false;
		}
		
	}
	
	protected boolean elementDirectlyFilteredIn(Object element) {
		if(filteringString.length() == 0)
			return true;
		
		String matchName = getText(element);
		assertNotNull(matchName);
		return matchName != null && matchNameDirectlyFilteredIn(matchName);
	}
	
	protected String getText(Object element) {
		IBaseLabelProvider provider = treeViewer.getLabelProvider();
		if(provider instanceof ILabelProvider) {
			ILabelProvider labelProvider = (ILabelProvider) provider;
			return labelProvider.getText(element);
		} else if(provider instanceof IStyledLabelProvider) {
			IStyledLabelProvider labelProvider = (IStyledLabelProvider) provider;
			return labelProvider.getStyledText(element).getString();
		} else if(provider instanceof DelegatingStyledCellLabelProvider) {
			DelegatingStyledCellLabelProvider styledCellLabelProvider = (DelegatingStyledCellLabelProvider) provider;
			IStyledLabelProvider labelProvider = styledCellLabelProvider.getStyledStringProvider();
			return labelProvider.getStyledText(element).getString();
		}
		return null;
	}
	
	protected abstract boolean matchNameDirectlyFilteredIn(String matchName);
	
	/* ----------------- Actions / operations ----------------- */
	
	@Override
	protected void fillDialogMenu(IMenuManager dialogMenu) {
		super.fillDialogMenu(dialogMenu);
		fillViewMenu(dialogMenu);
	}
	
	@SuppressWarnings("unused")
	protected void fillViewMenu(IMenuManager viewMenu) {
	}
	
	protected abstract void gotoSelectedElement();
	
}