/******************************************************************************* 
 * Copyright (c) 2011 Red Hat, Inc. 
 * Distributed under license by Red Hat, Inc. All rights reserved. 
 * This program is 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: 
 * Red Hat, Inc. - initial API and implementation 
 ******************************************************************************/ 
package org.jboss.tools.cdi.text.ext.hyperlink;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;

import org.eclipse.jdt.core.IMethod;
import org.eclipse.jface.action.Action;
import org.eclipse.jface.action.IMenuManager;
import org.eclipse.jface.dialogs.Dialog;
import org.eclipse.jface.dialogs.IDialogConstants;
import org.eclipse.jface.dialogs.IDialogSettings;
import org.eclipse.jface.dialogs.PopupDialog;
import org.eclipse.jface.resource.JFaceResources;
import org.eclipse.jface.viewers.CheckStateChangedEvent;
import org.eclipse.jface.viewers.CheckboxTreeViewer;
import org.eclipse.jface.viewers.DelegatingStyledCellLabelProvider;
import org.eclipse.jface.viewers.ICheckStateListener;
import org.eclipse.jface.viewers.ICheckStateProvider;
import org.eclipse.jface.viewers.IOpenListener;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.IStructuredContentProvider;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.ITreeContentProvider;
import org.eclipse.jface.viewers.OpenEvent;
import org.eclipse.jface.viewers.StructuredSelection;
import org.eclipse.jface.viewers.StyledCellLabelProvider;
import org.eclipse.jface.viewers.StyledString;
import org.eclipse.jface.viewers.StyledString.Styler;
import org.eclipse.jface.viewers.TableViewer;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.jface.viewers.ViewerCell;
import org.eclipse.swt.SWT;
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.MouseMoveListener;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.graphics.TextStyle;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Group;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Text;
import org.eclipse.swt.widgets.Widget;
import org.eclipse.ui.dialogs.SearchPattern;
import org.jboss.tools.cdi.core.CDIImages;
import org.jboss.tools.cdi.core.IBean;
import org.jboss.tools.cdi.core.ICDIElement;
import org.jboss.tools.cdi.core.IInjectionPoint;
import org.jboss.tools.cdi.core.IInjectionPointParameter;
import org.jboss.tools.cdi.core.IQualifierDeclaration;
import org.jboss.tools.cdi.core.util.BeanPresentationUtil;
import org.jboss.tools.cdi.text.ext.CDIExtensionsMessages;
import org.jboss.tools.cdi.text.ext.CDIExtensionsPlugin;
import org.jboss.tools.cdi.text.ext.hyperlink.AssignableBeanFilters.Checkbox;
import org.jboss.tools.cdi.text.ext.hyperlink.AssignableBeanFilters.Filter;
import org.jboss.tools.common.java.IParametedType;

/**
 * 
 * @author Viacheslav Kabanovich
 *
 */
public class AssignableBeansDialog extends PopupDialog {// TitleAreaDialog {
	IInjectionPoint injectionPoint;
	AssignableBeanFilters filters = null;
	Set<IBean> beans = new HashSet<IBean>();
	Set<IBean> eligibleBeans = new HashSet<IBean>();
	Set<IBean> resolvedBeans = new HashSet<IBean>();

	Composite composite;

	boolean showHideOptions = true;
	Group filterPanel;
	CheckboxTreeViewer filterView;

	private Text fFilterText;
	TableViewer list;

	public AssignableBeansDialog(Shell parentShell) {
		// TitleAreaDialog
//		super(parentShell);
		//PopupDialog
		super(parentShell, 0, true, true, true, true, true, "title", null);
		setShellStyle(getShellStyle() | SWT.RESIZE);
		filters = new AssignableBeanFilters();
	}

	protected void initSettings() {
		IDialogSettings settings = getDialogSettings();
		showHideOptions = settings.getBoolean(SHOW_HIDE_PANEL);
		for (Checkbox c: filters.getAllOptions().values()) {
			String name = FILTER_OPTION + c.id;
			c.state = !"false".equals(settings.get(name));
		}
		filters.getRoot().state = true;
	}

