/*******************************************************************************
 * Copyright (c) 2000, 2018 IBM Corporation 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:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.ui.internal.console;

import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.eclipse.core.runtime.Assert;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.DocumentEvent;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IDocumentAdapter;
import org.eclipse.jface.text.IDocumentListener;
import org.eclipse.swt.custom.TextChangeListener;
import org.eclipse.swt.custom.TextChangedEvent;
import org.eclipse.swt.custom.TextChangingEvent;

/**
 * Adapts a Console's document to the viewer StyledText widget. Allows proper line
 * wrapping of fixed width consoles without having to add line delimiters to the StyledText.
 *
 * By using this adapter, the offset of any character is the same in both the widget and the
 * document.
 *
 * @since 3.1
 */
public class ConsoleDocumentAdapter implements IDocumentAdapter, IDocumentListener {

	private int consoleWidth = -1;
	private List<TextChangeListener> textChangeListeners;
	private IDocument document;

	int[] offsets = new int[5000];
	int[] lengths = new int[5000];
	private int regionCount = 1;
	private Pattern pattern = Pattern.compile("$", Pattern.MULTILINE); //$NON-NLS-1$


	public ConsoleDocumentAdapter(int width) {
		textChangeListeners = new ArrayList<>();
		consoleWidth = width;
	}

	/*
	 * repairs lines list from the beginning of the line containing the offset of any
	 * DocumentEvent, to the end of the Document.
	 */
	private void repairLines(int eventOffset) {
		if (document == null) {
			return;
		}
		try {
			int docLine = document.getLineOfOffset(eventOffset);
			int docLineOffset = document.getLineOffset(docLine);
			int widgetLine = getLineAtOffset(docLineOffset);

			for (int i=regionCount-1; i>=widgetLine; i--) {
				regionCount--;
			}

			int numLinesInDoc = document.getNumberOfLines();

			int nextOffset =  document.getLineOffset(docLine);
			for (int i = docLine; i<numLinesInDoc; i++) {
				int offset = nextOffset;
				int length = document.getLineLength(i);
				nextOffset += length;

				if (length == 0) {
					addRegion(offset, 0);
				} else {
					while (length > 0) {
						int trimmedLength = length;
						String lineDelimiter = document.getLineDelimiter(i);
						int lineDelimiterLength = 0;
						if (lineDelimiter != null) {
							lineDelimiterLength = lineDelimiter.length();
							trimmedLength -= lineDelimiterLength;
						}

						if (consoleWidth > 0 && consoleWidth < trimmedLength) {
							addRegion(offset, consoleWidth);
							offset += consoleWidth;
							length -= consoleWidth;
						} else {
							addRegion(offset, length);
							offset += length;
							length -= length;
						}
					}
				}
			}
		} catch (BadLocationException e) {
		}

		if (regionCount == 0) {
			addRegion(0, document.getLength());
		}
	}

	private void addRegion(int offset, int length) {
		if (regionCount == 0) {
			offsets[0] = offset;
			lengths[0] = length;
		} else {
			if (regionCount == offsets.length) {
				growRegionArray(regionCount * 2);
			}
			offsets[regionCount] = offset;
			lengths[regionCount] = length;
		}
		regionCount++;
	}

	@Override
	public void setDocument(IDocument doc) {
		if (document != null) {
			document.removeDocumentListener(this);
		}

		document = doc;

		if (document != null) {
			document.addDocumentListener(this);
			repairLines(0);
		}
	}

	@Override
	public synchronized void addTextChangeListener(TextChangeListener listener) {
		Assert.isNotNull(listener);
		if (!textChangeListeners.contains(listener)) {
			textChangeListeners.add(listener);
		}
	}

	@Override
	public synchronized void removeTextChangeListener(TextChangeListener listener) {
		if(textChangeListeners != null) {
			Assert.isNotNull(listener);
			textChangeListeners.remove(listener);
		}
	}

	@Override
	public int getCharCount() {
		return document.getLength();
	}

	@Override
	public String getLine(int lineIndex) {
		try {
			StringBuffer line = new StringBuffer(document.get(offsets[lineIndex], lengths[lineIndex]));
			int index = line.length() - 1;
			while(index > -1 && (line.charAt(index)=='\n' || line.charAt(index)=='\r')) {
				index--;
			}
			return new String(line.substring(0, index+1));
		} catch (BadLocationException e) {
		}
		return ""; //$NON-NLS-1$
	}

