/*******************************************************************************
 * Copyright (c) 2007, 2018 Alphonse Van Assche and others.
 *
 * This program and the accompanying materials are made
 * available under the terms of the Eclipse Public License 2.0
 * which is available at https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *    Alphonse Van Assche - initial API and implementation
 *******************************************************************************/
package org.eclipse.linuxtools.internal.rpm.ui.editor;

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;

import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.ITextSelection;
import org.eclipse.jface.text.Position;
import org.eclipse.jface.text.source.Annotation;
import org.eclipse.jface.text.source.IAnnotationModel;
import org.eclipse.jface.text.source.ISourceViewer;
import org.eclipse.jface.viewers.IPostSelectionProvider;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.ISelectionChangedListener;
import org.eclipse.jface.viewers.SelectionChangedEvent;
import org.eclipse.linuxtools.rpm.ui.editor.parser.Specfile;
import org.eclipse.linuxtools.rpm.ui.editor.parser.SpecfileDefine;

public class RpmMacroOccurrencesUpdater implements ISelectionChangedListener {

	private static final String ANNOTATION_TYPE = Activator.PLUGIN_ID + ".highlightannotation"; //$NON-NLS-1$

	private final SpecfileEditor fEditor;

	private final List<Annotation> fOldAnnotations = new LinkedList<>();

	/**
	 * Creates a new instance on editor <code>specfileEditor</code>.
	 *
	 * @param specfileEditor The editor to mark occurrences on.
	 */
	public RpmMacroOccurrencesUpdater(SpecfileEditor specfileEditor) {
		((IPostSelectionProvider) specfileEditor.getSelectionProvider()).addPostSelectionChangedListener(this);
		fEditor = specfileEditor;
	}

	@Override
	public void selectionChanged(SelectionChangedEvent event) {
		update((ISourceViewer) event.getSource());
	}

	/**
	 * Updates the drawn annotations.
	 *
	 * @param viewer The viewer to get the document and annotation model from
	 */
	public void update(ISourceViewer viewer) {
		try {
			IDocument document = viewer.getDocument();
			IAnnotationModel model = viewer.getAnnotationModel();
			if (document == null || model == null) {
				return;
			}
			removeOldAnnotations(model);
			String currentSelectedWord = getWordAtSelection(fEditor.getSelectionProvider().getSelection(), document);
			if (isMacro(currentSelectedWord)) {
				Specfile spec = fEditor.getSpecfile();
				SpecfileDefine define = spec.getDefine(currentSelectedWord);
				String word = currentSelectedWord + ": "; //$NON-NLS-1$
				if (define != null) {
					word += define.getStringValue();
				} else {
					// If there's no such define we try to see if it corresponds
					// to
					// a Source or Patch declaration
					String retrivedValue = RPMUtils.getSourceOrPatchValue(spec, currentSelectedWord.toLowerCase());
					if (retrivedValue != null) {
						word += retrivedValue;
					} else {
						// If it does not correspond to a Patch or Source macro,
						// try to find it
						// in the macro proposals list.
						retrivedValue = RPMUtils.getMacroValueFromMacroList(currentSelectedWord);
						if (retrivedValue != null) {
							word += retrivedValue;
						}
					}
				}
				createNewAnnotations(currentSelectedWord, word, document, model);
			}
		} catch (BadLocationException e) {
			SpecfileLog.logError(e);
		}
	}

	/**
	 * Removes the previous set of annotations from the annotation model.
	 *
	 * @param model the annotation model
	 */
	private void removeOldAnnotations(IAnnotationModel model) {
		for (Annotation annotation : fOldAnnotations) {
			model.removeAnnotation(annotation);
		}
		fOldAnnotations.clear();
	}

	/**
	 * Checks if <code>word</code> is an macro.
	 *
	 * @param word the word to check
	 *
	 * @return <code>true</code> if <code>word</code> is an macro,
	 *         <code>false</code> otherwise
	 */
	private boolean isMacro(String word) {
		List<SpecfileDefine> defines = getMacros();
		if (word.length() > 0) {
			for (SpecfileDefine define : defines) {
				if (containsWord(define, word)) {
					return true;
				}
			}
			if (Activator.getDefault().getRpmMacroList().getProposals("%" + word).size() > 0) {//$NON-NLS-1$
				return true;
			}
		}
		return false;
	}

	/**
	 * Retrieves the macros from the editor's specfile.
	 *
	 * @return the macros from the editor's specfile
	 */
	private List<SpecfileDefine> getMacros() {
		Specfile specfile = fEditor.getSpecfile();
		if (specfile != null) {
			List<SpecfileDefine> macros = specfile.getDefines();
			if (macros != null) {
				return macros;
			}
		}
		return new ArrayList<>();
	}

	/**
	 * Returns <code>true</code> if <code>macro</code> equals the word
	 * <code>current</code>.
	 *
	 * @param macro   the <code>macro</code> to check
	 * @param current the word to look for
	 *
	 * @return <code>true</code> if <code>macro</code> contains the word
	 *         <code>current</code>,<code>false</code> if not
	 */
	private boolean containsWord(SpecfileDefine macro, String current) {
		return macro.getName().equalsIgnoreCase(current);
	}

	/**
	 * Returns the word at the current selection / caret position.
	 *
	 * @param selection the selection
	 * @param document  the document
	 * @return the currently selected text, or the word at the caret if the
	 *         selection has length 0
	 * @throws BadLocationException if accessing the document fails
	 */
	private String getWordAtSelection(ISelection selection, IDocument document) throws BadLocationException {
		String word;
		if (selection instanceof ITextSelection) {
			ITextSelection ts = (ITextSelection) selection;
			int offset = ts.getOffset();
			int end = offset + ts.getLength();

			// non-empty selections
			if (end != offset) {
				word = ts.getText();
			} else {
				while (offset > 0 && isDefineChar(document.getChar(offset - 1))) {
					offset--;
				}
				while (end < document.getLength() && isDefineChar(document.getChar(end))) {
					end++;
				}

				word = document.get(offset, end - offset);
			}
		} else {
			word = ""; //$NON-NLS-1$
		}
		return word.toLowerCase();
	}

	private boolean isDefineChar(char c) {
		return c != '{' && c != '}' && c != '?' && !Character.isWhitespace(c);
	}

	/**
	 * Adds an annotation for every occurrence of <code>macro</code> in the
	 * document. Also stores the created annotations in
	 * <code>fOldAnnotations</code>.
	 *
	 * @param macro    the word to look for
	 * @param document the document
	 * @param model    the annotation model
	 */
	private void createNewAnnotations(String macro, String hoverContent, IDocument document, IAnnotationModel model) {
		String content = document.get().toLowerCase();
		int idx = content.indexOf(macro.toLowerCase());
		while (idx != -1) {
			Annotation annotation = new Annotation(ANNOTATION_TYPE, false, hoverContent);
			Position position = new Position(idx, macro.length());
			model.addAnnotation(annotation, position);
			fOldAnnotations.add(annotation);
			idx = content.indexOf(macro, idx + 1);
		}
	}

	public void dispose() {
		((IPostSelectionProvider) fEditor.getSelectionProvider()).removePostSelectionChangedListener(this);
	}
}
