/*
 * 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.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.math.stochastic.StochasticProcess;
import net.morilib.range.CharSets;
import net.morilib.range.Interval;
import net.morilib.range.Range;
import net.morilib.util.Tuple2;

/**
 *
 *
 * @author MORIGUCHI, Yuichiro 2013/10/14
 */
public class MarkovChainBuilder implements NinaAction {

	//
	private static final Pattern PTN1 = Pattern.compile(
			"[\t\n ]*'(.*)'[\t\n ]*");
	private static final Pattern PTN2 = Pattern.compile(
			"[\t\n ]*\\[(.*)\\][\t\n ]*");

	static class DBS implements StochasticProcess {

		List<Tuple2<Number, DBS>> edges =
				new ArrayList<Tuple2<Number, DBS>>();
		boolean accepted;
		Object output;
		double sum;

		void linkAlphabet(DBS d, Number o) {
			edges.add(new Tuple2<Number, DBS>(o, d));
			sum += o.doubleValue();
		}

		public DBS go(double r) {
			double l = 0.0, v;
			DBS x = null;

			for(Tuple2<Number, DBS> e : edges) {
				v = e.getA().doubleValue() / sum;
				x = e.getB();
				if(r > l && r <= l + v) {
					return x;
				} else {
					l += v;
				}
			}
			return x;
		}

		public boolean isDead() {
			return edges.isEmpty();
		}

		public boolean isAccepted() {
			return accepted;
		}

		@SuppressWarnings("unchecked")
		public Object getOutput() {
			List<Integer> l;
			StringBuffer b;
			int k;

			if(output instanceof String) {
				return output;
			} else if(output instanceof List) {
				l = (List<Integer>)output;
				k = (int)(l.size() * Math.random());
				b = new StringBuffer();
				b.appendCodePoint(l.get(k));
				return b.toString();
			} else {
				return "";
			}
		}

	};

	//
	private DBS initial, vertex;
	private String label;
	private String name;

	private Object parseOutput(String s) {
		List<Integer> l;
		Matcher m;
		String t;
		Range r;
		int k;

		if((m = PTN1.matcher(s)).matches()) {
			t = m.group(1);
			t = t.replaceAll("\\n", "\n");
			t = t.replaceAll("\\r", "\r");
			t = t.replaceAll("\\t", "\t");
			return t;
		} else if((m = PTN2.matcher(s)).matches()) {
			t = m.group(1);
			r = CharSets.parse(t);
			if(r.contains(Integer.MAX_VALUE - 1)) {
				throw new NinaException(
						"complement is not available");
			}

			l = new ArrayList<Integer>();
			for(Interval v : r.intervals()) {
				k = ((Integer)v.getInfimumBound()).intValue();
				for(; v.contains(k); k++) {
					l.add(k);
				}
			}
			return l;
		} else {
			return s.trim();
		}
	}

	/* (non-Javadoc)
	 * @see net.morilib.nina.NinaAction#labelAdded(net.morilib.nina.NinaEvent, java.lang.String, boolean)
	 */
	@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 = createState(q);
			initial = vertex;
		}
		vertex.output = parseOutput(wr.toString());
		vertex.accepted = accept;
	}

	/* (non-Javadoc)
	 * @see net.morilib.nina.NinaAction#link(net.morilib.nina.NinaEvent, java.lang.Object)
	 */
	@Override
	public void link(NinaEvent q, Object ver) {
		DBS v = vertex;

		vertex = (ver != null) ? (DBS)ver : createState(q);
		v.linkAlphabet(vertex,
				Double.valueOf(q.getEdge().toString()));
	}

	/* (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 = (DBS)o;
	}

	/* (non-Javadoc)
	 * @see net.morilib.nina.NinaAction#doneBlockSearching(net.morilib.nina.NinaEvent)
	 */
	@Override
	public void doneBlockSearching(NinaEvent q) {
		vertex = (DBS)q.getScratch();
	}

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

	/* (non-Javadoc)
	 * @see net.morilib.nina.NinaAction#setEdgeCharSequence(net.morilib.nina.NinaEvent, java.lang.CharSequence)
	 */
	@Override
	public void setEdgeCharSequence(NinaEvent q, CharSequence o,
			int qc, int qf) {
		throw new NinaException("cannotaddcharsequence");
	}

	/* (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 sub) {
		throw new NinaException("cannotaddnfa");
	}

	/* (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) {
		throw new NinaException("cannotaddnfa");
	}

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

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

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

	/* (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) {
		// do nothing
	}

	/* (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#createState(net.morilib.nina.NinaEvent)
	 */
	public DBS createState(NinaEvent q) {
		DBS s;

		s = new DBS();
		return s;
	}

	/* (non-Javadoc)
	 * @see net.morilib.nina.NinaAction#setStartState(java.lang.Object)
	 */
	@Override
	public void setStartState(Object o) {
		vertex = (DBS)o;
		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) {
		vertex.accepted = accept;
	}

	/* (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);
	}

	/* (non-Javadoc)
	 * @see net.morilib.nina.NinaAction#setEdgeDigits(net.morilib.nina.NinaEvent)
	 */
	@Override
	public void setEdgeDigits(NinaEvent q, int digits, boolean sign) {
		throw new NinaException("isnotfa");
	}

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

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

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

	/* (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) {
		throw new NinaException("isnotfa");
	}

	/* (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) {
		throw new NinaException("isnotfa");
	}

	/* (non-Javadoc)
	 * @see net.morilib.nina.NinaAction#setEdgeStringSequence(net.morilib.nina.NinaEvent, int, net.morilib.range.Range)
	 */
	@Override
	public void setEdgeStringSequence(NinaEvent q, int l, String r) {
		throw new NinaException("isnotfa");
	}

	/* (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) {
		throw new NinaException("isnotfa");
	}

	/* (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) {
		throw new NinaException("isnotfa");
	}

	/* (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) {
		throw new NinaException("isnotfa");
	}

}
