/*******************************************************************************
 * Copyright (c) 2001, 2009 IBM Corporation and others.
 * All rights reserved. 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:
 *     IBM Corporation - initial API and implementation
 *     Jens Lukowski/Innoopract - initial renaming/restructuring
 *     
 *******************************************************************************/
package org.eclipse.wst.sse.ui.internal.provisional.style;

import java.util.Collection;
import java.util.HashMap;

import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.jface.text.ITypedRegion;
import org.eclipse.jface.text.TextAttribute;
import org.eclipse.jface.text.source.ISourceViewer;
import org.eclipse.jface.util.IPropertyChangeListener;
import org.eclipse.jface.util.PropertyChangeEvent;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.StyleRange;
import org.eclipse.swt.graphics.RGB;
import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument;
import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocumentRegion;
import org.eclipse.wst.sse.core.internal.provisional.text.ITextRegion;
import org.eclipse.wst.sse.core.internal.provisional.text.ITextRegionCollection;
import org.eclipse.wst.sse.core.internal.provisional.text.ITextRegionList;
import org.eclipse.wst.sse.core.internal.util.Debug;
import org.eclipse.wst.sse.ui.internal.preferences.ui.ColorHelper;
import org.eclipse.wst.sse.ui.internal.util.EditorUtility;


public abstract class AbstractLineStyleProvider {
	private class PropertyChangeListener implements IPropertyChangeListener {
		/*
		 * (non-Javadoc)
		 * 
		 * @see org.eclipse.jface.util.IPropertyChangeListener#propertyChange(org.eclipse.jface.util.PropertyChangeEvent)
		 */
		public void propertyChange(PropertyChangeEvent event) {
			// have to do it this way so others can override the method
			handlePropertyChange(event);
		}
	}

	protected IStructuredDocument fDocument;
	protected Highlighter fHighlighter;
	private boolean fInitialized;
	protected PropertyChangeListener fPreferenceListener = new PropertyChangeListener();

	//private ISourceViewer fSourceViewer = null;
	protected ReconcilerHighlighter fRecHighlighter = null;
	
	/** Contains all text attributes pertaining to this line style provider */
	private HashMap fTextAttributes = null;

	// we keep track of LogMessage to avoid writing hundreds of messages,
	// but still give a hint that something is wrong with attributeProviders
	// and/or regions.
	// It's only written in the case of a program error, but there's no use
	// adding
	// salt to the wound.
	// private boolean wroteOneLogMessage;
	/**
	 */
	protected AbstractLineStyleProvider() {
	}

	/**
	 * Looks up the colorKey in the preference store and adds the style
	 * information to list of TextAttributes
	 * 
	 * @param colorKey
	 */
	protected void addTextAttribute(String colorKey) {
		if (getColorPreferences() != null) {
			String prefString = getColorPreferences().getString(colorKey);
			String[] stylePrefs = ColorHelper.unpackStylePreferences(prefString);
			if (stylePrefs != null) {
				RGB foreground = ColorHelper.toRGB(stylePrefs[0]);
				RGB background = ColorHelper.toRGB(stylePrefs[1]);
				boolean bold = Boolean.valueOf(stylePrefs[2]).booleanValue();
				boolean italic = Boolean.valueOf(stylePrefs[3]).booleanValue();
				boolean strikethrough = Boolean.valueOf(stylePrefs[4]).booleanValue();
				boolean underline = Boolean.valueOf(stylePrefs[5]).booleanValue();
				int style = SWT.NORMAL;
				if (bold) {
					style = style | SWT.BOLD;
				}
				if (italic) {
					style = style | SWT.ITALIC;
				}
				if (strikethrough) {
					style = style | TextAttribute.STRIKETHROUGH;
				}
				if (underline) {
					style = style | TextAttribute.UNDERLINE;
				}

				TextAttribute createTextAttribute = createTextAttribute(foreground, background, style);
				getTextAttributes().put(colorKey, createTextAttribute);
			}
		}
	}

	protected void commonInit(IStructuredDocument document, Highlighter highlighter) {

		fDocument = document;
		fHighlighter = highlighter;
	}

