package smart_gs.swingui.toolbar.action;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;

import javax.swing.JOptionPane;

import smart_gs.logical.LineSegmentForEdit;
import smart_gs.util.GSLog;
import smart_gs.util.Pair;
import smart_gs.util.Triple;

public class LineSegEditorChangeIndexesResultLinesMaker {

	String input = null;
	List<LineSegmentForEdit> linesForEditPre;
	List<LineSegmentForEdit> linesForEditPost;
	boolean canceled = false;
	List<Triple<Integer,Integer,Integer>> toChangeIndexsTriplesList;
	int index;
	char ch;
	MapEntry[] map;
	int size;
	int changedIndexSize;
	GSLog log = GSLog.getInstance();

	public LineSegEditorChangeIndexesResultLinesMaker(String input,	List<LineSegmentForEdit> linesForEdit) {
		this.linesForEditPre = linesForEdit;
		this.toChangeIndexsTriplesList = new ArrayList<Triple<Integer,Integer,Integer>>();
		this.input = input;
		this.size = linesForEditPre.size();
		this.map = getNewMap(this.size);

		if (this.input.length() == 0) {
			log.warn("Empty input. Canceled.");
			return;
		}
	}
	
	
	private class MapEntry {
		int source;
		int target;
		int spec;
		
		int getSource() {
			return source;
		}
		
		int getTarget() {
			return target;
		}
		
		int getSpecIndex() {
			return spec;
		}
		
		void setSource(int i) {
			source = i;
		}
		
		void setTarget(int i) {
			target = i;
		}
		
		void setSpecIndex(int i) {
			spec = i;
		}
	}
	
	private MapEntry[] getNewMap(int size) {
		MapEntry[] map = new MapEntry[size];
		for (int i = 0; i < size; i++) {
			MapEntry entry = new MapEntry();
			entry.setSource(i);
			entry.setTarget(this.size);//Dummy target. It is larger than any index. This fact is important for sorting the array map.
			entry.setSpecIndex(-1);
			map[i] = entry;
		}
		return map;
	}

	private class EntryComparator implements Comparator<MapEntry> {

		@Override
		public int compare(MapEntry arg0, MapEntry arg1) {
			int firstDiff = arg0.getTarget() - arg1.getTarget();
			int secondDiff = arg0.getSource() - arg1.getSource();
			if (firstDiff !=0 ) return firstDiff;
			return secondDiff;
		}
	}

	public void process() {
		if (this.input == null || this.input.length() == 0) {
			this.canceled = true;
			return;
		}
		
		this.index = 0;
		this.ch = this.input.charAt(0);
		
		this.canceled = read();
		if (this.canceled) return;
		this.canceled = semanticCheckAndBuildMap();
		if (this.canceled) return;
		change();
	}

	public boolean isCanceled() {
		return canceled;
	}

	
	private void change() {
		LineSegmentForEdit[] linesForEditPreArray = (LineSegmentForEdit[])linesForEditPre.toArray(new LineSegmentForEdit[0]);
		for (Triple<Integer,Integer,Integer> p : this.toChangeIndexsTriplesList) {
			int pre = p.getFirst();
			int post = p.getSecond();
			int specIndex = p.getThird();

			this.linesForEditPost = new ArrayList<LineSegmentForEdit>();
			for (LineSegmentForEdit line : linesForEditPreArray) {
				if (line != null) linesForEditPost.add(line.deepCopy());
			}
		}
	}

	private boolean read() {
		boolean canceled = false;
		String errMsg;
		Sort errSort;

		while (!canceled) {
			errMsg  = null;
			errSort  = null;

			this.index = 0;
			this.ch = this.input.charAt(this.index);
			this.toChangeIndexsTriplesList.clear();
			try {
				checkIlliegalChar();
				startSymbol();
			} catch (ReadException e) {
				log.error(errMsg);
				errMsg = e.errMsg;
				errSort = e.sort;
			}

			//			Successfully read. No canceled by user.
			if (errSort == Sort.END_OF_INPUT) {
				canceled = false;
				break;
			} else {
				this.input = JOptionPane.showInputDialog(errMsg, this.input);
				//				Canceled by user.
				if (this.input == null) {
					canceled = true;
				} else if (this.input.length() == 0) {
					log.warn("Empty input. Canceled.");
					canceled = true;
				}
			}
		}
		return canceled;
	}
	
