/*
 * Copyright 2013 Yuichiro Moriguchi
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package net.morilib.nina;

import java.io.StringWriter;
import java.text.Normalizer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import net.morilib.automata.NFA;
import net.morilib.automata.NFAState;
import net.morilib.automata.nfa.NFAAccept;
import net.morilib.automata.nfa.NFAAlternative;
import net.morilib.automata.nfa.NFAConcatenation;
import net.morilib.automata.nfa.NFAObject;
import net.morilib.automata.nfa.RegexParseException;
import net.morilib.automata.nfa.RegexpParser;
import net.morilib.automata.nfa.SingleObjectNFA;
import net.morilib.range.Range;

/**
 *
 *
 * @author MORIGUCHI, Yuichiro 2013/10/13
 */
public class NFABuilder implements NinaAction {

	//
	private static final Pattern CHARX = Pattern.compile(".\\p{M}*",
			Pattern.DOTALL);

	private NinaState vertex;
	private NinaNFA nfa;
	private String label;
	private String name;

	/**
	 * 
	 * @param q
	 */
	public NFABuilder() {
		nfa = new NinaNFA();
	}

	/* (non-Javadoc)
	 * @see net.morilib.nina.NinaAction#labelAdded(int, java.lang.String, net.morilib.nina.NinaState)
	 */
	@Override
	public void labelAdded(NinaEvent q, NinaFrameReader rd,
			boolean accept) {
		StringWriter wr = new StringWriter();
		char[] a = new char[1024];
		int l;

		while((l = rd.read(a)) >= 0)  wr.write(a, 0, l);
		if(vertex == null) {
			// set initial state if it is not set
			vertex = new NinaState();
			nfa.initial = vertex;
		}

		vertex.label = wr.toString();
		if(accept) {
			// accepted state
			nfa.accept.put(vertex, null);
		}
	}

	@SuppressWarnings("unchecked")
	public void link(NinaEvent q, Object ver) {
		NinaState v = vertex;

		vertex = (ver != null) ? (NinaState)ver : new NinaState();
		if(q.getEdge() instanceof NFA) {
			nfa.linkNFA(v, vertex,
					(NFA<Object, NFAState, Void>)q.getEdge());
		} else if(q.getEdge() instanceof Range) {
			nfa.linkAlphabet(v, vertex, (Range)q.getEdge());
		} else {
			nfa.linkAlphabet(v, vertex, q.getEdge());
		}
	}

	/* (non-Javadoc)
	 * @see net.morilib.nina.NinaAction#getLabel()
	 */
	@Override
	public String getLabel() {
		return label;
	}

	/* (non-Javadoc)
	 * @see net.morilib.nina.NinaAction#getVertex()
	 */
	@Override
	public Object getVertex() {
		return vertex;
	}

	/* (non-Javadoc)
	 * @see net.morilib.nina.NinaAction#setVertex(java.lang.Object)
	 */
	@Override
	public void setVertex(Object o) {
		vertex = (NinaState)o;
	}

	/* (non-Javadoc)
	 * @see net.morilib.nina.NinaAction#doneBlockSearching(net.morilib.nina.NinaEvent)
	 */
	@Override
	public void doneBlockSearching(NinaEvent q) {
		// searching a block is done
		vertex = (NinaState)q.getScratch();
	}

	/* (non-Javadoc)
	 * @see net.morilib.nina.NinaAction#setEdge(net.morilib.nina.NinaEvent, java.lang.Character)
	 */
	@Override
	public void setEdge(NinaEvent q, Object o) {
		q.setEdge(o);
	}

	//
	private NFAObject<Object, NFAState, Void> createQuoteNFA1(
			NinaEvent q, CharSequence s) {
		List<NFAObject<Object, NFAState, Void>> l;
		NFAObject<Object, NFAState, Void> o;
		Integer c;
		char x;

		l = new ArrayList<NFAObject<Object, NFAState, Void>>();
		for(int k = 0; k < s.length(); k++) {
			x = s.charAt(k);
			c = Nina.prendCharcode(q.getCharset(), x);
			o = SingleObjectNFA.newInstance((Object)c);
			l.add(o);
		}
		return NFAConcatenation.newInstance(l);
	}

