/*******************************************************************************
 * Copyright (c) 2015 Bruno Medeiros and other Contributors.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     Bruno Medeiros - initial API and implementation
 *******************************************************************************/
package melnorme.lang.tooling.parser;

import static melnorme.utilbox.core.Assert.AssertNamespace.assertNotNull;
import static melnorme.utilbox.core.Assert.AssertNamespace.assertTrue;
import static melnorme.utilbox.core.CoreUtil.areEqual;
import static melnorme.utilbox.core.CoreUtil.arrayC;

import java.text.MessageFormat;

import melnorme.lang.utils.parse.BasicCharSource_CommonExceptionAdapter;
import melnorme.lang.utils.parse.IBasicCharSource;
import melnorme.lang.utils.parse.LexingUtils;
import melnorme.utilbox.core.CommonException;
import melnorme.utilbox.misc.ArrayUtil;

public class TextBlocksReader {
	
	public enum TokenKind {
		TEXT,
		BLOCK_OPEN,
		EOS,
	}
	
	/* -----------------  ----------------- */
	
	protected final BasicCharSource_CommonExceptionAdapter charSource;
	
	protected final char[] blockOpens;
	protected final char[] blockCloses;
	
	protected String lastValue;
	protected TokenKind lastKind;
	
	public TextBlocksReader(IBasicCharSource<? extends Exception> charSource) {
		this(charSource, 
			arrayC('{', '(', '['), 
			arrayC('}', ')', ']')
		);
	}
	
	public TextBlocksReader(IBasicCharSource<? extends Exception> charSource, 
			char[] blockOpens, char[] blockCloses) {
		this.charSource = new BasicCharSource_CommonExceptionAdapter(assertNotNull(charSource));
		this.blockOpens = assertNotNull(blockOpens);
		this.blockCloses = assertNotNull(blockCloses);
		assertTrue(blockOpens.length == blockCloses.length);
	}
	
	public int peekTokenStart() throws CommonException {
		LexingUtils.skipWhitespace(charSource);
		return charSource.lookahead();
	}
	
	protected char consumeChar() throws CommonException {
		lastKind = null;
		lastValue = null;
		return charSource.consume();
	}
	
	/* -----------------  ----------------- */
	
	public String setTokenResult(String value, TokenKind kind) {
		this.lastValue = value;
		this.lastKind = kind;
		return value;
	}
	
	public TokenKind tokenAhead() throws CommonException {
		int tokenStartChar = peekTokenStart();
		if(tokenStartChar == -1) {
			return TokenKind.EOS;
		}
		char ch = (char) tokenStartChar;
		
		if(charIsBlockOpen(ch)) {
			return TokenKind.BLOCK_OPEN;
		}
		if(charIsBlockClose(ch)) {
			return TokenKind.EOS;
		}
		return TokenKind.TEXT;
	}
	
	protected boolean charIsBlockOpen(char ch) {
		return ArrayUtil.indexOf(blockOpens, ch) != -1;
	}
	
	protected boolean charIsBlockClose(char ch) {
		return ArrayUtil.indexOf(blockCloses, ch) != -1;
	}
	
	public boolean aheadIsEnd() throws CommonException {
		return tokenAhead() == TokenKind.EOS;
	}
	
	public boolean aheadIsBlockStart() throws CommonException {
		return tokenAhead() == TokenKind.BLOCK_OPEN;
	}
	
	public boolean aheadIsText() throws CommonException {
		return tokenAhead() == TokenKind.TEXT;
	}
	
	public String consumeText() throws CommonException {
		if(tokenAhead() != TokenKind.TEXT) {
			throw createParseException("Expected text, {0}.", errorAtTokenStart());
		}
		return parseTextValue();
	}
	
