/*******************************************************************************
 * Copyright (c) 2000, 2018 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:
 *     IBM Corporation - initial API and implementation
 *     Brock Janiczak <brockj@tpg.com.au> - Bug 182267 "Add Date..." button shouldn't be visible in merge wizard
 *******************************************************************************/
package org.eclipse.team.internal.ccvs.ui.tags;

import java.util.*;
import java.util.List;

import org.eclipse.jface.action.*;
import org.eclipse.jface.dialogs.Dialog;
import org.eclipse.jface.operation.IRunnableContext;
import org.eclipse.jface.viewers.*;
import org.eclipse.osgi.util.NLS;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.*;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.widgets.*;
import org.eclipse.team.internal.ccvs.core.CVSTag;
import org.eclipse.team.internal.ccvs.core.ICVSRepositoryLocation;
import org.eclipse.team.internal.ccvs.ui.CVSUIMessages;
import org.eclipse.team.internal.ccvs.ui.CVSUIPlugin;
import org.eclipse.team.internal.ccvs.ui.repo.NewDateTagAction;
import org.eclipse.team.internal.ccvs.ui.repo.RepositoryManager;
import org.eclipse.team.internal.ccvs.ui.tags.TagSourceWorkbenchAdapter.ProjectElementComparator;
import org.eclipse.team.internal.ui.PixelConverter;
import org.eclipse.team.internal.ui.SWTUtils;
import org.eclipse.team.internal.ui.actions.TeamAction;
import org.eclipse.team.internal.ui.dialogs.DialogArea;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.model.WorkbenchContentProvider;
import org.eclipse.ui.model.WorkbenchLabelProvider;
import org.eclipse.ui.part.PageBook;

/**
 * A dialog area that displays a list of tags for selection and supports
 * filtering of the displayed tags.
 */
public class TagSelectionArea extends DialogArea {
	
    private static int COLUMN_TRIM = "carbon".equals(SWT.getPlatform()) ? 24 : 3; //$NON-NLS-1$
    
    private static int ICON_WIDTH = 40; 
    
    /*
     * Please see bug 184660
     */
    private static final int SAFETY_MARGIN = 50;
    
    /*
     * Property constant which identifies the selected tag or
     * null if no tag is selected
     */
    public static final String SELECTED_TAG = "selectedTag"; //$NON-NLS-1$
    
    /*
     * Property constant which indicates that a tag has been selected in such 
     * a way as to indicate that this is the desired tag (e.g double-click)
     */
    public static final String OPEN_SELECTED_TAG = "openSelectedTag";  //$NON-NLS-1$
    
    /*
     * Constants used to configure which tags are shown
     */
	public static final int INCLUDE_HEAD_TAG = TagSourceWorkbenchAdapter.INCLUDE_HEAD_TAG;
	public static final int INCLUDE_BASE_TAG = TagSourceWorkbenchAdapter.INCLUDE_BASE_TAG;
	public static final int INCLUDE_BRANCHES = TagSourceWorkbenchAdapter.INCLUDE_BRANCHES;
	public static final int INCLUDE_VERSIONS = TagSourceWorkbenchAdapter.INCLUDE_VERSIONS;
	public static final int INCLUDE_DATES = TagSourceWorkbenchAdapter.INCLUDE_DATES;
	public static final int INCLUDE_ALL_TAGS = TagSourceWorkbenchAdapter.INCLUDE_ALL_TAGS;
	
    private String tagAreaLabel;
    private final int includeFlags;
    private CVSTag selection;
    private String helpContext;
    private Text filterText;
    private TagSource tagSource;
    private final Shell shell;
    private TagRefreshButtonArea tagRefreshArea;
    private final TagSource.ITagSourceChangeListener listener = source -> {
		Shell shell = getShell();
		if (!shell.isDisposed()) {
	        shell.getDisplay().syncExec(() -> refresh());
		}
	};
    private final DisposeListener disposeListener = e -> {
	    if (tagSource != null)
	        tagSource.removeListener(listener);
	};

    private PageBook switcher;
    private TreeViewer tagTree;
    private TableViewer tagTable;
    private boolean treeVisible = true;
    private boolean includeFilterInputArea = true;
    private String filterPattern = ""; //$NON-NLS-1$

    private IRunnableContext context;
    