	//
	private NFAObject<Object, NFAState, Void> createQuoteNFA2(
			NinaEvent q, CharSequence s) {
		List<NFAObject<Object, NFAState, Void>> l;
		NFAObject<Object, NFAState, Void> o, p;
		Integer c;
		char x, X;

		l = new ArrayList<NFAObject<Object, NFAState, Void>>();
		for(int k = 0; k < s.length(); k++) {
			x = s.charAt(k);
			X = Character.toUpperCase(x);
			x = Character.toLowerCase(x);
			if(x == X) {
				c = Nina.prendCharcode(q.getCharset(), x);
				o = SingleObjectNFA.newInstance((Object)c);
				l.add(o);
			} else {
				c = Nina.prendCharcode(q.getCharset(), x);
				o = SingleObjectNFA.newInstance((Object)c);
				c = Nina.prendCharcode(q.getCharset(), X);
				p = SingleObjectNFA.newInstance((Object)c);
				l.add(NFAAlternative.newInstance(o, p));
			}
		}
		return NFAConcatenation.newInstance(l);
	}

	//
	private NFAObject<Object, NFAState, Void> createQuoteNFA(
			NinaEvent q, CharSequence s, int qf) {
		List<NFAObject<Object, NFAState, Void>> l;
		NFAObject<Object, NFAState, Void> o, p;
		String x, nc, nd;
		Matcher m;

		if(qf == 'I') {
			o = createQuoteNFA1(q, s.toString().toUpperCase());
			p = createQuoteNFA1(q, s.toString().toLowerCase());
			return NFAAlternative.newInstance(o, p);
		} else if(qf == 'i') {
			return createQuoteNFA2(q, s);
		} else if(qf == 'n') {
			l = new ArrayList<NFAObject<Object, NFAState, Void>>();
			m = CHARX.matcher(s);
			while(m.find()) {
				x  = m.group();
				nc = Normalizer.normalize(x, Normalizer.Form.NFC);
				nd = Normalizer.normalize(x, Normalizer.Form.NFD);
				o  = createQuoteNFA1(q, nc);
				p  = createQuoteNFA1(q, nd);
				l.add(NFAAlternative.newInstance(o, p));
			}
			return NFAConcatenation.newInstance(l);
		} else {
			return createQuoteNFA1(q, s);
		}
	}

	/* (non-Javadoc)
	 * @see net.morilib.nina.NinaAction#setEdgeCharSequence(net.morilib.nina.NinaEvent, java.lang.CharSequence)
	 */
	@Override
	public void setEdgeCharSequence(NinaEvent q, CharSequence s,
			int qc, int qf) {
		List<NFAObject<Object, NFAState, Void>> l;
		NFAObject<Object, NFAState, Void> o;
		List<DoubleQuoteTokenizer.Elems> b;
		NFAState d;

		if(qc == '"') {
			l = new ArrayList<NFAObject<Object, NFAState, Void>>();
			b = DoubleQuoteTokenizer.parse(s.toString());
			for(DoubleQuoteTokenizer.Elems p : b) {
				l.add(createQuoteNFA(q, p.getString(), qf));
			}
			o = NFAAlternative.newInstance(l);
		} else {
			o = createQuoteNFA(q, s, qf);
		}

		// state must not be null
		// if accept state is null, optimizer will group different state
		d = new NFAState() {};
		q.setEdge(NFAAccept.newInstance(o, d));
	}

	/* (non-Javadoc)
	 * @see net.morilib.nina.NinaAction#setEdgeResource(net.morilib.nina.NinaEvent, net.morilib.nina.NinaParser, java.lang.String)
	 */
	@Override
	public void setEdgeResource(NinaEvent q,
			NinaParser p,
			String s,
			Map<String, String> map,
			NinaSubautomata b) {
		Object n;

		n = p.compileSubautomaton(s, new NFABuilder(), map, b,
				null).getMachine();
		q.setEdge(n);
	}