	protected String parseTextValue() throws CommonException {
		peekTokenStart();
		
		StringBuilder sb = new StringBuilder();
		
		while(charSource.hasCharAhead()) {
			
			char ahead = charSource.lookaheadChar();
			if(Character.isWhitespace(ahead) || charIsBlockClose(ahead) || charIsBlockOpen(ahead)) {
				break;
			}
			
			char ch = consumeChar();
			if(ch == '"') {
				boolean isEOS = LexingUtils.consumeUntilDelimiter_intoStringBuilder(charSource, '"', '\\', sb);
				if(isEOS) {
					throw createParseException("Unterminated text `{0}`.", sb.toString());
				}
			} else {
				sb.append(ch);
			}
		}
		
		return setTokenResult(sb.toString(), TokenKind.TEXT);
	}
	
	/* -----------------  ----------------- */
	
	public TextBlocksSubReader enterBlock() throws CommonException {
		if(tokenAhead() != TokenKind.BLOCK_OPEN) {
			throw createParseException("Expected block open, {0}.", errorAtTokenStart());
		}
		
		return parseBlock();
	}
	
	protected TextBlocksSubReader parseBlock() throws CommonException {
		char ch = consumeChar();
		int ix = ArrayUtil.indexOf(blockOpens, ch);
		char expectedClose = blockCloses[ix];
		return new TextBlocksSubReader(this.charSource, this.blockOpens, this.blockCloses, expectedClose);
	}
	
	public static class TextBlocksSubReader extends TextBlocksReader implements AutoCloseable {
		
		protected final char expectedClose;
		
		public TextBlocksSubReader(IBasicCharSource<? extends Exception> charSource, char[] blockOpens,
				char[] blockCloses, char expectedClose) {
			super(charSource, blockOpens, blockCloses);
			this.expectedClose = expectedClose;
		}
		
		@Override
		public void close() throws CommonException {
			int ahead = peekTokenStart();
			if(ahead == expectedClose) {
				char ch = charSource.consume();
				assertTrue(ch == ahead);
			} else {
				throw createParseException("Expected BLOCK_CLOSE `{0}`, {1}.", expectedClose, errorAtTokenStart());
			}
		}
		
	}
	
	/* ----------------- Error handling  ----------------- */
	
	protected CommonException createParseException(String pattern, Object... arguments) {
		return new CommonException(MessageFormat.format(pattern, arguments));
	}
	
	public String errorAtTokenStart() throws CommonException {
		return found(peekTokenStart()); 
	}
	
	protected String found(int ahead) {
		if(ahead == -1) {
			return "found EOS";
		}
		char ch = (char) ahead;
		return "found `" + ch + "`";
	}
	
	/* ----------------- Helpers ----------------- */
	
	public void expectText(String expectedText) throws CommonException {
		if(tokenAhead() != TokenKind.TEXT) {
			throw createParseException("Expected text `{0}`, {1}.", expectedText, errorAtTokenStart());
		}
		
		String text = parseTextValue();
		if(!areEqual(text, expectedText)) {
			throw createParseException("Expected text `{0}`, found text `{1}`.", expectedText, text);
		}
	}
	
	public <RET, EXC extends Exception> RET consumeBlock(BlockVisitorX<RET, EXC> visitor) 
			throws CommonException, EXC {
		try(TextBlocksSubReader subReader = enterBlock()) {
			return visitor.consumeChildren(subReader);
		}
	}
	
	public interface BlockVisitorX<RET, EXC extends Exception> {
		
		public RET consumeChildren(TextBlocksSubReader subReader) throws EXC;
		
	}
	
	public void skipNextElement() throws CommonException {
		if(aheadIsEnd()) {
			throw createParseException("Expected `{0}`, {1}.", "element", errorAtTokenStart());
		} else if(aheadIsText()) {
			consumeText();
		} else {
			consumeBlock((subReader) -> {
				subReader.skipToEnd();
				return null;
			});
		} 
	}
	
	public void skipToEnd() throws CommonException {
		while(!aheadIsEnd()) {
			skipNextElement();
		}
	}
	
}