    public TagSelectionArea(Shell shell, TagSource tagSource, int includeFlags, String helpContext) {
        this.shell = shell;
        this.includeFlags = includeFlags;
        this.helpContext = helpContext;
        this.tagSource = tagSource;
        setSelection(null);
    }

    @Override
	public void createArea(Composite parent) {
        initializeDialogUnits(parent);
        Dialog.applyDialogFont(parent);
        final PixelConverter converter= new PixelConverter(parent);
        
        // Create a composite for the entire area
        Composite composite= new Composite(parent, SWT.NONE);
        composite.setLayoutData(SWTUtils.createHVFillGridData());
        composite.setLayout(SWTUtils.createGridLayout(1, converter, SWTUtils.MARGINS_NONE));
        
		// Add F1 help
		if (helpContext != null) {
            PlatformUI.getWorkbench().getHelpSystem().setHelp(composite, helpContext);
		}
		
		// Create the tree area and refresh buttons with the possibility to add stuff in between
		createTagDisplayArea(composite);	
		createCustomArea(composite);
		createRefreshButtons(composite);
		
        Dialog.applyDialogFont(parent);
        updateTagDisplay(true);
    }

    private void createTagDisplayArea(Composite parent) {
        Composite inner = createGrabbingComposite(parent, 1);
        if (isIncludeFilterInputArea()) {
            createFilterInput(inner);
            createWrappingLabel(inner, CVSUIMessages.TagSelectionArea_0, 1); 
        } else {
		    createWrappingLabel(inner, NLS.bind(CVSUIMessages.TagSelectionArea_1, new String[] { getTagAreaLabel() }), 1);  
        }
		switcher = new PageBook(inner, SWT.NONE);
		GridData gridData = new GridData(GridData.FILL_BOTH);
		gridData.heightHint = 0;
		gridData.widthHint = 0;
        switcher.setLayoutData(gridData);
		tagTree = createTree(switcher);
		tagTable = createTable(switcher);
    }

    private void createFilterInput(Composite inner) {
        createWrappingLabel(inner, NLS.bind(CVSUIMessages.TagSelectionArea_2, new String[] { getTagAreaLabel() }), 1); 
        filterText = createText(inner, 1);
        filterText.addModifyListener(e -> setFilter(filterText.getText()));
        filterText.addKeyListener(new KeyListener() {
            @Override
			public void keyPressed(KeyEvent e) {
        		if (e.keyCode == SWT.ARROW_DOWN && e.stateMask == 0) {			
        			tagTable.getControl().setFocus();
        		}
            }
            @Override
			public void keyReleased(KeyEvent e) {
                // Ignore
            }
        });
    }

    /**
     * Return the label that should be used for the tag area.
     * It should not have any trailing punctuations as the tag area
     * may position it differently depending on whether the filter
     * text input is included in the area.
     * @return the tag area label
     */
    public String getTagAreaLabel() {
        if (tagAreaLabel == null)
            tagAreaLabel = CVSUIMessages.TagSelectionArea_3; 
        return tagAreaLabel;
    }

    /**
     * Set the label that should be used for the tag area.
     * It should not have any trailing punctuations as the tag area
     * may position it differently depending on whether the filter
     * text input is included in the area.
     * @param tagAreaLabel the tag area label
     */
    public void setTagAreaLabel(String tagAreaLabel) {
        this.tagAreaLabel = tagAreaLabel;
    }
    
    /**
     * Update the tag display to show the tags that match the
     * include flags and the filter entered by the user.
     */
    protected void updateTagDisplay(boolean firstTime) {
        String filter = getFilterString();
        if ((filter != null && filter.length() > 0) || isTableOnly()) {
            // Show the table and filter it accordingly
            try {
	            switcher.setRedraw(false);
	            treeVisible = false;
	            switcher.showPage(tagTable.getControl());
	            FilteredTagList list = (FilteredTagList)tagTable.getInput();
	            list.setPattern(filter);
	            tagTable.refresh();
                int maxWidth = getMaxWidth(list.getChildren(null));
                if (maxWidth > 0) {
                    maxWidth = maxWidth + ICON_WIDTH + COLUMN_TRIM + SAFETY_MARGIN; /* space for the tag icon */
                    tagTable.getTable().getColumn(0).setWidth(maxWidth);
                }
	            if (filterText == null || filter == null || filter.length() == 0) {
	                setSelection(selection);
	            } else {
	                // Only set the top selection if there is a filter from the filter text
	                // of this area. This is done to avoid selection loops
	                selectTopElement();
	            }
            } finally {
                switcher.setRedraw(true);
            }
        } else {
            // Show the tree
            if (!isTreeVisible() || firstTime) {
                try {
                    switcher.setRedraw(false);
                    treeVisible = true;
	                switcher.showPage(tagTree.getControl());
	                tagTree.refresh();
	                setSelection(selection);
                } finally {
                    switcher.setRedraw(true);
                }
            }
        }
    }
    