	public void setInjectionPoint(IInjectionPoint injectionPoint) {
		this.injectionPoint = injectionPoint;
		filters.init(injectionPoint);
		initSettings();
		beans = new HashSet<IBean>(injectionPoint.getCDIProject().getBeans(false, injectionPoint));
		eligibleBeans = new HashSet<IBean>(beans);
		for (int i = AssignableBeanFilters.OPTION_UNAVAILABLE_BEANS + 1; i < AssignableBeanFilters.OPTION_ELIMINATED_AMBIGUOUS; i++) {
			Filter f = filters.getFilter(i);
			if(f != null) {
				f.filter(eligibleBeans);
			}
		}
		resolvedBeans = new HashSet<IBean>(injectionPoint.getCDIProject().getBeans(true, injectionPoint));
	}

	String computeTitle() {
		StringBuffer result = new StringBuffer();
		result.append("@Inject ");
		
		if(injectionPoint instanceof IInjectionPointParameter) {
			IMethod m = ((IInjectionPointParameter)injectionPoint).getBeanMethod().getMethod();
			result.append(m.getElementName()).append("(");
		}
		for (IQualifierDeclaration d: injectionPoint.getQualifierDeclarations()) {
			result.append("@").append(d.getType().getElementName()).append(" ");
		}
		
		IParametedType type = injectionPoint.getMemberType();
		result.append(type.getType().getElementName()).append(" ");

		if(injectionPoint instanceof IInjectionPointParameter) {
			result.append(((IInjectionPointParameter)injectionPoint).getName());
			result.append(")");
		} else {
			result.append(injectionPoint.getSourceMember().getElementName());
		}

		return result.toString();
	}

	protected void buttonPressed(int buttonId) {
		if(buttonId == IDialogConstants.CLOSE_ID) {
			// TitleAreaDialog
//			cancelPressed();
		}
	}

	@Override
	protected Control createTitleMenuArea(Composite parent) {
		Composite fViewMenuButtonComposite= (Composite) super.createTitleMenuArea(parent);
		fFilterText = createFilterText(parent);
		return fViewMenuButtonComposite;
	}

	@Override
	protected void configureShell(Shell newShell) {
		super.configureShell(newShell);
		newShell.setText(CDIExtensionsMessages.ASSIGNABLE_BEANS_DIALOG_TITLE);
	}

	@Override
	protected Control createDialogArea(Composite parent) {
		composite = new Composite(parent, SWT.NONE);
		GridLayout layout = new GridLayout(2, false);
		layout.marginHeight = 0;
		layout.marginWidth = 0;
		layout.verticalSpacing = 0;
		layout.horizontalSpacing = 0;
		composite.setLayout(layout);
		composite.setLayoutData(new GridData(GridData.FILL_BOTH));
		composite.setFont(parent.getFont());
		// TitleAreaDialog
//		setTitle(computeTitle());
		// PopupDialog
		setTitleText(computeTitle());
		createListView(composite);
		if(showHideOptions) {
			createFilterView(composite);
		}
		installFilter();
		return composite;
	}

	protected void createButtonsForButtonBar(Composite parent) {
		// TitleAreaDialog
//		createButton(parent, IDialogConstants.CLOSE_ID, IDialogConstants.CLOSE_LABEL, true);
	}
	
	void createListView(Composite parent) {
		list = new TableViewer(parent, SWT.SINGLE | SWT.H_SCROLL | SWT.V_SCROLL | SWT.BORDER/* | SWT.VIRTUAL*/);
		GridData g = new GridData(GridData.FILL_BOTH);
		list.getControl().setLayoutData(g);
		list.setContentProvider(new ListContent());
		list.setLabelProvider(new LP());
		list.setInput(injectionPoint);
		list.addOpenListener(new IOpenListener() {			
			@Override
			public void open(OpenEvent event) {
				ISelection s = event.getSelection();
				if(!s.isEmpty() && s instanceof IStructuredSelection) {
					Object o = ((IStructuredSelection)s).getFirstElement();
					if(o instanceof IBean) {
						((IBean)o).open();
						close();
					}
				}
			}
		});

		// PopupDialog
		list.getTable().addMouseMoveListener(new MouseMoveListener() {
			@Override
			public void mouseMove(MouseEvent e) {
				ViewerCell cell = list.getCell(new Point(e.x, e.y));
				if(cell != null) {
					Widget w = cell.getItem();
					if(w != null && w.getData() != null) {
						list.setSelection(new StructuredSelection(w.getData()));
					}
				}
				list.getTable().setCursor(cell == null ? null : list.getTable().getDisplay().getSystemCursor(SWT.CURSOR_HAND));
			}
		});
		list.getTable().addMouseListener(new MouseAdapter() {
			@Override
			public void mouseUp(MouseEvent e) {
				gotoSelectedElement();
			}
		});

		list.refresh();
	}