	/**
	 * this version does "trim" regions to match request
	 */
	private StyleRange createStyleRange(ITextRegionCollection flatNode, ITextRegion region, TextAttribute attr, int startOffset, int length) {
		int start = flatNode.getStartOffset(region);
		if (start < startOffset)
			start = startOffset;
		
		// Base the text end offset off of the, possibly adjusted, start
		int textEnd = start + region.getTextLength();
		int maxOffset = startOffset + length;
		
		int end = flatNode.getEndOffset(region);
		// Use the end of the text in the region to avoid applying background color to trailing whitespace
		if(textEnd < end)
			end = textEnd;
		// instead of end-start?
		if (end > maxOffset)
			end = maxOffset;
		StyleRange result = new StyleRange(start, end - start, attr.getForeground(), attr.getBackground(), attr.getStyle());
		if((attr.getStyle() & TextAttribute.STRIKETHROUGH) != 0) {
			result.strikeout = true;
		}
		if((attr.getStyle() & TextAttribute.UNDERLINE) != 0) {
			result.underline = true;
		}
		return result;

	}

	protected TextAttribute createTextAttribute(RGB foreground, RGB background, boolean bold) {
		return new TextAttribute((foreground != null) ? EditorUtility.getColor(foreground) : null, (background != null) ? EditorUtility.getColor(background) : null, bold ? SWT.BOLD : SWT.NORMAL);
	}

	protected TextAttribute createTextAttribute(RGB foreground, RGB background, int style) {
		return new TextAttribute((foreground != null) ? EditorUtility.getColor(foreground) : null, (background != null) ? EditorUtility.getColor(background) : null, style);
	}

	abstract protected TextAttribute getAttributeFor(ITextRegion region);
	
	protected TextAttribute getAttributeFor(ITextRegionCollection collection, ITextRegion region) {
		return getAttributeFor(region);
	}

	abstract protected IPreferenceStore getColorPreferences();

	protected IStructuredDocument getDocument() {
		return fDocument;
	}
	
	public void setDocument(IStructuredDocument document) {
		fDocument = document;
	}

	/**
	 */
	protected Highlighter getHighlighter() {
		return fHighlighter;
	}

	/**
	 * Returns the hashtable containing all the text attributes for this line
	 * style provider. Lazily creates a hashtable if one has not already been
	 * created.
	 * 
	 * @return
	 */
	protected HashMap getTextAttributes() {
		if (fTextAttributes == null) {
			fTextAttributes = new HashMap();
			loadColors();
		}
		return fTextAttributes;
	}

	protected void handlePropertyChange(PropertyChangeEvent event) {
		// force a full update of the text viewer
		if(fRecHighlighter != null)
			fRecHighlighter.refreshDisplay();
	}

	public void init(IStructuredDocument structuredDocument, Highlighter highlighter) {

		commonInit(structuredDocument, highlighter);

		if (isInitialized())
			return;

		registerPreferenceManager();

		setInitialized(true);
	}
	
	public void init(IStructuredDocument structuredDocument, ISourceViewer sourceViewer) {
		init(structuredDocument, (Highlighter) null);
	}
	
	public void init(IStructuredDocument structuredDocument, ReconcilerHighlighter highlighter)	{
		fDocument = structuredDocument;
		fRecHighlighter = highlighter;
		
		if(isInitialized())
			return;
		
		registerPreferenceManager();
		
		setInitialized(true);
	}

	/**
	 * @deprecated - left because it's public, but we aren't adapters any more
	 */
	public boolean isAdapterForType(java.lang.Object type) {
		return type == LineStyleProvider.class;
	}

	/**
	 * Returns the initialized.
	 * 
	 * @return boolean
	 */
	public boolean isInitialized() {
		return fInitialized;
	}
	
	abstract protected void loadColors();

	public boolean prepareRegions(ITypedRegion typedRegion, int lineRequestStart, int lineRequestLength, Collection holdResults) {
		final int partitionStartOffset = typedRegion.getOffset();
		final int partitionLength = typedRegion.getLength();
		IStructuredDocumentRegion structuredDocumentRegion = getDocument().getRegionAtCharacterOffset(partitionStartOffset);
		boolean handled = false;

		handled = prepareTextRegions(structuredDocumentRegion, partitionStartOffset, partitionLength, holdResults);

		return handled;
	}

