package workerthreads.fitnessescriptcorrector;

import java.util.Calendar;

import workerthreads.changedates.ChangeDatesWorker;

public class Lexer {

	enum State {
		BeginOfLine, ReadingStatement, ReadingBetweenPipes, ReadingHashComment, ReadingExclamationmarkComment, ReadingText
	}

	private TokenList tokenList;

	private String text;

	private int textPos;

	private State state;

	private StringBuilder lastRead;

	public TokenList lex(String text) {
		tokenList = new TokenList();
		this.text = text;
		textPos = 0;
		state = State.BeginOfLine;
		lastRead = new StringBuilder();

		startLexing();

		markTokensBetweenPipes();

		return tokenList;
	}

	private void markTokensBetweenPipes() {

		boolean betweenPipes = false;

		int lastPipe = 0;

		for (int j = 0; j < tokenList.size(); j++) {

			Token t = tokenList.get(j);

			switch (t.getTokenType()) {
			case Pipe:

				if (betweenPipes) {
					for (int i = lastPipe + 1; i < j; i++) {
						tokenList.get(i).setBetweenPipes(true);
					}
				} else {
					betweenPipes = true;
				}

				lastPipe = j;

				break;
			case Text:
			case Date:
			case Year:
				break;
			default:
				betweenPipes = false;
				break;
			}
		}

	}

	private void startLexing() {

		while (peek() != (char) 0x00) {

			switch (state) {
			case BeginOfLine:
				lexBeginOfLine();
				break;
			case ReadingBetweenPipes:
				consumeText("|", "\n", "\r");
				if (peek() == '|') {
					getChar();
					push(TokenType.Pipe);
					state = State.ReadingBetweenPipes;
				} else if (peek() == '\n' || peek() == '\r') {
					readNewLine();
					state = State.BeginOfLine;
				}
				break;
			case ReadingExclamationmarkComment:
				consumeText("-!", "", "");
				if (comes("-!")) {
					consume("-!");
					push(TokenType.ExclamationmarkCommentEnd);
					readNewLine();
					state = State.BeginOfLine;
				}
				break;
			case ReadingStatement:
			case ReadingHashComment:
			case ReadingText:
				consumeText("\n", "\r", "");
				if (peek() == '\n' || peek() == '\r') {
					readNewLine();
					state = State.BeginOfLine;
				}
				// else: End of text reached!
				break;
			default:
				break;
			}

		}
	}

	private void consumeText(String end1, String end2, String end3) {

		boolean lastCharWasDigit = false;

		while (peek() != (char) 0x00 && !comes(end1) && !comes(end2)
				&& !comes(end3)) {

			boolean yearOrDateOrSchoolyearOrPKZOrEinsatz = false;

			boolean comesDigit = isDigit(peek());
			
			if (!yearOrDateOrSchoolyearOrPKZOrEinsatz && !lastCharWasDigit && comesDigit) {
				Calendar calendar = comesDate();
				if (calendar != null) {
					yearOrDateOrSchoolyearOrPKZOrEinsatz = true;

					// consume("xx.xx.xxxx"); -> this is done by comesDate()

					// TODO: store calendar in token!

					Token token = new Token(TokenType.Date, flushLastRead());
					token.setCalendar(calendar);
					tokenList.add(token);

				} else if (comesSchoolyear()) {
					yearOrDateOrSchoolyearOrPKZOrEinsatz = true;

					consume("xxxx/xx");
					push(TokenType.Schoolyear);
				} else if (comesPKZ()) {

					yearOrDateOrSchoolyearOrPKZOrEinsatz = true;

					consume("xxxxxxxxx");
					push(TokenType.PKZ);
				} else if (comesYear()) {

					yearOrDateOrSchoolyearOrPKZOrEinsatz = true;

					consume("xxxx");
					push(TokenType.Year);
				} else if (comesEinsatz()) {
					yearOrDateOrSchoolyearOrPKZOrEinsatz = true;
					consume("xx/xx");
					push(TokenType.Einsatz);
				}

			}

			lastCharWasDigit = comesDigit;

			if (!yearOrDateOrSchoolyearOrPKZOrEinsatz) {
				getChar();
			}
		}

		storeLastReadInTextToken();

	}