	protected void gotoSelectedElement() {
		ISelection s = list.getSelection();
		if(!s.isEmpty() && s instanceof IStructuredSelection) {
			IBean b = (IBean)((IStructuredSelection)s).getFirstElement();
			b.open();
			close();
		}
	}

	void createFilterView(Composite parent) {
		Group g = filterPanel = new Group(parent, 0);
		g.setBackground(parent.getBackground());
		g.setText("Show/Hide");
		g.setLayoutData(new GridData(GridData.FILL_VERTICAL | GridData.VERTICAL_ALIGN_BEGINNING));
		g.setLayout(new GridLayout(1, false));
		filterView = new CheckboxTreeViewer(g, 0);
		filterView.setAutoExpandLevel(3);
		filterView.setContentProvider(checkboxContentProvider);
		filterView.setInput(filters.getRoot());
		for (Checkbox c: filters.getAllOptions().values()) {
			filterView.setChecked(c, true);
		}
		filterView.getControl().setBackground(g.getBackground());
		g.setData(new GridData(GridData.FILL_BOTH));
		filterView.addCheckStateListener(new ICheckStateListener() {			
			@Override
			public void checkStateChanged(CheckStateChangedEvent event) {
				((Checkbox)event.getElement()).state = event.getChecked();
				filterView.refresh();
				list.refresh();
			}
		});
		filterView.setCheckStateProvider(new ICheckStateProvider() {			
			@Override
			public boolean isGrayed(Object element) {
				Checkbox c = (Checkbox)element;
				return c.parent != null && !c.parent.state;
			}
			
			@Override
			public boolean isChecked(Object element) {
				Checkbox c = (Checkbox)element;
				return c.state;
			}
		});
	}

	@Override
	protected Control getFocusControl() {
		return getFilterText();
	}

	protected Text getFilterText() {
		return fFilterText;
	}

	protected Text createFilterText(Composite parent) {
		fFilterText= new Text(parent, SWT.NONE);
		Dialog.applyDialogFont(fFilterText);

		GridData data= new GridData(GridData.FILL_HORIZONTAL);
		data.horizontalAlignment= GridData.FILL;
		data.verticalAlignment= GridData.CENTER;
		fFilterText.setLayoutData(data);

		fFilterText.addKeyListener(new KeyListener() {
			@Override
			public void keyPressed(KeyEvent e) {
				if (e.keyCode == 0x0D) // return
					gotoSelectedElement();
				if (e.keyCode == SWT.ARROW_DOWN)
					list.getTable().setFocus();
				if (e.keyCode == SWT.ARROW_UP)
					list.getTable().setFocus();
				if (e.character == 0x1B) // ESC
					close();
			}
			@Override
			public void keyReleased(KeyEvent e) {
				// do nothing
			}
		});
		return fFilterText;
	}

	private void installFilter() {
		fFilterText.setText(""); //$NON-NLS-1$

		fFilterText.addModifyListener(new ModifyListener() {
			@Override
			public void modifyText(ModifyEvent e) {
				list.refresh();
			}
		});
	}

	protected String getId() {
		return AssignableBeansDialog.class.getName();
	}

	@Override
	protected Point getDefaultSize() {
		return new Point(700, 400);
	}

	@Override
	protected Point getDefaultLocation(Point size) {
		Display display = Display.getCurrent();
		if(display == null) {
			display = Display.getDefault();
		}
		if(display.getActiveShell() == null) {
			return super.getDefaultLocation(size);
		}
		Rectangle b = display.getActiveShell().getBounds();
		int x = b.x + (b.width - size.x) / 2;
		int y = b.y + (b.height - size.y) / 2;
		return new Point(x, y);
	}

