/**
 * Copyright (C) 2009 - 2019 <a href="https://www.wudsn.com" target="_top">Peter Dell</a>
 *
 * This file is part of WUDSN IDE.
 * 
 * WUDSN IDE is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 2 of the License, or
 * (at your option) any later version.
 * 
 * WUDSN IDE is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with WUDSN IDE.  If not, see <http://www.gnu.org/licenses/>.
 */

package com.wudsn.ide.asm.editor;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.QualifiedName;
import org.eclipse.jface.action.Action;
import org.eclipse.jface.action.IToolBarManager;
import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.jface.text.Position;
import org.eclipse.jface.text.TextSelection;
import org.eclipse.jface.viewers.ContentViewer;
import org.eclipse.jface.viewers.IBaseLabelProvider;
import org.eclipse.jface.viewers.ILabelProvider;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.SelectionChangedEvent;
import org.eclipse.jface.viewers.TreePath;
import org.eclipse.jface.viewers.TreeSelection;
import org.eclipse.jface.viewers.TreeViewer;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.jface.viewers.ViewerComparator;
import org.eclipse.swt.custom.BusyIndicator;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.ui.IEditorInput;
import org.eclipse.ui.plugin.AbstractUIPlugin;
import org.eclipse.ui.views.contentoutline.ContentOutlinePage;

import com.wudsn.ide.asm.AssemblerPlugin;
import com.wudsn.ide.asm.Texts;
import com.wudsn.ide.asm.compiler.parser.CompilerSourceFile;
import com.wudsn.ide.asm.compiler.parser.CompilerSourceParserTreeObject;
import com.wudsn.ide.asm.compiler.parser.CompilerSourceParserTreeObjectType;
import com.wudsn.ide.base.common.Profiler;
import com.wudsn.ide.base.common.RunnableWithLogging;

/**
 * Outline page for the assembler editor.
 * 
 * @author Peter Dell.
 * @author Andy Reek
 */
final class AssemblerContentOutlinePage extends ContentOutlinePage {

    /*
     * Toggle action to toggle the sorting in the outline tree. The state of the
     * action will be persisted along with the file in the editor. If there is
     * no state store yet along with in the file, the default is taken from the
     * last file which was opened.
     */
    private static final class OutlineViewerSortAction extends Action {
	private static final QualifiedName CHECKED = new QualifiedName("OutlineViewerSortAction", "Checked");

	final AssemblerEditor editor;
	final TreeViewer treeViewer;

	/**
	 * Creates a new sort action.
	 * 
	 * @param editor
	 *            The editor which holds the respective file, not
	 *            <code>null</code>.
	 * @param treeViewer
	 *            The tree viewer which displays the outline.
	 */
	public OutlineViewerSortAction(AssemblerEditor editor, TreeViewer treeViewer) {
	    super("", AS_CHECK_BOX);
	    if (editor == null) {
		throw new IllegalArgumentException("Parameter 'editor' must not be null.");
	    }
	    if (treeViewer == null) {
		throw new IllegalArgumentException("Parameter 'treeViewer' must not be null.");
	    }
	    setToolTipText(Texts.ASSEMBLER_CONTENT_OUTLINE_SORT_BUTTON_TOOL_TIP);
	    ImageDescriptor imageDescriptor = AbstractUIPlugin.imageDescriptorFromPlugin(AssemblerPlugin.ID,
		    "icons/outline-sort.gif");
	    setImageDescriptor(imageDescriptor);
	    this.editor = editor;
	    this.treeViewer = treeViewer;

	    String checkedProperty;
	    try {
		IFile iFile = editor.getCurrentIFile();
		if (iFile != null) {
		    checkedProperty = iFile.getPersistentProperty(CHECKED);
		} else {
		    checkedProperty = "";
		}
	    } catch (CoreException ignore) {
		checkedProperty = null;
	    }
	    if (checkedProperty == null) {
		checkedProperty = editor.getPlugin().getProperty(CHECKED);
	    }

	    boolean checked = Boolean.parseBoolean(checkedProperty);
	    setChecked(checked);

	}

	@Override
	public void run() {

	    // Get current state and update the UI.
	    boolean checked = isChecked();
	    setChecked(checked);

	    // Store the property.
	    String checkedProperty = Boolean.toString(checked);
	    try {
		IFile iFile = editor.getCurrentIFile();
		if (iFile != null) {
		    iFile.setPersistentProperty(CHECKED, checkedProperty);
		}
	    } catch (CoreException ex) {
		editor.getPlugin().logError("Cannot set property {0}", new Object[] { CHECKED }, ex);
	    }
	    editor.getPlugin().setProperty(CHECKED, checkedProperty);

	    // Refresh the tree viewer.
	    BusyIndicator.showWhile(treeViewer.getControl().getDisplay(), new RunnableWithLogging() {
		@Override
		protected void runWithLogging() {
		    treeViewer.refresh(false);
		}
	    });
	}
    }