    private int getMaxWidth(Object[] children) {
        PixelConverter converter = new PixelConverter(tagTable.getTable());
        int maxWidth = 0;
        for (int i = 0; i < children.length; i++) {
            Object object = children[i];
            if (object instanceof TagElement) {
                TagElement tag = (TagElement) object;
                int width = tag.getTag().getName().length();
                if (width > maxWidth) {
                    maxWidth = width;
                }
            }
        }
        return converter.convertWidthInCharsToPixels(maxWidth);
    }

    /**
     * Return whether only a table should be used
     * @return whether only a table should be used
     */
    protected boolean isTableOnly() {
        return (includeFlags == INCLUDE_VERSIONS) || (includeFlags == INCLUDE_BRANCHES);
    }

    private String getFilterString() {
        return filterPattern;
    }

    /*
     * Select the top element in the tag table
     */
    private void selectTopElement() {
        if (tagTable.getTable().getItemCount() > 0) {
            TableItem item = tagTable.getTable().getItem(0);
            tagTable.getTable().setSelection(new TableItem[] { item });
            tagTable.setSelection(tagTable.getSelection());
        }   
    }

    private FilteredTagList createFilteredInput() {
        return new FilteredTagList(tagSource, TagSource.convertIncludeFlaqsToTagTypes(includeFlags));
    }
    
    private Text createText(Composite parent, int horizontalSpan) {
        Text text = new Text(parent, SWT.SEARCH);
		GridData data = new GridData();
		data.horizontalSpan = horizontalSpan;
		data.horizontalAlignment = GridData.FILL;
		data.grabExcessHorizontalSpace = true;
		data.widthHint= 0;
		text.setLayoutData(data);
        return text;
    }

    protected void createRefreshButtons(Composite parent) {
	    tagSource.addListener(listener);
        parent.addDisposeListener(disposeListener);
        Listener listener = null;
        if ((includeFlags & TagSourceWorkbenchAdapter.INCLUDE_DATES) != 0) {
            listener = event -> {
			    CVSTag dateTag = NewDateTagAction.getDateTag(getShell(), getLocation());
			    addDateTag(dateTag);
			};
        }
	    tagRefreshArea = new TagRefreshButtonArea(shell, tagSource, listener);
	    if (context != null)
	        tagRefreshArea.setRunnableContext(context);
	    tagRefreshArea.createArea(parent);
    }

    protected void createTreeMenu(TreeViewer tagTree) {
        if ((includeFlags & TagSourceWorkbenchAdapter.INCLUDE_DATES) != 0) {
	        // Create the popup menu
			MenuManager menuMgr = new MenuManager();
			Tree tree = tagTree.getTree();
			Menu menu = menuMgr.createContextMenu(tree);
			menuMgr.addMenuListener(manager -> addMenuItemActions(manager));
			menuMgr.setRemoveAllWhenShown(true);
			tree.setMenu(menu);
        }
    }

    /**
     * Create aq custom area that is below the tag selection area but above the refresh busson group
     * @param parent
     */
	protected void createCustomArea(Composite parent) {
		// No default custom area
    }
	
    protected TreeViewer createTree(Composite parent) {
		Tree tree = new Tree(parent, SWT.SINGLE | SWT.BORDER);
		GridData data = new GridData(GridData.FILL_BOTH);
        tree.setLayoutData(data);
		TreeViewer result = new TreeViewer(tree);
		initialize(result);
		result.getControl().addKeyListener(new KeyListener() {
			@Override
			public void keyPressed(KeyEvent event) {
				handleKeyPressed(event);
			}
			@Override
			public void keyReleased(KeyEvent event) {
				handleKeyReleased(event);
			}
		});
		result.setInput(createUnfilteredInput());
		createTreeMenu(result);
		return result;
	}