	/* (non-Javadoc)
	 * @see net.morilib.nina.NinaAction#setEdgeNFA(net.morilib.nina.NinaEvent, net.morilib.nina.NinaParser, net.morilib.automata.NFA)
	 */
	@Override
	public void setEdgeNFA(NinaEvent q,
			NFA<Object, NFAState, Integer> s,
			int f) {
		q.setEdge(s);
	}

	/* (non-Javadoc)
	 * @see net.morilib.nina.NinaAction#setEdgeEnd(net.morilib.nina.NinaEvent)
	 */
	@Override
	public void setEdgeEnd(NinaEvent q) {
		throw new NinaException("endtransitionerror", "NFA");
	}

	/* (non-Javadoc)
	 * @see net.morilib.nina.NinaAction#accept()
	 */
	@Override
	public NinaAction accept(String name) {
		this.name = name;
		return this;
	}

	/* (non-Javadoc)
	 * @see net.morilib.nina.NinaAction#getMachine()
	 */
	@Override
	public Object getMachine() {
		return nfa;
	}

	/* (non-Javadoc)
	 * @see net.morilib.nina.NinaAction#setMealyEdge(int)
	 */
	@Override
	public void setMealyEdge(int c) {
		throw new NinaException("isnotdfa");
	}

	/* (non-Javadoc)
	 * @see net.morilib.nina.NinaAction#setMealyEdge(java.lang.Object)
	 */
	@Override
	public void setMealyEdge(Object o) {
		throw new NinaException("isnotdfa");
	}

	/* (non-Javadoc)
	 * @see net.morilib.nina.NinaAction#setPriority(int)
	 */
	@Override
	public void setPriority(int p) {
		if(vertex != null) {
			vertex.priority = p;
		}
	}

	/* (non-Javadoc)
	 * @see net.morilib.nina.NinaAction#createState(net.morilib.nina.NinaEvent)
	 */
	@Override
	public NinaState createState(NinaEvent q) {
		return new NinaState();
	}

	/* (non-Javadoc)
	 * @see net.morilib.nina.NinaAction#putStateByLabel(java.lang.String, java.lang.Object)
	 */
	@Override
	public void putStateByLabel(String l, Object s) {
		// do nothing
	}

	/* (non-Javadoc)
	 * @see net.morilib.nina.NinaAction#getStateByLabel(java.lang.String)
	 */
	@Override
	public Object getStateByLabel(String l) {
		return null;
	}

	/* (non-Javadoc)
	 * @see net.morilib.nina.NinaAction#getLabelByState(java.lang.Object)
	 */
	@Override
	public String getLabelByState(Object s) {
		return null;
	}

	/* (non-Javadoc)
	 * @see net.morilib.nina.NinaAction#getDeadState()
	 */
	@Override
	public Object getDeadState() {
		return getStateByLabel(NinaParser.DEADSYM);
	}

	/* (non-Javadoc)
	 * @see net.morilib.nina.NinaAction#setStartState(java.lang.Object)
	 */
	@Override
	public void setStartState(Object o) {
		vertex = (NinaState)o;
		nfa.initial = vertex;
	}

	/* (non-Javadoc)
	 * @see net.morilib.nina.NinaAction#setUserEdge(net.morilib.nina.NinaEvent, java.lang.String)
	 */
	@Override
	public void setUserEdge(NinaEvent q, String o) {
		throw new NinaException("isnotdfa");
	}

	/* (non-Javadoc)
	 * @see net.morilib.nina.NinaAction#getLabels()
	 */
	@Override
	public Set<String> getLabels() {
		return Collections.emptySet();
	}

	/* (non-Javadoc)
	 * @see net.morilib.nina.NinaAction#putTypeByLabel(java.lang.String, java.lang.Object)
	 */
	@Override
	public void putTypeByLabel(String l, String s) {
		// do nothing
	}

	/* (non-Javadoc)
	 * @see net.morilib.nina.NinaAction#getTypeByLabel(java.lang.String)
	 */
	@Override
	public String getTypeByLabel(String l) {
		return null;
	}

	/* (non-Javadoc)
	 * @see net.morilib.nina.NinaAction#getName()
	 */
	@Override
	public String getName() {
		return name;
	}

