/*******************************************************************************
 * Copyright (c) 2007, 2015 Wind River Systems and others.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     Wind River Systems - initial API and implementation
 *******************************************************************************/
package org.eclipse.cdt.dsf.debug.internal.ui.disassembly.presentation;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.eclipse.cdt.internal.ui.editor.SemanticHighlightings;
import org.eclipse.cdt.ui.PreferenceConstants;
import org.eclipse.cdt.ui.text.ICPartitions;
import org.eclipse.cdt.ui.text.IColorManager;
import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.jface.preference.PreferenceConverter;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.ITypedRegion;
import org.eclipse.jface.text.TextAttribute;
import org.eclipse.jface.text.TextPresentation;
import org.eclipse.jface.text.rules.DefaultDamagerRepairer;
import org.eclipse.jface.text.rules.IToken;
import org.eclipse.jface.text.rules.ITokenScanner;
import org.eclipse.jface.util.PropertyChangeEvent;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.StyleRange;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.RGB;

/**
 *
 */
public class SourceTagDamagerRepairer extends DefaultDamagerRepairer implements ISourceTagListener {

	private ISourceTagProvider fSourceTagProvider;
	private Map<String, ITokenScanner> fScannerMap = new HashMap<>();
	private List<ISourceTag> fSourceTags = new ArrayList<>();
	private IColorManager fColorManager;
	private IPreferenceStore fPreferenceStore;
	private Map<String, TextAttribute> fAttributeMap = new HashMap<>();

	private final static String[] KEYS = { SemanticHighlightings.CLASS, SemanticHighlightings.METHOD_DECLARATION,
			SemanticHighlightings.FUNCTION_DECLARATION, SemanticHighlightings.FIELD,
			SemanticHighlightings.GLOBAL_VARIABLE, SemanticHighlightings.TYPEDEF,
			SemanticHighlightings.MACRO_DEFINITION, SemanticHighlightings.ENUMERATOR, SemanticHighlightings.ENUM, };

	/**
	 * @param scanner
	 * @param sourceTagProvider
	 */
	public SourceTagDamagerRepairer(ITokenScanner scanner, ISourceTagProvider sourceTagProvider,
			IColorManager colorManager, IPreferenceStore store) {
		super(scanner);
		fSourceTagProvider = sourceTagProvider;
		fColorManager = colorManager;
		fPreferenceStore = store;
		fDefaultTextAttribute = new TextAttribute(null, null, SWT.NORMAL);
		if (fSourceTagProvider != null) {
			fSourceTagProvider.addSourceTagListener(this);
			sourceTagsChanged(fSourceTagProvider);
		}
	}

	private void initTextAttributes() {
		boolean shEnabled = fPreferenceStore.getBoolean(PreferenceConstants.EDITOR_SEMANTIC_HIGHLIGHTING_ENABLED);
		for (int i = 0; i < KEYS.length; i++) {
			String enabledKey = PreferenceConstants.EDITOR_SEMANTIC_HIGHLIGHTING_PREFIX + KEYS[i]
					+ PreferenceConstants.EDITOR_SEMANTIC_HIGHLIGHTING_ENABLED_SUFFIX;
			String colorKey = PreferenceConstants.EDITOR_SEMANTIC_HIGHLIGHTING_PREFIX + KEYS[i]
					+ PreferenceConstants.EDITOR_SEMANTIC_HIGHLIGHTING_COLOR_SUFFIX;
			boolean enabled = shEnabled && fPreferenceStore.getBoolean(enabledKey);
			if (enabled) {
				String boldKey = PreferenceConstants.EDITOR_SEMANTIC_HIGHLIGHTING_PREFIX + KEYS[i]
						+ PreferenceConstants.EDITOR_SEMANTIC_HIGHLIGHTING_BOLD_SUFFIX;
				String italicKey = PreferenceConstants.EDITOR_SEMANTIC_HIGHLIGHTING_PREFIX + KEYS[i]
						+ PreferenceConstants.EDITOR_SEMANTIC_HIGHLIGHTING_ITALIC_SUFFIX;
				String strikethroughKey = PreferenceConstants.EDITOR_SEMANTIC_HIGHLIGHTING_PREFIX + KEYS[i]
						+ PreferenceConstants.EDITOR_SEMANTIC_HIGHLIGHTING_STRIKETHROUGH_SUFFIX;
				String underlineKey = PreferenceConstants.EDITOR_SEMANTIC_HIGHLIGHTING_PREFIX + KEYS[i]
						+ PreferenceConstants.EDITOR_SEMANTIC_HIGHLIGHTING_UNDERLINE_SUFFIX;
				addTextAttribute(KEYS[i], colorKey, boldKey, italicKey, strikethroughKey, underlineKey);
			} else {
				removeTextAttribute(KEYS[i], colorKey);
			}
		}
	}