    private static final class OutlineViewerComparator extends ViewerComparator {
	private final OutlineViewerSortAction sortAction;

	OutlineViewerComparator(OutlineViewerSortAction sortAction) {
	    if (sortAction == null) {
		throw new IllegalArgumentException("Parameter 'sortAction' must not be null.");
	    }
	    this.sortAction = sortAction;
	}

	@Override
	public int category(Object element) {
	    int result;
	    CompilerSourceParserTreeObject object = (CompilerSourceParserTreeObject) element;

	    // Treat equate definition and label definition as equal.
	    result = object.getType();

	    switch (object.getType()) {
	    case CompilerSourceParserTreeObjectType.DEFAULT:
	    case CompilerSourceParserTreeObjectType.DEFINITION_SECTION:
	    case CompilerSourceParserTreeObjectType.IMPLEMENTATION_SECTION:
		break;

	    case CompilerSourceParserTreeObjectType.EQUATE_DEFINITION:
		result = CompilerSourceParserTreeObjectType.EQUATE_DEFINITION;
		break;
	    case CompilerSourceParserTreeObjectType.LABEL_DEFINITION:
		result = CompilerSourceParserTreeObjectType.EQUATE_DEFINITION;
		break;

	    case CompilerSourceParserTreeObjectType.ENUM_DEFINITION_SECTION:
	    case CompilerSourceParserTreeObjectType.STRUCTURE_DEFINITION_SECTION:
	    case CompilerSourceParserTreeObjectType.LOCAL_SECTION:
	    case CompilerSourceParserTreeObjectType.MACRO_DEFINITION_SECTION:
	    case CompilerSourceParserTreeObjectType.PAGES_SECTION:
	    case CompilerSourceParserTreeObjectType.PROCEDURE_DEFINITION_SECTION:
	    case CompilerSourceParserTreeObjectType.REPEAT_SECTION:
		break;

	    case CompilerSourceParserTreeObjectType.SOURCE_INCLUDE:
	    case CompilerSourceParserTreeObjectType.BINARY_INCLUDE:
		break;

	    default:
		throw new RuntimeException("Element '" + object.getName() + "' has unknown type " + object.getType()
			+ ".");

	    }
	    return result;
	}

	@Override
	public int compare(Viewer viewer, Object e1, Object e2) {

	    if (!sortAction.isChecked()) {
		return 0;
	    }

	    int cat1 = category(e1);
	    int cat2 = category(e2);

	    // Never sort definition or implementation sections.
	    if (cat1 == CompilerSourceParserTreeObjectType.DEFINITION_SECTION
		    && cat2 == CompilerSourceParserTreeObjectType.DEFINITION_SECTION) {
		return 0;
	    }

	    if (cat1 != cat2) {
		return cat1 - cat2;
	    }

	    String name1;
	    String name2;

	    if (viewer == null || !(viewer instanceof ContentViewer)) {
		name1 = e1.toString();
		name2 = e2.toString();
	    } else {
		IBaseLabelProvider prov = ((ContentViewer) viewer).getLabelProvider();
		if (prov instanceof ILabelProvider) {
		    ILabelProvider lprov = (ILabelProvider) prov;
		    name1 = lprov.getText(e1);
		    name2 = lprov.getText(e2);
		} else {
		    name1 = e1.toString();
		    name2 = e2.toString();
		}
	    }
	    if (name1 == null) {
		name1 = "";//$NON-NLS-1$
	    }
	    if (name2 == null) {
		name2 = "";//$NON-NLS-1$
	    }

	    // Use direct comparison as identified are ASCII only.
	    return name1.compareTo(name2);

	}
    }

    /**
     * Editor updater for selection changes in the content outline page.
     */
    private final static class EditorUpdater extends RunnableWithLogging {
	private final Profiler profiler;

	private final AssemblerEditor editor;
	private final AssemblerContentOutlinePage outlinePage;
	private final TreeViewer viewer;
	private final AssemblerContentOutlineTreeContentProvider contentProvider;

