/*******************************************************************************
 * Copyright (c) 2006, 2014 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.jdt.internal.compiler.parser;

import java.util.Arrays;
import org.eclipse.jdt.core.compiler.CharOperation;
import org.eclipse.jdt.core.compiler.InvalidInputException;

public class RecoveryScanner extends Scanner {
	public static final char[] FAKE_IDENTIFIER = "$missing$".toCharArray(); //$NON-NLS-1$

	private RecoveryScannerData data;

	private int[] pendingTokens;
	private int pendingTokensPtr = -1;
	private char[] fakeTokenSource = null;
	private boolean isInserted = true;
	private boolean precededByRemoved = false;
	private int skipNextInsertedTokens = -1;

	public boolean record = true;

	public RecoveryScanner(Scanner scanner, RecoveryScannerData data) {
		super(false,
				scanner.tokenizeWhiteSpace,
				scanner.checkNonExternalizedStringLiterals,
				scanner.sourceLevel,
				scanner.complianceLevel,
				scanner.taskTags,
				scanner.taskPriorities,
				scanner.isTaskCaseSensitive,
				scanner.previewEnabled);
		setData(data);
	}

	public RecoveryScanner(
			boolean tokenizeWhiteSpace,
			boolean checkNonExternalizedStringLiterals,
			long sourceLevel,
			long complianceLevel,
			char[][] taskTags,
			char[][] taskPriorities,
			boolean isTaskCaseSensitive,
			boolean isPreviewEnabled,
			RecoveryScannerData data) {
		super(false,
				tokenizeWhiteSpace,
				checkNonExternalizedStringLiterals,
				sourceLevel,
				complianceLevel,
				taskTags,
				taskPriorities,
				isTaskCaseSensitive,
				isPreviewEnabled);
		setData(data);
	}

	public void insertToken(int token, int completedToken, int position) {
		insertTokens(new int []{token}, completedToken, position);
	}

	private int[] reverse(int[] tokens) {
		int length = tokens.length;
		for(int i = 0, max = length / 2; i < max; i++) {
			int tmp = tokens[i];
			tokens[i] = tokens[length - i - 1];
			tokens[length - i - 1] = tmp;
		}
		return filterTokens(tokens);
	}
	public void insertTokens(int[] tokens, int completedToken, int position) {
		if(!this.record) return;
		tokens = filterTokens(tokens);
		if (tokens.length == 0)
			return;

		if(completedToken > -1 && Parser.statements_recovery_filter[completedToken] != 0) return;

		this.data.insertedTokensPtr++;
		if(this.data.insertedTokens == null) {
			this.data.insertedTokens = new int[10][];
			this.data.insertedTokensPosition = new int[10];
			this.data.insertedTokenUsed = new boolean[10];
		} else if(this.data.insertedTokens.length == this.data.insertedTokensPtr) {
			int length = this.data.insertedTokens.length;
			System.arraycopy(this.data.insertedTokens, 0, this.data.insertedTokens = new int[length * 2][], 0, length);
			System.arraycopy(this.data.insertedTokensPosition, 0, this.data.insertedTokensPosition = new int[length * 2], 0, length);
			System.arraycopy(this.data.insertedTokenUsed, 0, this.data.insertedTokenUsed = new boolean[length * 2], 0, length);
		}
		this.data.insertedTokens[this.data.insertedTokensPtr] = reverse(tokens);
		this.data.insertedTokensPosition[this.data.insertedTokensPtr] = position;
		this.data.insertedTokenUsed[this.data.insertedTokensPtr] = false;
	}

	public void insertTokenAhead(int token, int index) {
		if(!this.record) return;
		if (token == TerminalTokens.TokenNameRestrictedIdentifierrecord)
			return;
		int length = this.data.insertedTokens[index].length;
		int [] tokens = new int [length + 1];
		System.arraycopy(this.data.insertedTokens[index], 0, tokens, 1, length);
		tokens[0] = token;
		this.data.insertedTokens[index] = tokens;
	}

	public void replaceTokens(int token, int start, int end) {
		replaceTokens(new int []{token}, start, end);
	}

	int[] filterTokens(int[] tokens) {
//		if (this.sourceLevel >= ClassFileConstants.JDK14)
//			return tokens;
		return Arrays.stream(tokens)
				.filter(x -> x != TerminalTokens.TokenNameRestrictedIdentifierrecord)
				.toArray();
	}
	public void replaceTokens(int[] tokens, int start, int end) {
		if(!this.record) return;
		tokens = filterTokens(tokens);
		if (tokens.length == 0)
			return;
		this.data.replacedTokensPtr++;
		if(this.data.replacedTokensStart == null) {
			this.data.replacedTokens = new int[10][];
			this.data.replacedTokensStart = new int[10];
			this.data.replacedTokensEnd = new int[10];
			this.data.replacedTokenUsed= new boolean[10];
		} else if(this.data.replacedTokensStart.length == this.data.replacedTokensPtr) {
			int length = this.data.replacedTokensStart.length;
			System.arraycopy(this.data.replacedTokens, 0, this.data.replacedTokens = new int[length * 2][], 0, length);
			System.arraycopy(this.data.replacedTokensStart, 0, this.data.replacedTokensStart = new int[length * 2], 0, length);
			System.arraycopy(this.data.replacedTokensEnd, 0, this.data.replacedTokensEnd = new int[length * 2], 0, length);
			System.arraycopy(this.data.replacedTokenUsed, 0, this.data.replacedTokenUsed = new boolean[length * 2], 0, length);
		}
		this.data.replacedTokens[this.data.replacedTokensPtr] = reverse(tokens);
		this.data.replacedTokensStart[this.data.replacedTokensPtr] = start;
		this.data.replacedTokensEnd[this.data.replacedTokensPtr] = end;
		this.data.replacedTokenUsed[this.data.replacedTokensPtr] = false;
	}

	public void removeTokens(int start, int end) {
		if(!this.record) return;
		this.data.removedTokensPtr++;
		if(this.data.removedTokensStart == null) {
			this.data.removedTokensStart = new int[10];
			this.data.removedTokensEnd = new int[10];
			this.data.removedTokenUsed = new boolean[10];
		} else if(this.data.removedTokensStart.length == this.data.removedTokensPtr) {
			int length = this.data.removedTokensStart.length;
			System.arraycopy(this.data.removedTokensStart, 0, this.data.removedTokensStart = new int[length * 2], 0, length);
			System.arraycopy(this.data.removedTokensEnd, 0, this.data.removedTokensEnd = new int[length * 2], 0, length);
			System.arraycopy(this.data.removedTokenUsed, 0, this.data.removedTokenUsed = new boolean[length * 2], 0, length);
		}
		this.data.removedTokensStart[this.data.removedTokensPtr] = start;
		this.data.removedTokensEnd[this.data.removedTokensPtr] = end;
		this.data.removedTokenUsed[this.data.removedTokensPtr] = false;
	}

	@Override
	protected int getNextToken0() throws InvalidInputException {
		if(this.pendingTokensPtr > -1) {
			int pendingToken = this.pendingTokens[this.pendingTokensPtr--];
			if(pendingToken == TerminalTokens.TokenNameIdentifier){
				this.fakeTokenSource = FAKE_IDENTIFIER;
			} else {
				this.fakeTokenSource = CharOperation.NO_CHAR;
			}
			return pendingToken;
		}

		this.fakeTokenSource = null;
		this.precededByRemoved = false;

		if(this.data.insertedTokens != null) {
			for (int i = 0; i <= this.data.insertedTokensPtr; i++) {
				if(this.data.insertedTokensPosition[i] == this.currentPosition - 1 && i > this.skipNextInsertedTokens) {
					this.data.insertedTokenUsed[i] = true;
					this.pendingTokens = this.data.insertedTokens[i];
					this.pendingTokensPtr = this.data.insertedTokens[i].length - 1;
					this.isInserted = true;
					this.startPosition = this.currentPosition;
					this.skipNextInsertedTokens = i;
					int pendingToken = this.pendingTokens[this.pendingTokensPtr--];
					if(pendingToken == TerminalTokens.TokenNameIdentifier){
						this.fakeTokenSource = FAKE_IDENTIFIER;
					} else {
						this.fakeTokenSource = CharOperation.NO_CHAR;
					}
					return pendingToken;
				}
			}
			this.skipNextInsertedTokens = -1;
		}

		int previousLocation = this.currentPosition;
		int currentToken = super.getNextToken0();

		if(this.data.replacedTokens != null) {
			for (int i = 0; i <= this.data.replacedTokensPtr; i++) {
				if(this.data.replacedTokensStart[i] >= previousLocation &&
						this.data.replacedTokensStart[i] <= this.startPosition &&
						this.data.replacedTokensEnd[i] >= this.currentPosition - 1) {
					this.data.replacedTokenUsed[i] = true;
					this.pendingTokens = this.data.replacedTokens[i];
					this.pendingTokensPtr = this.data.replacedTokens[i].length - 1;
					this.fakeTokenSource = FAKE_IDENTIFIER;
					this.isInserted = false;
					this.currentPosition = this.data.replacedTokensEnd[i] + 1;
					int pendingToken = this.pendingTokens[this.pendingTokensPtr--];
					if(pendingToken == TerminalTokens.TokenNameIdentifier){
						this.fakeTokenSource = FAKE_IDENTIFIER;
					} else {
						this.fakeTokenSource = CharOperation.NO_CHAR;
					}
					return pendingToken;
				}
			}
		}
		if(this.data.removedTokensStart != null) {
			for (int i = 0; i <= this.data.removedTokensPtr; i++) {
				if(this.data.removedTokensStart[i] >= previousLocation &&
						this.data.removedTokensStart[i] <= this.startPosition &&
						this.data.removedTokensEnd[i] >= this.currentPosition - 1) {
					this.data.removedTokenUsed[i] = true;
					this.currentPosition = this.data.removedTokensEnd[i] + 1;
					this.precededByRemoved = false;
					return getNextToken0();
				}
			}
		}
		return currentToken;
	}

	@Override
	public char[] getCurrentIdentifierSource() {
		if(this.fakeTokenSource != null) return this.fakeTokenSource;
		return super.getCurrentIdentifierSource();
	}

	@Override
	public char[] getCurrentTokenSourceString() {
		if(this.fakeTokenSource != null) return this.fakeTokenSource;
		return super.getCurrentTokenSourceString();
	}

	@Override
	public char[] getCurrentTokenSource() {
		if(this.fakeTokenSource != null) return this.fakeTokenSource;
		return super.getCurrentTokenSource();
	}

	public RecoveryScannerData getData() {
		return this.data;
	}

	public boolean isFakeToken() {
		return this.fakeTokenSource != null;
	}

	public boolean isInsertedToken() {
		return this.fakeTokenSource != null && this.isInserted;
	}

	public boolean isReplacedToken() {
		return this.fakeTokenSource != null && !this.isInserted;
	}

	public boolean isPrecededByRemovedToken() {
		return this.precededByRemoved;
	}

	public void setData(RecoveryScannerData data) {
		if(data == null) {
			this.data = new RecoveryScannerData();
		} else {
			this.data = data;
		}
	}

	public void setPendingTokens(int[] pendingTokens) {
		this.pendingTokens = pendingTokens;
		this.pendingTokensPtr = pendingTokens.length - 1;
	}
}