	// PopupDialog
	@Override
	protected void fillDialogMenu(IMenuManager dialogMenu) {
		super.fillDialogMenu(dialogMenu);
		dialogMenu.add(new ShowHideAction());
	}

	@Override
	protected IDialogSettings getDialogSettings() {
		IDialogSettings settings = CDIExtensionsPlugin.getDefault().getDialogSettings().getSection(getId());
		if(settings == null && filters != null && injectionPoint != null) {
			settings = CDIExtensionsPlugin.getDefault().getDialogSettings().addNewSection(getId());
			settings.put(SHOW_HIDE_PANEL, true);
			for (Checkbox c: filters.getAllOptions().values()) {
				settings.put(FILTER_OPTION + c.id, "" + c.state);
			}
		}
		return settings;
	}

	@Override
	public boolean close() {
		saveFilterOptions();
		return super.close();
	}

	private static final String SHOW_HIDE_PANEL = "SHOW_HIDE_PANEL";
	private static final String FILTER_OPTION = "FILTER_OPTION_";

	private void saveFilterOptions() {
		IDialogSettings settings = getDialogSettings();
		settings.put(SHOW_HIDE_PANEL, showHideOptions);
		for (Checkbox c: filters.getAllOptions().values()) {
			settings.put(FILTER_OPTION + c.id, "" + c.state);
		}
	}

	public List<IBean> getDisplayedBeans() {
		List<IBean> bs = new ArrayList<IBean>();
		int index = 0;
		while(true) {
			Object o = list.getElementAt(index);
			if(o instanceof IBean) {
				bs.add((IBean)o);
			} else {
				break;
			}
			index++;
		}
		return bs;
	}

	public boolean isFilterEnabled(int index) {
		Checkbox c = filters.getOption(index);
		return c != null && c.state;
	}

	public void setFilterEnabled(int index, boolean value) {
		Checkbox c = filters.getOption(index);
		if(c == null) return;
		c.state = value;
		filterView.refresh();
//		filterView.setChecked(AssignableBeanFilters.ALL_OPTIONS[index], value);
		list.refresh();
	}

	public Collection<Checkbox> getOptions() {
		return filters.getAllOptions().values();
	}

	class ShowHideAction extends Action {
		public ShowHideAction() {
			super("Show/Hide panel", Action.AS_CHECK_BOX);
			setChecked(showHideOptions);
		}
		@Override
		public void run() {
			setFiltersEnabled(isChecked());
		}
	}

	protected void setFiltersEnabled(boolean enabled) {
		if(enabled != showHideOptions) {
			showHideOptions = enabled;
			if(!enabled && filterPanel != null) {
				filterPanel.dispose();
				filterPanel = null;
				filterView = null;
			} else if(enabled && filterPanel == null) {
				createFilterView(composite);
			}
			composite.update();
			composite.layout(true);
		}
	}

	static CheckboxContentProvider checkboxContentProvider = new CheckboxContentProvider();
	
	static class CheckboxContentProvider implements ITreeContentProvider {

		@Override
		public void dispose() {
		}

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

		@Override
		public Object[] getElements(Object inputElement) {
			return getChildren(inputElement);
		}

		@Override
		public Object[] getChildren(Object parentElement) {
			return ((Checkbox)parentElement).children.toArray(new Checkbox[0]);
		}

		@Override
		public Object getParent(Object element) {
			return ((Checkbox)element).parent;
		}

		@Override
		public boolean hasChildren(Object element) {
			return !((Checkbox)element).children.isEmpty();
		}
	}

	TextFilter textFilter = new TextFilter();

	class TextFilter implements Filter {
		SearchPattern patternMatcher = new SearchPattern();
		@Override
		public void filter(Set<IBean> beans) {
			if(fFilterText != null && !fFilterText.isDisposed()) {
				String text = fFilterText.getText();
				if(text.length() > 0) {
					patternMatcher.setPattern(text);
					Iterator<IBean> it = beans.iterator();
					while(it.hasNext()) {
						IBean bean = it.next();
						if(!patternMatcher.matches(bean.getElementName())) {
							it.remove();
						}
					}
				}
			}			
		}		
	}