	/**
	 * ret
	 * 
	 * @return
	 */
	private boolean comesEinsatz() {

		if (text.length() - textPos >= ("10/11 ").length()) {

			String s = text.substring(textPos, textPos + 6);

			if (startsWithFormat(s, "99/99")) {

				char c = s.charAt(5);
				if (c == ' ' || c == ')' || c == '|') {

					int i1 = Integer.parseInt(s.substring(0, 2));
					int i2 = Integer.parseInt(s.substring(3, 5));

					if ((i2 - i1 + 100) % 100 == 1) {
						return true;
					}

				}

			}
		}
		return false;
	}

	private boolean comesPKZ() {

		boolean ret = comesFormat("999999999");

		if (ret && text.length() - textPos > 9
				&& isDigit(text.charAt(textPos + 9))) {
			ret = false;
		}

		if (ret) {
			String pkz = text.substring(textPos, textPos + 9);
			int pruefziffer = ChangeDatesWorker.getPruefziffer(pkz.substring(0,
					8));
			if (pruefziffer != Integer.parseInt(pkz.substring(8, 9))) {
				ret = false;

				consume(pkz);

			}
		}

		return ret;
	}

	private boolean comesSchoolyear() {
		boolean ret = comesFormat("9999/99 ");
		if (ret && text.length() - textPos > 7
				&& isDigit(text.charAt(textPos + 7))) {
			ret = false;
		}

		if (ret) {

			int year = Integer.parseInt(text.substring(textPos, textPos + 4));
			if (year < 1900 || year > 2200) {
				ret = false;
			}

			int suffix = Integer.parseInt(text.substring(textPos + 5,
					textPos + 7));

			if (suffix != (year + 1) % 100) {
				ret = false;
			}

			if (!ret) {
				/*
				 * prevent first 4 digits later to be interpreted as year
				 */
				for (int i = 0; i < 7; i++) {
					getChar();
				}
			}

		}

		return ret;
	}

	private int numberOfDigitsToCome(int startIndex, int maxDigits) {
		int i = startIndex;
		int number = 0;

		while (number < maxDigits && i < text.length()) {
			if (isDigit(text.charAt(i))) {
				number++;
			} else {
				break;
			}
			i++;
		}

		return number;

	}

	private Calendar comesDate() {

		int numberOfDigits1 = numberOfDigitsToCome(textPos, 2);

		int numberOfDigits2;

		Calendar ret = null;

		if (numberOfDigits1 >= 1 && numberOfDigits1 <= 2) {
			if (text.charAt(textPos + numberOfDigits1) == '.') {
				numberOfDigits2 = numberOfDigitsToCome(textPos
						+ numberOfDigits1 + 1, 2);
				if (numberOfDigits2 >= 1 && numberOfDigits2 <= 2) {
					if (text.charAt(textPos + numberOfDigits1 + numberOfDigits2
							+ 1) == '.') {
						if (numberOfDigitsToCome(textPos + numberOfDigits1
								+ numberOfDigits2 + 2, 5) == 4) {

							int length = numberOfDigits1 + numberOfDigits2 + 6;

							int date = Integer.parseInt(text.substring(textPos,
									textPos + numberOfDigits1));
							int month = Integer.parseInt(text.substring(textPos
									+ numberOfDigits1 + 1, textPos
									+ numberOfDigits1 + 1 + numberOfDigits2));
							int yearStart = textPos + numberOfDigits1
									+ numberOfDigits2 + 2;
							int year = Integer.parseInt(text.substring(
									yearStart, yearStart + 4));

							ret = Calendar.getInstance();

							ret.clear();
							
							/*
							 * month ist 1-based (januar == 1), ret.set erwartet aber einen 0-based Monat (Januar == 0)
							 * Daher "-1".
							 */
							ret.set(year, month - 1, date);

							if (ret.get(Calendar.DATE) != date
									|| ret.get(Calendar.MONTH) != month - 1
									|| ret.get(Calendar.YEAR) != year) {
								ret = null;

								/**
								 * force recognition of incorrect date as text
								 * in order to prevent year-part later to be
								 * parsed as year;
								 */

								for (int i = 0; i < length; i++) {
									getChar();
								}

							}

							if (ret != null) {

								storeLastReadInTextToken();

								for (int i = 0; i < length; i++) {
									getChar();
								}
							}

						}
					}
				}
			}
		}

		return ret;

	}

	private void storeLastReadInTextToken() {

		if (lastRead.length() > 0) {
			push(TokenType.Text);
		}

	}