	EditorUpdater(AssemblerEditor editor, AssemblerContentOutlinePage outlinePage, TreeViewer viewer) {
	    if (editor == null) {
		throw new IllegalArgumentException("Parameter 'editor' must not be null.");
	    }
	    if (outlinePage == null) {
		throw new IllegalArgumentException("Parameter 'outlinePage' must not be null.");
	    }
	    if (viewer == null) {
		throw new IllegalArgumentException("Parameter 'viewer' must not be null.");
	    }
	    this.editor = editor;
	    this.outlinePage = outlinePage;
	    this.viewer = viewer;
	    this.contentProvider = (AssemblerContentOutlineTreeContentProvider) viewer.getContentProvider();
	    profiler = new Profiler(this);

	}

	/**
	 * Triggers a new
	 * {@link AssemblerContentOutlineTreeContentProvider#parse} run and
	 * updates the display.
	 */
	@Override
	protected void runWithLogging() {
	    synchronized (outlinePage) {
		try {
		    outlinePage.inputUpdateCounter++;
		    runSynchronized();
		} finally {
		    outlinePage.inputUpdateCounter--;
		}
	    }
	}

	private void runSynchronized() {
	    // Stop drawing the control.
	    Control control = viewer.getControl();

	    // Check if this call is caused by closing the editor.
	    if (control.isDisposed()) {
		return;
	    }

	    profiler.begin("runSynchronized");

	    profiler.begin("updateOutline");
	    control.setRedraw(false);

	    // Remember the currently selected tree object in the content
	    // outline tree viewer.
	    ISelection selection = viewer.getSelection();
	    Object[] expandedElements = viewer.getExpandedElements();

	    // Trigger the the new parse run.
	    viewer.setInput(outlinePage.input);
	    // viewer.refresh(); Not required?

	    profiler.begin("expandElements");
	    if (expandedElements.length > 0) {
		viewer.setExpandedElements(expandedElements);
	    } else {
		viewer.expandToLevel(2);
	    }
	    profiler.end("expandElements");

	    restoreSelection(selection);
	    // Now that all changes are done, draw the control again.
	    control.setRedraw(true);
	    profiler.end("updateOutline");

	    // // Reselect the previous text selection in the editor.
	    // editor.getSelectionProvider().setSelection(textSelection);

	    CompilerSourceFile compilerSourceFile;
	    compilerSourceFile = contentProvider.getCompilerSourceFile();

	    // Update the identifiers to be highlighted
	    profiler.begin("updateIdentifiers");
	    editor.updateIdentifiers(compilerSourceFile);
	    profiler.end("updateIdentifiers");

	    // Update the folding structure.
	    profiler.begin("updateFoldingStructure");
	    List<Position> foldingPositions;
	    if (compilerSourceFile != null) {
		foldingPositions = compilerSourceFile.getFoldingPositions();
	    } else {
		foldingPositions = Collections.emptyList();
	    }
	    editor.updateFoldingStructure(foldingPositions);

	    profiler.end("updateFoldingStructure");

	    profiler.end("runSynchronized");

	}

	private void restoreSelection(ISelection selection) {
	    if (selection instanceof TreeSelection) {
		TreeSelection treeSelection = (TreeSelection) selection;
		TreePath[] selectedTreePaths = treeSelection.getPaths();
		List<TreePath> reselectedTreePaths = new ArrayList<TreePath>(selectedTreePaths.length);
		for (int i = 0; i < selectedTreePaths.length; i++) {
		    TreePath treePath = selectedTreePaths[i];
		    List<CompilerSourceParserTreeObject> treeObjects = contentProvider.getCompilerSourceFile()
			    .getSections();

		    List<Object> segments = new ArrayList<Object>(treePath.getSegmentCount());

		    for (int j = 0; j < treePath.getSegmentCount(); j++) {
			CompilerSourceParserTreeObject oldTreeObject;
			CompilerSourceParserTreeObject newTreeObject;
			oldTreeObject = (CompilerSourceParserTreeObject) treePath.getSegment(j);
			newTreeObject = null;

			for (int k = 0; newTreeObject == null && k < treeObjects.size(); k++) {
			    if (treeObjects.get(k).getTreePath().equals(oldTreeObject.getTreePath())) {
				newTreeObject = treeObjects.get(k);
				segments.add(newTreeObject);
				treeObjects = newTreeObject.getChildren();
			    }
			}

		    }
		    if (!segments.isEmpty()) {
			reselectedTreePaths.add(new TreePath(segments.toArray()));
		    }

		}

		TreePath[] reselectedTreePathsArray = new TreePath[reselectedTreePaths.size()];
		reselectedTreePaths.toArray(reselectedTreePathsArray);
		selection = new TreeSelection(reselectedTreePathsArray);

		// Reselect the previously selected tree object in the
		// content outline tree viewer.
		viewer.setSelection(selection);
	    }
	}
    }