	private boolean semanticCheckAndBuildMap() {
		boolean canceled = false;
		String errMsg = null;

		while (!canceled) {
			errMsg = null;
			this.map = getNewMap(this.size);
			outerloop: for(int i=0; i< this.toChangeIndexsTriplesList.size(); i++) {
				Triple<Integer,Integer,Integer> p = this.toChangeIndexsTriplesList.get(i);
				int start = p.getFirst();
				int end = p.getSecond();
				int target = p.getThird();
				
				if (start > end) {
					errMsg = String.format("Starting number is larger than ending number in %d-th specification %d-%d",i+1, start, end);
					break;
				}
				
				if (end >= this.size) {
					String[] s = this.input.split("[\t \n\r]+");
					errMsg = String.format("Line number %d of %d-th specification %s is larger than the largest line number %d.",end, i+1, s[i], this.size-1);
					break;
				}
				
				if (target + end - start >= this.size) {
					String[] s = this.input.split("[\t \n\r]+");
					errMsg = String.format("%d-th specification %s specifies a line number %d, which is larger than the largest line number %d.",i+1, s[i], target + end - start, this.size-1);
					break;
				}
				
				for (int j = start; j <= end; j++) {
					int currenttargetvalue = target+j-start;
					if (map[j].getSpecIndex() == -1) {
						map[j].setTarget(currenttargetvalue);
						map[j].setSpecIndex(i);
					} else {
						int oldtarget = map[j].getTarget();
						int oldSpecIndex = map[j].getSpecIndex();
						if (oldtarget != currenttargetvalue) {
							String[] s = this.input.split("[\t \n\r]+");
							errMsg = String.format("%d is mapped to %d by %d-th spec %s,\nbut is mapped to %d by %d-th spec %s",
									j, oldtarget, oldSpecIndex+1, s[oldSpecIndex],
									currenttargetvalue, i+1, s[i]);
							break outerloop;
						}
					}
	
				}
			}
			
			// Sort
			Arrays.sort(map, new EntryComparator());
			//All the MapEntries specifying chage are now before the entries which do not specify change (index is -1, target is size).
			
			this.changedIndexSize=0;
			for (MapEntry entry : this.map) {
				if (entry.getSpecIndex() != -1) this.changedIndexSize++;
			}
			
			// Check many to one mapping
			MapEntry oldEntry = this.map[0];
			for (int i=1; i < this.changedIndexSize; i++) {
				MapEntry currentEntry = map[i];
				int currentTarget = currentEntry.getTarget();
				int oldTarget = oldEntry.getTarget();
				if (oldTarget == currentTarget) { 
					String[] s = this.input.split("[\t \n\r]+");
					errMsg = String.format("%d of %d-th spec %s and %d of %d-th spec %s\nare both mapped to %d",
							oldEntry.getSource(), oldEntry.getSpecIndex()+1, s[oldEntry.getSpecIndex()],
							currentEntry.getSource(), currentEntry.getSpecIndex()+1, s[currentEntry.getSpecIndex()],
							oldTarget);
					break;
				}
			}

			// No semantic error. Thus, no cancellation.
			if (errMsg == null) {
				canceled = false;
				break;
			} else {
				// Something wrong semantically. Try again.
				this.input = JOptionPane.showInputDialog(errMsg, this.input);
				//				Canceled by user.
				if (this.input == null) {
					canceled = true;
				} else if (this.input.length() == 0) {
					log.warn("Empty input. Canceled.");
					canceled = true;
				} else {
					canceled = read();
				}
			}
		}
		
		if (! canceled) {
			LineSegmentForEdit[] construct = new LineSegmentForEdit[this.size];
			for (LineSegmentForEdit line : construct) line = null; // 念のため。必要ないはず。
			// Fill the lines specified to change their indexes.
			for (int i=0; i < this.changedIndexSize; i++) {
				MapEntry entry = this.map[i];
				construct[entry.getTarget()] = linesForEditPre.get(entry.getSource());
			}
			// Fill the lines not specified to change. They are not specified to change, but they can be changed, since
			// positions of the other lines to be changed.
			int index = changedIndexSize;
			for (int i=0; i< construct.length; i++) {
				if (construct[i] == null) {
					construct[i] = linesForEditPre.get(this.map[index].getSource());
					index++;
				}
			}
			linesForEditPre = new ArrayList<LineSegmentForEdit>(Arrays.asList(construct));
		}
		return canceled;
	}


	public enum Sort {
	    END_OF_INPUT, SYNTAX_ERR
	}
	
	class ReadException extends Exception {
		public Sort sort;
		public String errMsg;
		ReadException(Sort sort1, String msg) {
			sort = sort1;
			errMsg = msg; 
		}
	}

	public boolean getVerdict() {
		return this.canceled;
	}

	private void startSymbol() throws ReadException {
		if (isWhiteSpace(this.ch)) {whiteSpaces(); startSymbol();}
		if (isNumeral(this.ch)) {changeSpec(); startSymbol();}
		throwSyntaxErr();
	}
	
	private void whiteSpaces() throws ReadException {
		while (isWhiteSpace(this.ch)) {
			if (isEndOfInput()) throwEndOfInput();
			this.index++;
			this.ch = input.charAt(this.index);
			checkIlliegalChar();
		}
	}