	private void removeTextAttribute(String key, String colorKey) {
		if (fColorManager != null && colorKey != null) {
			Color color = fColorManager.getColor(colorKey);
			if (color != null) {
				fColorManager.unbindColor(colorKey);
			}
		}

		fAttributeMap.remove(key);
	}

	private void addTextAttribute(String key, String colorKey, String boldKey, String italicKey,
			String strikethroughKey, String underlineKey) {
		if (fColorManager != null && colorKey != null) {
			RGB rgb = PreferenceConverter.getColor(fPreferenceStore, colorKey);
			Color color = fColorManager.getColor(colorKey);
			if (color == null || !rgb.equals(color.getRGB())) {
				fColorManager.unbindColor(colorKey);
				fColorManager.bindColor(colorKey, rgb);
			}
		}

		TextAttribute textAttribute = createTextAttribute(colorKey, boldKey, italicKey, strikethroughKey, underlineKey);
		fAttributeMap.put(key, textAttribute);
	}

	private TextAttribute createTextAttribute(String colorKey, String boldKey, String italicKey,
			String strikethroughKey, String underlineKey) {
		Color color = null;
		if (colorKey != null)
			color = fColorManager.getColor(colorKey);

		int style = fPreferenceStore.getBoolean(boldKey) ? SWT.BOLD : SWT.NORMAL;
		if (fPreferenceStore.getBoolean(italicKey))
			style |= SWT.ITALIC;

		if (fPreferenceStore.getBoolean(strikethroughKey))
			style |= TextAttribute.STRIKETHROUGH;

		if (fPreferenceStore.getBoolean(underlineKey))
			style |= TextAttribute.UNDERLINE;

		return new TextAttribute(color, null, style);
	}

	/**
	 * Set scanner for contentType.
	 * @param contentType
	 * @param scanner
	 */
	public void setScanner(String contentType, ITokenScanner scanner) {
		fScannerMap.put(contentType, scanner);
	}

	/*
	 * @see org.eclipse.jface.text.presentation.IPresentationRepairer#createPresentation(org.eclipse.jface.text.TextPresentation, org.eclipse.jface.text.ITypedRegion)
	 */
	@Override
	public void createPresentation(TextPresentation presentation, ITypedRegion region) {
		if (fAttributeMap.isEmpty()) {
			initTextAttributes();
		}
		String contentType = region.getType();
		fScanner = fScannerMap.get(contentType);
		if (!contentType.equals(IDocument.DEFAULT_CONTENT_TYPE) && !contentType.equals(ICPartitions.C_PREPROCESSOR)) {
			super.createPresentation(presentation, region);
			return;
		}
		if (fScanner == null) {
			return;
		}

		int lastStart = region.getOffset();
		int regionEnd = lastStart + region.getLength();
		int length = 0;

		int sourceTagCount = fSourceTags.size();
		int sourceTagIdx = 0;

		ISourceTag sourceTag = null;
		ISourceRange range = null;
		int sourceTagStart = 0;
		int sourceTagEnd = 0;

		if (sourceTagCount > 0 && fDocument.getLength() > 0) {
			int left = 0;
			int mid = (int) (sourceTagCount * ((float) lastStart / fDocument.getLength()));
			int right = sourceTagCount - 1;
			while (true) {
				sourceTag = fSourceTags.get(mid);
				range = sourceTag.getRangeOfIdentifier();
				sourceTagStart = range.getBeginOffset();
				sourceTagEnd = range.getEndOffset() + 1;
				if (mid == left) {
					break;
				} else if (mid < right && sourceTagEnd < lastStart) {
					left = mid;
					mid = (mid + right) / 2;
				} else if (sourceTagStart >= regionEnd) {
					right = mid;
					mid = (left + mid) / 2;
				} else if (sourceTagStart > lastStart) {
					--mid;
					right = mid;
				} else {
					break;
				}
			}
			// set to next index
			sourceTagIdx = mid + 1;
		}

		TextAttribute lastAttribute = fDefaultTextAttribute;

		fScanner.setRange(fDocument, lastStart, region.getLength());

		while (true) {
			IToken token = fScanner.nextToken();

			// if the attribute is the same as the previous, extend range and continue
			TextAttribute attribute = getTokenTextAttribute(token);
			int tokenLength = fScanner.getTokenLength();
			if (tokenLength > 0
					&& (lastAttribute == attribute || lastAttribute != null && lastAttribute.equals(attribute))) {
				length += tokenLength;
				continue;
			}
			// attribute has changed, now add the style range
			while (sourceTag != null && length > 0) {
				if (sourceTagStart >= regionEnd) {
					// we are past the region boundary -> no more source tags
					sourceTag = null;
					break;
				}
				if (sourceTagStart >= lastStart) {
					if (sourceTagStart < lastStart + length) {
						String sourceTagStyle = getSourceTagStyle(sourceTag.getStyleCode());
						if (sourceTagStyle != null) {
							if (sourceTagStart > lastStart) {
								addRange(presentation, lastStart, Math.min(sourceTagStart - lastStart, length),
										lastAttribute);
							}
							int rangeEnd = Math.min(sourceTagEnd, regionEnd);
							addRange(presentation, sourceTagStart, rangeEnd - sourceTagStart,
									getSourceTagTextAttribute(sourceTagStyle));
							length = lastStart + length - rangeEnd;
							lastStart = rangeEnd;
						} else {
							fSourceTags.remove(--sourceTagIdx);
							--sourceTagCount;
						}
					} else {
						break;
					}
				}
				sourceTag = sourceTagIdx < sourceTagCount ? fSourceTags.get(sourceTagIdx++) : null;
				if (sourceTag != null) {
					range = sourceTag.getRangeOfIdentifier();
					sourceTagStart = range.getBeginOffset();
					sourceTagEnd = range.getEndOffset() + 1;
				}
			}
			if (token.isEOF()) {
				break;
			}

			if (length > 0) {
				addRange(presentation, lastStart, length, lastAttribute);
				lastAttribute = attribute;
				lastStart = fScanner.getTokenOffset();
				length = tokenLength;
			} else {
				lastAttribute = attribute;
				length = fScanner.getTokenOffset() - lastStart + tokenLength;
			}

		}

		addRange(presentation, lastStart, length, lastAttribute);
	}