    /**
     * The owning editor.
     */
    final AssemblerEditor editor;

    /**
     * The visual components.
     */
    private OutlineViewerSortAction treeViewerSortAction;
    private OutlineViewerComparator treeViewerComparator;

    /**
     * The current input.
     */
    IEditorInput input;
    int inputUpdateCounter;

    /**
     * Creates a new instance.
     * 
     * @param editor
     *            The assembler editor, not <code>null</code>.
     */
    AssemblerContentOutlinePage(AssemblerEditor editor) {
	if (editor == null) {
	    throw new IllegalArgumentException("Parameter 'editor' must not be null.");
	}
	this.editor = editor;
    }

    /**
     * Sets the input for the outline page.
     * 
     * @param input
     *            The new input, not <code>null</code>.
     */
    final void setInput(IEditorInput input) {
	if (input == null) {
	    throw new IllegalArgumentException("Parameter 'input' must not be null.");
	}
	this.input = input;

	runEditorUpdater();
    }

    private void runEditorUpdater() {
	final TreeViewer viewer = getTreeViewer();

	if ((viewer != null) && (viewer.getContentProvider() != null)) {
	    editor.getSite().getShell().getDisplay().asyncExec(new EditorUpdater(editor, this, viewer));
	}
    }

    /**
     * Create the control and configures it. See code of
     * org.eclipse.jdt.internal.ui.text.JavaOutlineInformationControl for
     * similar use case.
     * 
     * @param parent
     *            īThe parent, not <code>null</code>.
     */
    @Override
    public void createControl(Composite parent) {
	super.createControl(parent);

	TreeViewer treeViewer = getTreeViewer();

	// Configure the toolbar.
	treeViewerSortAction = new OutlineViewerSortAction(editor, treeViewer);
	treeViewerComparator = new OutlineViewerComparator(treeViewerSortAction);
	IToolBarManager toolBarManager = getSite().getActionBars().getToolBarManager();

	// Configure the content.
	treeViewer.setContentProvider(new AssemblerContentOutlineTreeContentProvider(this));
	treeViewer.setLabelProvider(new CompilerSourceParserTreeObjectLabelProvider());
	treeViewer.setComparator(treeViewerComparator);
	treeViewer.addSelectionChangedListener(this);

	toolBarManager.add(treeViewerSortAction);
	toolBarManager.update(true);

	if (input != null) {
	    runEditorUpdater();
	}

    }

    @Override
    public void selectionChanged(SelectionChangedEvent event) {
	super.selectionChanged(event);

	synchronized (this) {
	    if (inputUpdateCounter > 0) {
		return;
	    }
	}

	ISelection selection = event.getSelection();

	if (selection.isEmpty()) {
	    editor.resetHighlightRange();
	} else {
	    if (selection instanceof IStructuredSelection) {
		Object object = ((IStructuredSelection) selection).getFirstElement();

		if (object instanceof CompilerSourceParserTreeObject) {

		    CompilerSourceParserTreeObject treeObject;
		    AssemblerContentOutlineTreeContentProvider contentProvider;
		    contentProvider = (AssemblerContentOutlineTreeContentProvider) getTreeViewer().getContentProvider();
		    treeObject = (CompilerSourceParserTreeObject) object;

		    // If this is the tree object from another (source
		    // include) file, step off the tree to find the source
		    // include statement.
		    while (treeObject != null
			    && treeObject.getCompilerSourceFile() != contentProvider.getCompilerSourceFile()) {
			treeObject = treeObject.getParent();
		    }
		    if (treeObject != null) {
			try {
			    editor.setHighlightRange(treeObject.getStartOffset(), 0, true);
			    editor.getSelectionProvider().setSelection(
				    new TextSelection(treeObject.getStartOffset(), 1));
			} catch (IllegalArgumentException x) {
			    editor.resetHighlightRange();
			}
		    }

		}
	    }
	}
    }

    /**
     * Gets the compiler source file of the last parse process.
     * 
     * @return The compiler source file of the last parse process or
     *         <code>null</code>.
     */
    final CompilerSourceFile getCompilerSourceFile() {
	AssemblerContentOutlineTreeContentProvider contentProvider;
	contentProvider = (AssemblerContentOutlineTreeContentProvider) getTreeViewer().getContentProvider();
	CompilerSourceFile compilerSourceFile = contentProvider.getCompilerSourceFile();
	return compilerSourceFile;
    }

}