	class ListContent implements IStructuredContentProvider {

		@Override
		public void dispose() {
		}

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

		@Override
		public Object[] getElements(Object inputElement) {
			Set<IBean> bs = new HashSet<IBean>(beans);
			Set<String> keys = new HashSet<String>();
			LP p = new LP();
			filters.getRoot().filter(bs);
			textFilter.filter(bs);
			Map<String, IBean> map = new TreeMap<String, IBean>();
			for (IBean b: bs) {
				if(resolvedBeans.contains(b)) {
					String key = p.getText(b);
					if(!keys.contains(key)) {
						map.put(key, b);
						keys.add(key);
					}
				}
			}
			List<IBean> sorted = new ArrayList<IBean>();
			sorted.addAll(map.values());
			bs.removeAll(map.values());
			map.clear();

			for (IBean b: bs) {
				if(eligibleBeans.contains(b)) {
					String key = p.getText(b);
					if(!keys.contains(key)) {
						map.put(key, b);
						keys.add(key);
					}
				}
			}
			sorted.addAll(map.values());
			bs.removeAll(map.values());
			map.clear();

			for (IBean b: bs) {
				String key = p.getText(b);
				if(!keys.contains(key)) {
					map.put(key, b);
					keys.add(key);
				}
			}
			sorted.addAll(map.values());

			return sorted.toArray(new IBean[0]);
		}		
	}

	static Color gray = new Color(null, 128, 128, 128);
	static Color black = Display.getDefault().getSystemColor(SWT.COLOR_INFO_FOREGROUND);

	static Styler RESOLVED_NAME = new DefaultStyler(black, true, false);
	static Styler ELIGIBLE_NAME = new DefaultStyler(black, false, false);
	static Styler ELIGIBLE_QUALIFIER = new DefaultStyler(gray, false, false);
	static Styler DISABLED = new DefaultStyler(gray, false, false);

	private static class DefaultStyler extends Styler {
		private Color foreground;
		private boolean bold;
		private boolean italic;

		public DefaultStyler(Color foreground, boolean bold, boolean italic) {
			this.foreground = foreground;
			this.italic = italic;
			this.bold = bold;
		}

		@Override
		public void applyStyles(TextStyle textStyle) {
			if (foreground != null) {
				textStyle.foreground = foreground;
			}
			if(italic) {
				textStyle.font = JFaceResources.getFontRegistry().getItalic(JFaceResources.DEFAULT_FONT);
			}
			if(bold) {
				textStyle.font = JFaceResources.getFontRegistry().getBold(JFaceResources.DEFAULT_FONT);
			}
		}
	}

	class LP extends StyledCellLabelProvider implements DelegatingStyledCellLabelProvider.IStyledLabelProvider {
		@Override
		public void update(ViewerCell cell) {
			ELIGIBLE_QUALIFIER = DISABLED;
			Object element = cell.getElement();
			StyledString styledString = getStyledText(element);
			cell.setText(styledString.getString());
			cell.setStyleRanges(styledString.getStyleRanges());
			cell.setImage(getImage(element));

			super.update(cell);
		}

		public String getText(Object element) {
			return getStyledText(element).getString();
		}

		@Override
		public StyledString getStyledText(Object element) {
			IBean b = (IBean)element;
			RESOLVED_NAME = new DefaultStyler(black, true, false);
			Styler nameStyler = resolvedBeans.contains(b) ? RESOLVED_NAME 
					: eligibleBeans.contains(b) ? ELIGIBLE_NAME : DISABLED;
			StyledString sb = new StyledString();

			//1.bean kind
			String kind = BeanPresentationUtil.getBeanKind(b);
			if(kind != null) {
				sb.append(kind, nameStyler).append(' ');
			}
			//2. bean element name
			sb.append(b.getElementName(), nameStyler);

			//3. bean location
			Styler qualifierStyler = eligibleBeans.contains(b) ? ELIGIBLE_QUALIFIER : DISABLED;
			sb.append(BeanPresentationUtil.getBeanLocation(b, false),qualifierStyler);
			return sb;
		}

		@Override
		public Image getImage(Object element) {
			return CDIImages.getImageByElement((ICDIElement)element);
		}
	}
}