	//	2>3 or 29-34>3
	private void changeSpec() throws ReadException {
		Integer start = null;
		Integer end = null;
		Integer target = null;
		StringBuffer startIndexString = new StringBuffer();
		StringBuffer endIndexString = new StringBuffer();
		StringBuffer targetIndexString = new StringBuffer();

		// read startIndexString
		while (isNumeral(this.ch)) {
			startIndexString.append(this.ch);
			if(isEndOfInput()) throwSyntaxErr();
			this.index++;
			this.ch = input.charAt(this.index);
			checkIlliegalChar();
		}
		
		if(isWhiteSpace(this.ch)) throwSyntaxErr();
		
		// At this point, start *should* end with '-' or '>'
		if (this.ch != '-' && this.ch != '>') throwSyntaxErr();
		
		if (this.ch == '>') {

			this.index++;
			this.ch = input.charAt(this.index);
			checkIlliegalChar();
			
			// At this point, an numeral must come.
			// Read a number to endIndexString
			if (!isNumeral(this.ch)) throwSyntaxErr();
			
			while (isNumeral(this.ch)) {
				targetIndexString.append(this.ch);
				if(isEndOfInput()) {
					start = new Integer(new String(startIndexString));
					end = start;
					target = new Integer(new String(targetIndexString));
					this.toChangeIndexsTriplesList.add(new Triple<Integer,Integer,Integer>(start,end,target));
					throwEndOfInput();
				} 
				this.index++;
				this.ch = input.charAt(this.index);
				checkIlliegalChar();
			}	
			
			start = new Integer(new String(startIndexString));
			end = start;
			target = new Integer(new String(targetIndexString));
			this.toChangeIndexsTriplesList.add(new Triple<Integer,Integer,Integer>(start,end,target));
		} else if (this.ch == '-') {

			this.index++;
			this.ch = input.charAt(this.index);
			checkIlliegalChar();

			// At this point, an numeral must come.
			// Read a number to endIndexString
			if (!isNumeral(this.ch)) throwSyntaxErr();

			while (isNumeral(this.ch)) {
				endIndexString.append(this.ch);
				if(isEndOfInput()) throwSyntaxErr();

				this.index++;
				this.ch = input.charAt(this.index);
				checkIlliegalChar();
			}
			
			// At this point, '>' must come.
			if (this.ch != '>') throwSyntaxErr();
			
			this.index++;
			this.ch = input.charAt(this.index);
			checkIlliegalChar();
			
			// At this point, an numeral must come.
			// Read a number to endIndexString
			if (!isNumeral(this.ch)) throwSyntaxErr();

			while (isNumeral(this.ch)) {
				targetIndexString.append(this.ch);
				if(isEndOfInput()) {
					start = new Integer(new String(startIndexString));
					end = new Integer(new String(endIndexString));
					target = new Integer(new String(targetIndexString));
					this.toChangeIndexsTriplesList.add(new Triple<Integer,Integer,Integer>(start,end,target));
					throwEndOfInput();
				} 
				this.index++;
				this.ch = input.charAt(this.index);
				checkIlliegalChar();
			}
			
			start = new Integer(new String(startIndexString));
			end = new Integer(new String(endIndexString));
			target = new Integer(new String(targetIndexString));
			this.toChangeIndexsTriplesList.add(new Triple<Integer,Integer,Integer>(start,end,target));
		} else {
			throwSyntaxErr();
		}
	}
	
	private boolean isEndOfInput () {
		return this.index >= this.input.length()-1;
	}

	
	private void throwEndOfInput () throws ReadException {
		ReadException e = new ReadException(Sort.END_OF_INPUT,"");
		throw e;
	}
	
	private void throwSyntaxErr () throws ReadException {
		ReadException e = new ReadException(Sort.SYNTAX_ERR,String.format("Syntax error at %d-th character: %s ...\n",this.index+1,this.input.substring(index,Math.min(index+4,this.input.length()))));
		throw e;
	}
	
	private void checkIlliegalChar () throws ReadException {
		if (isIllegalChar(this.ch)) {
			ReadException e = new ReadException(Sort.SYNTAX_ERR,String.format("%d-th character %c is illegal: %s ...\n",this.index+1,this.ch, this.input.substring(index,Math.min(index+4, this.input.length()))));
			throw e;
		}
	}
	
	private boolean isWhiteSpace(char ch) {
		return ch == ' ' || ch == '\t' || ch == '\r' || ch == '\n';
	}
	
	private boolean isNumeral(char ch) {
		return '0' <= ch && ch <= '9';
	}
	
	private boolean isIllegalChar(char ch) {
		return ! (isWhiteSpace(ch) || isNumeral(ch) || ch == '-' || ch == '>');
	}

	public List<LineSegmentForEdit> getLines() {
		return linesForEditPost;
	}

}