    protected TableViewer createTable(Composite parent) {
		Table table = new Table(parent, SWT.H_SCROLL | SWT.V_SCROLL | SWT.BORDER | SWT.SINGLE | SWT.FULL_SELECTION);
		GridData data = new GridData(GridData.FILL_BOTH);
		table.setLayoutData(data);
		TableLayout layout = new TableLayout();
		layout.addColumnData(new ColumnWeightData(100));
		table.setLayout(layout);
		new TableColumn(table, SWT.NONE);
		TableViewer viewer = new TableViewer(table);
		initialize(viewer);
		viewer.setInput(createFilteredInput());
		return viewer;

	}

    private void initialize(StructuredViewer viewer) {
        viewer.setContentProvider(new WorkbenchContentProvider());
		viewer.setLabelProvider(new WorkbenchLabelProvider());
		viewer.setComparator(new ProjectElementComparator());
		viewer.addSelectionChangedListener(event -> handleSelectionChange());
		// select and close on double click
		// To do: use defaultselection instead of double click
		viewer.getControl().addMouseListener(new MouseAdapter() {
			@Override
			public void mouseDoubleClick(MouseEvent e) {
			    CVSTag tag = internalGetSelectedTag();
			    if (tag != null) {
			        firePropertyChangeChange(OPEN_SELECTED_TAG, null, tag);
			    }
			}
		});
    }

    private Object createUnfilteredInput() {
        return TagSourceWorkbenchAdapter.createInput(tagSource, includeFlags);
    }

    public void handleKeyPressed(KeyEvent event) {
		if (event.character == SWT.DEL && event.stateMask == 0) {			
			deleteDateTag();
		}
	}
	private void deleteDateTag() {
		TagElement[] selectedDateTagElements = getSelectedDateTagElement();
		if (selectedDateTagElements.length == 0) return;
		for(int i = 0; i < selectedDateTagElements.length; i++){
			RepositoryManager mgr = CVSUIPlugin.getPlugin().getRepositoryManager();
			CVSTag tag = selectedDateTagElements[i].getTag();
			if(tag.getType() == CVSTag.DATE){
				mgr.removeDateTag(getLocation(),tag);
			}				
		}
		tagTree.refresh();
		handleSelectionChange();
	}
	
	/**
	 * Returns the selected date tag elements
	 */
	private TagElement[] getSelectedDateTagElement() {
		ArrayList dateTagElements = null;
		IStructuredSelection selection = tagTree.getStructuredSelection();
		if (selection!=null && !selection.isEmpty()) {
			dateTagElements = new ArrayList();
			Iterator elements = selection.iterator();
			while (elements.hasNext()) {
				Object next = TeamAction.getAdapter(elements.next(), TagElement.class);
				if (next instanceof TagElement) {
					if(((TagElement)next).getTag().getType() == CVSTag.DATE){
						dateTagElements.add(next);
					}
				}
			}
		}
		if (dateTagElements != null && !dateTagElements.isEmpty()) {
			TagElement[] result = new TagElement[dateTagElements.size()];
			dateTagElements.toArray(result);
			return result;
		}
		return new TagElement[0];
	}
	private void addDateTag(CVSTag tag){
		if(tag == null) return;
		List dateTags = new ArrayList();
		ICVSRepositoryLocation location = getLocation();
		dateTags.addAll(Arrays.asList(CVSUIPlugin.getPlugin().getRepositoryManager().getDateTags(location)));
		if(!dateTags.contains( tag)){
            CVSUIPlugin.getPlugin().getRepositoryManager().addDateTag(location, tag);
		}
		try {
			tagTree.getControl().setRedraw(false);
			tagTree.refresh();
			setSelection(tag);
		} finally {
			tagTree.getControl().setRedraw(true);
		}
		handleSelectionChange();
	}
	private void addMenuItemActions(IMenuManager manager) {
		manager.add(new Action(CVSUIMessages.TagSelectionDialog_0) { 
			@Override
			public void run() {
				CVSTag dateTag = NewDateTagAction.getDateTag(getShell(), getLocation());
				addDateTag(dateTag);
			}
		});
		if(getSelectedDateTagElement().length > 0){
			manager.add(new Action(CVSUIMessages.TagSelectionDialog_1) { 
				@Override
				public void run() {
					deleteDateTag();
				}
			});			
		}
	}

	protected void handleKeyReleased(KeyEvent event) {
	}
	