	/* (non-Javadoc)
	 * @see net.morilib.nina.NinaAction#setEdgeSequence(net.morilib.nina.NinaEvent)
	 */
	@Override
	public void setEdgeSequence(NinaEvent q, Range r, int len) {
		throw new NinaException("isnotdfa");
	}

	/* (non-Javadoc)
	 * @see net.morilib.nina.NinaAction#setMealyEdgeType(int)
	 */
	@Override
	public void setMealyEdgeType(int c) {
		// do nothing
	}

	/* (non-Javadoc)
	 * @see net.morilib.nina.NinaAction#setAccept(boolean)
	 */
	@Override
	public void setAccept(boolean accept) {
		if(accept) {
			// accepted state
			nfa.accept.put(vertex, null);
		}
	}

	/* (non-Javadoc)
	 * @see net.morilib.nina.NinaAction#setEdgeDynamic(net.morilib.nina.NinaEvent, java.lang.String)
	 */
	@Override
	public void setEdgeDynamic(NinaEvent q, String var) {
		throw new NinaException("isnotdfa");
	}

	/* (non-Javadoc)
	 * @see net.morilib.nina.NinaAction#getDynamicTransitMap()
	 */
	@Override
	public Map<String, String> getDynamicTransitMap() {
		return Collections.emptyMap();
	}

	/* (non-Javadoc)
	 * @see net.morilib.nina.NinaAction#setEdgeGrammar(net.morilib.nina.NinaEvent, java.lang.String)
	 */
	@Override
	public void setEdgeGrammar(NinaEvent q, String name) {
		throw new NinaException("isnotdfa");
	}

	/* (non-Javadoc)
	 * @see net.morilib.nina.NinaAction#setEdgeRepeatedResource(net.morilib.nina.NinaEvent, java.lang.String, java.lang.Object)
	 */
	@Override
	public void setEdgeRepeatedResource(NinaEvent q, String s,
			Object d) {
		throw new NinaException("isnotdfa");
	}

	/* (non-Javadoc)
	 * @see net.morilib.nina.NinaAction#empty(net.morilib.nina.NinaEvent, net.morilib.nina.NinaConsole)
	 */
	@Override
	public NinaAction empty(NinaEvent q, String name) {
		setStartState(createState(q));
		return accept(name);
	}

	//
	private void _setRe(NinaEvent q, String re) {
		NFA<Object, NFAState, Void> s;
		NFAState d;

		try {
			// state must not be null
			d = new NFAState() {};
			s = RegexpParser.parse(re, d);
			setEdge(q, s);
		} catch(RegexParseException e) {
			throw new NinaException("invalidregex");
		}
	}

	@Override
	public void setEdgeDigits(NinaEvent q, int digits, boolean sign) {
		StringBuilder b = new StringBuilder();

		if(sign) {
			b.append("[-+]?");
		}
		if(digits < 0) {
			b.append("[0-9]+");
		} else {
			b.append("[0-9]");
			for(int i = 1; i < digits; i++) {
				b.append("[0-9]?");
			}
		}
		_setRe(q, b.toString());
//		throw new NinaException("isnotdfa");
	}

	/* (non-Javadoc)
	 * @see net.morilib.nina.NinaAction#setEdgeHexadecimal(net.morilib.nina.NinaEvent, int)
	 */
	@Override
	public void setEdgeHexadecimal(NinaEvent q, int bits) {
		StringBuilder b = new StringBuilder();

		if(bits < 0) {
			b.append("[0-9A-Fa-f]+");
		} else {
			b.append("[0-9A-Fa-f]");
			for(int i = 8; i < bits; i++) {
				b.append("[0-9A-Fa-f]?");
			}
		}
		_setRe(q, b.toString());
//		throw new NinaException("isnotdfa");
	}

	/* (non-Javadoc)
	 * @see net.morilib.nina.NinaAction#setEdgeOctal(net.morilib.nina.NinaEvent, int)
	 */
	@Override
	public void setEdgeOctal(NinaEvent q, int bits) {
		switch(bits) {
		case 8:
			_setRe(q, "([0-3][0-7]?|[4-7])[0-7]?");
			break;
		case 16:
			_setRe(q, "([0-1][0-7]?|[2-7])[0-7]?[0-7]?[0-7]?[0-7]?");
			break;
		case 32:
			_setRe(q, "([0-3][0-7]?|[4-7])" +
					"[0-7]?[0-7]?[0-7]?[0-7]?[0-7]?[0-7]?[0-7]?[0-7]?[0-7]?");
			break;
		default:
			_setRe(q, "[0-7]+");
			break;
		}
	}