	@Override
	public int getLineAtOffset(int offset) {
		if (offset == 0 || regionCount <= 1) {
			return 0;
		}

		if (offset == document.getLength()) {
			return regionCount-1;
		}

		int left= 0;
		int right= regionCount-1;
		int midIndex = 0;

		while (left <= right) {
			if(left == right) {
				return right;
			}
			midIndex = (left + right) / 2;

			if (offset < offsets[midIndex]) {
				right = midIndex;
			} else if (offset >= offsets[midIndex] + lengths[midIndex]) {
				left = midIndex + 1;
			} else {
				return midIndex;
			}
		}

		return midIndex;
	}

	@Override
	public int getLineCount() {
		return regionCount;
	}

	@Override
	public String getLineDelimiter() {
		return System.getProperty("line.separator"); //$NON-NLS-1$
	}

	@Override
	public int getOffsetAtLine(int lineIndex) {
		return offsets[lineIndex];
	}

	@Override
	public String getTextRange(int start, int length) {
		try {
			return document.get(start, length);
		} catch (BadLocationException e) {
		}
		return null;
	}

	@Override
	public void replaceTextRange(int start, int replaceLength, String text) {
		try {
			document.replace(start, replaceLength, text);
		} catch (BadLocationException e) {
		}
	}

	@Override
	public synchronized void setText(String text) {
		TextChangedEvent changeEvent = new TextChangedEvent(this);
		for (TextChangeListener listener : textChangeListeners) {
			listener.textSet(changeEvent);
		}
	}

	@Override
	public synchronized void documentAboutToBeChanged(DocumentEvent event) {
		if (document == null) {
			return;
		}

		TextChangingEvent changeEvent = new TextChangingEvent(this);
		changeEvent.start = event.fOffset;
		changeEvent.newText = (event.fText == null ? "" : event.fText); //$NON-NLS-1$
		changeEvent.replaceCharCount = event.fLength;
		changeEvent.newCharCount = (event.fText == null ? 0 : event.fText.length());

		int first = getLineAtOffset(event.fOffset);
		int lOffset = Math.max(event.fOffset + event.fLength - 1, 0);
		int last = getLineAtOffset(lOffset);
		changeEvent.replaceLineCount = Math.max(last - first, 0);

		int newLineCount = countNewLines(event.fText);
		changeEvent.newLineCount = newLineCount >= 0 ? newLineCount : 0;

		if (changeEvent.newLineCount > offsets.length-regionCount) {
			growRegionArray(changeEvent.newLineCount);
		}

		for (TextChangeListener listener : textChangeListeners) {
			listener.textChanging(changeEvent);
		}
	}

	private void growRegionArray(int minSize) {
		int size = Math.max(offsets.length*2, minSize*2);
		int[] newOffsets = new int[size];
		System.arraycopy(offsets, 0, newOffsets, 0, regionCount);
		offsets = newOffsets;
		int[] newLengths = new int[size];
		System.arraycopy(lengths, 0, newLengths, 0, regionCount);
		lengths = newLengths;
	}

	private int countNewLines(String string) {
		int count = 0;

		if (string.length() == 0) {
			return 0;
		}

		// work around to
		// http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4994840
		// see bug 84641
		int offset = string.length() - 1;
		while (offset > -1 && string.charAt(offset) == '\r') {
			offset--;
			count++;
		}
		// if offset == -1, the line was all '\r' and there is no string to search for matches (bug 207743)
		if (offset > -1) {
			String str = string;
			if (offset < (str.length() - 1)) {
				str = str.substring(0, offset);
			}

			int lastIndex = 0;
			int index = 0;

			Matcher matcher = pattern.matcher(str);

			while (matcher.find()) {
				index = matcher.start();

				if (index == 0) {
					count++;
				} else if (index != str.length()) {
					count++;
				}

				if (consoleWidth > 0) {
					int lineLen = index - lastIndex + 1;
					if (index == 0) {
						lineLen += lengths[regionCount-1];
					}
					count += lineLen/consoleWidth;
				}

				lastIndex = index;
			}
		}
		return count;
	}


	@Override
	public synchronized void documentChanged(DocumentEvent event) {
		if (document == null) {
			return;
		}

		repairLines(event.fOffset);

		TextChangedEvent changeEvent = new TextChangedEvent(this);

		for (TextChangeListener listener : textChangeListeners) {
			listener.textChanged(changeEvent);
		}
	}

	/**
	 * sets consoleWidth, repairs line information, then fires event to the viewer text widget.
	 * @param width The console's width
	 */
	public void setWidth(int width) {
		if (width != consoleWidth) {
			consoleWidth = width;
			repairLines(0);
			TextChangedEvent changeEvent = new TextChangedEvent(this);
			for (TextChangeListener listener : textChangeListeners) {
				listener.textSet(changeEvent);
			}
		}
	}
}