	/**
	 * @param sourceTagStyle
	 * @return
	 */
	private TextAttribute getSourceTagTextAttribute(String sourceTagStyle) {
		return fAttributeMap.get(sourceTagStyle);
	}

	/**
	 * Get the style id for a source tag style code.
	 * @param styleCode
	 * @return the associated style id or <code>null</code>
	 */
	private String getSourceTagStyle(int styleCode) {
		switch (styleCode) {
		case ISourceTag.STYLE_None:
			return null;
		case ISourceTag.STYLE_Class:
			return SemanticHighlightings.CLASS;
		case ISourceTag.STYLE_Struct:
			return SemanticHighlightings.CLASS;
		case ISourceTag.STYLE_Union:
			return SemanticHighlightings.CLASS;
		case ISourceTag.STYLE_Function:
			return SemanticHighlightings.FUNCTION_DECLARATION;
		case ISourceTag.STYLE_Method:
			return SemanticHighlightings.METHOD_DECLARATION;
		case ISourceTag.STYLE_Variable:
			return SemanticHighlightings.GLOBAL_VARIABLE;
		case ISourceTag.STYLE_MemberVariable:
			return SemanticHighlightings.FIELD;
		case ISourceTag.STYLE_Enumerator:
			return SemanticHighlightings.ENUMERATOR;
		case ISourceTag.STYLE_Macro:
			return SemanticHighlightings.MACRO_DEFINITION;
		case ISourceTag.STYLE_Include:
			// include is colored by the scanner
			return null;
		case ISourceTag.STYLE_Enumeration:
			return SemanticHighlightings.ENUM;
		case ISourceTag.STYLE_Undefined:
			return null;
		case ISourceTag.STYLE_Typedef:
			return SemanticHighlightings.TYPEDEF;
		default:
			return null;
		}
	}

	@Override
	public void sourceTagsChanged(ISourceTagProvider sourceTagProvider) {
		fSourceTags.clear();
		if (sourceTagProvider != null) {
			sourceTagProvider.getSourceTags(fSourceTags);
			Collections.sort(fSourceTags, new Comparator<Object>() {
				@Override
				public int compare(Object o1, Object o2) {
					ISourceRange sr1 = ((ISourceTag) o1).getRangeOfIdentifier();
					ISourceRange sr2 = ((ISourceTag) o2).getRangeOfIdentifier();
					return (sr1.getBeginOffset() - sr2.getBeginOffset());
				}
			});
		}
	}

	/*
	 * @see org.eclipse.jface.text.rules.DefaultDamagerRepairer#addRange(org.eclipse.jface.text.TextPresentation, int, int, org.eclipse.jface.text.TextAttribute)
	 */
	@Override
	protected void addRange(TextPresentation presentation, int offset, int length, TextAttribute attr) {
		if (length > 0 && attr != null) {
			presentation.addStyleRange(
					new StyleRange(offset, length, attr.getForeground(), attr.getBackground(), attr.getStyle()));
		}
	}

	/**
	 * Test whether the given preference change affects us.
	 *
	 * @param event
	 * @return <code>true</code> if the given event affects the behavior.
	 */
	public boolean affectsBahvior(PropertyChangeEvent event) {
		return event.getProperty().startsWith(PreferenceConstants.EDITOR_SEMANTIC_HIGHLIGHTING_PREFIX);
	}

	/**
	 * Adapt to changes in the preferences.
	 *
	 * @param event
	 */
	public void handlePropertyChangeEvent(PropertyChangeEvent event) {
		initTextAttributes();
	}

}