	/* (non-Javadoc)
	 * @see net.morilib.nina.NinaAction#setEdgeFloat(net.morilib.nina.NinaEvent)
	 */
	@Override
	public void setEdgeFloat(NinaEvent q) {
		_setRe(q, "[-+]?([0-9]+|[0-9]*.[0-9]+)([eE][-+]?[0-9]+)?");
	}

	/* (non-Javadoc)
	 * @see net.morilib.nina.NinaAction#setEdgeIndent(net.morilib.nina.NinaEvent, java.lang.String, int)
	 */
	@Override
	public void setEdgeIndent(NinaEvent q, String cmp, int off) {
		throw new NinaException("isnotdfa");
	}

	/* (non-Javadoc)
	 * @see net.morilib.nina.NinaAction#setEdgeIdentifier(net.morilib.nina.NinaEvent, java.lang.String)
	 */
	@Override
	public void setEdgeIdentifier(NinaEvent q, String lang) {
		if(lang.equals("Java")) {
			_setRe(q, "[A-Za-z_$\\u0080-\\ufffc][A-Za-z0-9_$\\u0080-\\ufffc]*");
		} else if(lang.equals("C")) {
			_setRe(q, "[A-Za-z_][A-Za-z0-9_]*");
		} else if(lang.equals("String")) {
			throw new NinaException("notsupported");
		} else {
			throw new RuntimeException();
		}
	}

	/* (non-Javadoc)
	 * @see net.morilib.nina.NinaAction#setEdgeSkipSpaces(net.morilib.nina.NinaEvent, java.lang.String)
	 */
	@Override
	public void setEdgeSkipSpaces(NinaEvent q, String str) {
		throw new NinaException("isnotdfa");
	}

	/* (non-Javadoc)
	 * @see net.morilib.nina.NinaAction#setEdgeBackslash(net.morilib.nina.NinaEvent, int)
	 */
	@Override
	public void setEdgeBackslash(NinaEvent q, int c) {
		switch(c) {
		case 'X':
			_setRe(q, "[\\P{M}][\\p{M}]*");
			break;
		case 'b':  case 'B':
			throw new NinaException("isnotdfa");
		default:
			throw new NinaException("invalidsequence");
		}
	}

	/* (non-Javadoc)
	 * @see net.morilib.nina.NinaAction#setEdgeStringSequence(net.morilib.nina.NinaEvent, int, java.lang.String)
	 */
	@Override
	public void setEdgeStringSequence(NinaEvent q, int l, String cs) {
		StringBuilder b;

		if(l < 0) {
			_setRe(q, "[" + cs + "]*");
		} else {
			b = new StringBuilder();
			for(int i = 0; i < l; i++) {
				b.append("[").append(cs).append("]");
			}
			_setRe(q, b.toString());
		}
	}

	/* (non-Javadoc)
	 * @see net.morilib.nina.NinaAction#setEdgeFormatInteger(net.morilib.nina.NinaEvent, int, java.lang.String)
	 */
	@Override
	public void setEdgeFormatInteger(NinaEvent q, int l, String flg) {
		setEdgeStringSequence(q, l, "0-9");
	}

	/* (non-Javadoc)
	 * @see net.morilib.nina.NinaAction#setEdgeFormatHex(net.morilib.nina.NinaEvent, int, java.lang.String)
	 */
	@Override
	public void setEdgeFormatHex(NinaEvent q, int l, String flg) {
		setEdgeStringSequence(q, l, "0-9A-Fa-f");
	}

	/* (non-Javadoc)
	 * @see net.morilib.nina.NinaAction#setEdgeFormatOct(net.morilib.nina.NinaEvent, int, java.lang.String)
	 */
	@Override
	public void setEdgeFormatOct(NinaEvent q, int l, String flg) {
		setEdgeStringSequence(q, l, "0-7");
	}

}