	/**
	 * @param region
	 * @param start
	 * @param length
	 * @param holdResults
	 * @return
	 */
	private boolean prepareTextRegion(ITextRegionCollection blockedRegion, int partitionStartOffset, int partitionLength, Collection holdResults) {
		boolean handled = false;
		final int partitionEndOffset = partitionStartOffset + partitionLength - 1;
		ITextRegion region = null;
		ITextRegionList regions = blockedRegion.getRegions();
		int nRegions = regions.size();
		StyleRange styleRange = null;
		for (int i = 0; i < nRegions; i++) {
			region = regions.get(i);
			TextAttribute attr = null;
			TextAttribute previousAttr = null;
			if (blockedRegion.getStartOffset(region) > partitionEndOffset)
				break;
			if (blockedRegion.getEndOffset(region) <= partitionStartOffset)
				continue;

			if (region instanceof ITextRegionCollection) {
				handled = prepareTextRegion((ITextRegionCollection) region, partitionStartOffset, partitionLength, holdResults);
			} else {

				attr = getAttributeFor(blockedRegion, region);
				if (attr != null) {
					handled = true;
					// if this region's attr is the same as previous one, then
					// just adjust the previous style range
					// instead of creating a new instance of one
					// note: to use 'equals' in this case is important, since
					// sometimes
					// different instances of attributes are associated with a
					// region, even the
					// the attribute has the same values.
					// TODO: this needs to be improved to handle readonly
					// regions correctly
					if ((styleRange != null) && (previousAttr != null) && (previousAttr.equals(attr))) {
						styleRange.length += region.getLength();
					} else {
						styleRange = createStyleRange(blockedRegion, region, attr, partitionStartOffset, partitionLength);
						holdResults.add(styleRange);
						// technically speaking, we don't need to update
						// previousAttr
						// in the other case, because the other case is when
						// it hasn't changed
						previousAttr = attr;
					}
				} else {
					previousAttr = null;
				}
			}
		}
		return handled;
	}

	private boolean prepareTextRegions(IStructuredDocumentRegion structuredDocumentRegion, int partitionStartOffset, int partitionLength, Collection holdResults) {
		boolean handled = false;
		final int partitionEndOffset = partitionStartOffset + partitionLength - 1;
		while (structuredDocumentRegion != null && structuredDocumentRegion.getStartOffset() <= partitionEndOffset) {
			ITextRegion region = null;
			ITextRegionList regions = structuredDocumentRegion.getRegions();
			int nRegions = regions.size();
			StyleRange styleRange = null;
			for (int i = 0; i < nRegions; i++) {
				region = regions.get(i);
				TextAttribute attr = null;
				TextAttribute previousAttr = null;
				if (structuredDocumentRegion.getStartOffset(region) > partitionEndOffset)
					break;
				if (structuredDocumentRegion.getEndOffset(region) <= partitionStartOffset)
					continue;

				if (region instanceof ITextRegionCollection) {
					boolean handledCollection = (prepareTextRegion((ITextRegionCollection) region, partitionStartOffset, partitionLength, holdResults));
					handled = (!handled) ? handledCollection : handled;
				} else {

					attr = getAttributeFor(structuredDocumentRegion, region);
					if (attr != null) {
						handled = true;
						// if this region's attr is the same as previous one,
						// then just adjust the previous style range
						// instead of creating a new instance of one
						// note: to use 'equals' in this case is important,
						// since sometimes
						// different instances of attributes are associated
						// with a region, even the
						// the attribute has the same values.
						// TODO: this needs to be improved to handle readonly
						// regions correctly
						if ((styleRange != null) && (previousAttr != null) && (previousAttr.equals(attr))) {
							styleRange.length += region.getLength();
						} else {
							styleRange = createStyleRange(structuredDocumentRegion, region, attr, partitionStartOffset, partitionLength);
							holdResults.add(styleRange);
							// technically speaking, we don't need to update
							// previousAttr
							// in the other case, because the other case is
							// when it hasn't changed
							previousAttr = attr;
						}
					} else {
						previousAttr = null;
					}
				}

				if (Debug.syntaxHighlighting && !handled) {
					System.out.println("not handled in prepareRegions"); //$NON-NLS-1$
				}
			}
			structuredDocumentRegion = structuredDocumentRegion.getNext();
		}
		return handled;
	}

	protected void registerPreferenceManager() {
		IPreferenceStore pref = getColorPreferences();
		if (pref != null) {
			pref.addPropertyChangeListener(fPreferenceListener);
		}
	}

	public void release() {
		unRegisterPreferenceManager();
		if (fTextAttributes != null) {
			fTextAttributes.clear();
			fTextAttributes = null;
		}
		setInitialized(false);
	}

	/**
	 * Sets the initialized.
	 * 
	 * @param initialized
	 *            The initialized to set
	 */
	private void setInitialized(boolean initialized) {
		this.fInitialized = initialized;
	}

	protected void unRegisterPreferenceManager() {
		IPreferenceStore pref = getColorPreferences();
		if (pref != null) {
			pref.removePropertyChangeListener(fPreferenceListener);
		}
	}
}