	private boolean comesYear() {
		boolean ret = comesFormat("9999 ");
				
		if (ret && text.length() - textPos > 4
				&& isDigit(text.charAt(textPos + 4))) {
			ret = false;
		}

		if (ret) {
			int year = Integer.parseInt(text.substring(textPos, textPos + 4));
			if (year < 1970 || year > 2020) {
				ret = false;
			}
		}

		return ret;
	}

	private boolean comesFormat(String format) {
		/**
		 * '9' stands for digit, '.' for '.', ' ' for ' ', '/' for '/'
		 */
		boolean ret = false;

		if (text.length() - textPos >= format.length()) {

			ret = true;

			for (int j = 0; j < format.length(); j++) {
				char formatChar = format.charAt(j);
				char textChar = text.charAt(textPos + j);

				if (!(formatChar == '9' && isDigit(textChar))
						&& !(formatChar == textChar)) {
					ret = false;
					break;
				}
			}
		}

		return ret;
	}

	private boolean startsWithFormat(String s, String formatString) {
		/**
		 * '9' stands for digit, '.' for '.', ' ' for ' ', '/' for '/'
		 */
		boolean ret = false;

		if (s.length() >= formatString.length()) {

			ret = true;

			for (int j = 0; j < formatString.length(); j++) {
				char formatChar = formatString.charAt(j);
				char textChar = s.charAt(j);

				if (!(formatChar == '9' && isDigit(textChar))
						&& !(formatChar == textChar)) {
					ret = false;
					break;
				}
			}
		}

		return ret;

	}

	private boolean isDigit(char c) {
		return (c >= '1' && c <= '9') || c == '0';
	}

	private void lexBeginOfLine() {
		readSpace();
		switch (peek()) {
		case '|':
			getChar();
			push(TokenType.Pipe);
			state = State.ReadingBetweenPipes;
			break;
		case '!':
			lexExclamationMark();
			break;
		case '#':
			getChar();
			push(TokenType.Hash);
			state = State.ReadingHashComment;
			break;
		default:
			state = State.ReadingText;
			break;
		}
	}

	private void lexExclamationMark() {
		if (comes("!-")) {
			consume("!-");
			push(TokenType.ExclamationmarkCommentStart);
			state = State.ReadingExclamationmarkComment;
		} else if (comes("!|")) {
			getChar();
			push(TokenType.Exclamationmark);
			getChar();
			push(TokenType.Pipe);
			state = State.ReadingBetweenPipes;
		} else if (comes("!define tab (Einsatz")) {
			consumeText("\n", "\r", "@@@@@@@@@");
			state = State.BeginOfLine;
		} else {
			consume("!");
			push(TokenType.Exclamationmark);
			state = State.ReadingStatement;
		}

	}

	private void consume(String s) {

		storeLastReadInTextToken();

		for (int i = 0; i < s.length(); i++) {
			getChar();
		}
	}

	private void push(TokenType t) {
		push(t, flushLastRead());
	}

	private void push(TokenType t, String s) {
		tokenList.add(new Token(t, s));
	}

	/**
	 * reads and consumes Space and newLine until nonSpace or non-newLine comes
	 */
	private void readSpace() {
		do {
			while (isSpace(peek())) {
				getChar();
			}
			if (lastRead.length() > 0) {
				push(TokenType.Space);
			}
			readNewLine();
		} while (isSpace(peek()));
	}

	private void readNewLine() {
		while (peek() == '\n' || peek() == '\r') {
			char c = getChar();
			
			if ((c == '\r' && peek() != '\n') || (c == '\n')) {
				push(TokenType.NewLine);
			}
			
		}
		if (lastRead.length() > 0) {
			push(TokenType.NewLine);
		}
	}

	private boolean isSpace(char c) {
		return c == ' ' || c == '\t' || (int) c == 65279;
	}

	private char peek() {
		if (textPos < text.length()) {
			return text.charAt(textPos);
		} else {
			return (char) 0x00;
		}
	}

	private char getChar() {
		char c = peek();
		if (textPos < text.length()) {
			textPos++;
		}
		lastRead.append(c);
		return c;
	}

	private boolean comes(String s) {
		boolean erg = false;

		if (text.length() - textPos >= s.length() && s.length() > 0) {
			erg = true;
			int i = 0;
			while (i < s.length()) {
				if (s.charAt(i) != text.charAt(textPos + i)) {
					erg = false;
					break;
				}
				i++;
			}
		}
		return erg;
	}

	private String flushLastRead() {
		String erg = lastRead.toString();
		lastRead = new StringBuilder();
		return erg;
	}

}