	/**
	 * handle a selection change event from the visible tag display
	 * (which could be either the table or the tree).
	 */
	protected void handleSelectionChange() {
	    CVSTag newSelection = internalGetSelectedTag();
	    if (selection != null && newSelection != null && selection.equals(newSelection)) {
	        // the selection hasn't change so return
	        return;
	    }
	    CVSTag oldSelection = selection;
	    selection = newSelection;
	    firePropertyChangeChange(SELECTED_TAG, oldSelection, selection);
	}
	
	private CVSTag internalGetSelectedTag() {
	    IStructuredSelection selection;
	    if (isTreeVisible()) {
	        selection = tagTree.getStructuredSelection();
	    } else {
	        selection = tagTable.getStructuredSelection();
	    }
		Object o = selection.getFirstElement();
		if (o instanceof TagElement)
		    return ((TagElement)o).getTag();
		return null;
	}
	
    private boolean isTreeVisible() {
        return treeVisible;
    }

    private ICVSRepositoryLocation getLocation(){
		return tagSource.getLocation();
	}
    public CVSTag getSelection() {
        return selection;
    }
    public Shell getShell() {
        return shell;
    }

    /**
     * Set the focus to the filter text widget
     */
    public void setFocus() {
        if (filterText != null)
            filterText.setFocus();
        else if (switcher != null)
            switcher.setFocus();
            
        // Refresh in case tags were added since the last time the area had focus
        refresh();
    }

    /**
     * Select the given tag
     * @param selectedTag the tag to be selected
     */
    public void setSelection(CVSTag selectedTag) {
        if (isTreeVisible())
			if (tagTree != null && !tagTree.getControl().isDisposed()) {
				// TODO: Hack to instantiate the model before revealing the selection
				tagTree.expandToLevel(2);
				tagTree.collapseAll();
				// Reveal the selection
				tagTree.reveal(new TagElement(selectedTag));
				tagTree.setSelection(new StructuredSelection(new TagElement(selectedTag)));
			}
		else
		    if (tagTable != null && !tagTable.getControl().isDisposed()) {
		        tagTable.setSelection(new StructuredSelection(new TagElement(selectedTag)));
		    }
    }

    /**
     * Refresh the state of the tag selection area
     */
    public void refresh() {
        if (isTreeVisible()) {
            if (tagTree != null && !tagTree.getControl().isDisposed()) {
                tagTree.refresh();
            }
        } else {
	        if (tagTable != null && !tagTable.getControl().isDisposed()) {
	            tagTable.refresh();
	        }
        }
    }
    
    public void refreshTagList() {
    	tagRefreshArea.refresh(true);
    }

    /**
     * Set the enablement state of the area
     * @param enabled the enablement state
     */
    public void setEnabled(boolean enabled) {
        if (filterText != null)
            filterText.setEnabled(enabled);
        tagTree.getControl().setEnabled(enabled);
        tagTable.getControl().setEnabled(enabled);
    }
    
    /**
     * Set the tag source from which the displayed tags are determined
     * @param tagSource the source of the tags being displayed
     */
    public void setTagSource(TagSource tagSource) {
        if (this.tagSource != null) {
            this.tagSource.removeListener(listener);
        }
        this.tagSource = tagSource;
        this.tagSource.addListener(listener);
        tagRefreshArea.setTagSource(this.tagSource);
        setTreeAndTableInput();
    }

    private void setTreeAndTableInput() {
        if (tagTree != null) {
            tagTree.setInput(createUnfilteredInput());
        }
        if (tagTable != null) {
            tagTable.setInput(createFilteredInput());
        }
        
    }

    /**
     * Set whether the input filter text is to be included in the tag selection area.
     * If excluded, clientscan still set the filter text directly using
     * <code>setFilter</code>.
     * @param include whether filter text input should be included
     */
    public void setIncludeFilterInputArea(boolean include) {
        includeFilterInputArea = include;
    }
    
    /**
     * Return whether the input filter text is to be included in the tag selection area.
     * If excluded, clientscan still set the filter text directly using
     * <code>setFilter</code>.
     * @return whether filter text input should be included
     */
    public boolean isIncludeFilterInputArea() {
        return includeFilterInputArea;
    }

    /**
     * Set the text used to filter the tag list.
     * @param filter the filter pattern
     */
    public void setFilter(String filter) {
        this.filterPattern = filter;
        updateTagDisplay(false);
    }
    
    public void setRunnableContext(IRunnableContext context) {
        this.context = context;
        if (tagRefreshArea != null)
            tagRefreshArea.setRunnableContext(context);
    }
}
