/*
 * 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.c.pre;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.LineNumberReader;
import java.io.Reader;
import java.io.StringReader;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.Stack;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import net.morilib.c.pre.directive.CpreDefine;
import net.morilib.c.pre.directive.CpreDirective;
import net.morilib.c.pre.directive.CpreElif;
import net.morilib.c.pre.directive.CpreElse;
import net.morilib.c.pre.directive.CpreEndif;
import net.morilib.c.pre.directive.CpreError;
import net.morilib.c.pre.directive.CpreIf;
import net.morilib.c.pre.directive.CpreIfState;
import net.morilib.c.pre.directive.CpreIfdef;
import net.morilib.c.pre.directive.CpreIfndef;
import net.morilib.c.pre.directive.CpreNull;
import net.morilib.c.pre.directive.CprePragma;
import net.morilib.c.pre.directive.CpreUndef;

public class CPreprocessorReader extends Reader {

	private static enum S0 { INI, SH1, EX1, EX2, AR1, AR2, DQ1, DQ2 };
	private static enum S1 { INI, SH1, SH2, DQ1, DQ2 };

	static final Map<String, CpreDirective> DIRS;
	static final Pattern PI1 = Pattern.compile("#include *<(.+)>");
	static final Pattern PI2 = Pattern.compile("#include *\"(.+)\"");
	static final SimpleDateFormat DATEFMT =
			new SimpleDateFormat("MMM dd yyyy", Locale.US);
	static final SimpleDateFormat TIMEFMT =
			new SimpleDateFormat("HH:mm:ss", Locale.US);

	static final String ERR001 =
			"#include requires <filename> or \"filename\"";

	static {
		Map<String, CpreDirective> d;

		d = new HashMap<String, CpreDirective>();
		d.put("#", new CpreNull());
		d.put("#define", new CpreDefine());
		d.put("#elif", new CpreElif());
		d.put("#else", new CpreElse());
		d.put("#endif", new CpreEndif());
		d.put("#error", new CpreError());
		d.put("#if", new CpreIf());
		d.put("#ifdef", new CpreIfdef());
		d.put("#ifndef", new CpreIfndef());
		d.put("#pragma", new CprePragma());
		d.put("#undef", new CpreUndef());
		DIRS = Collections.unmodifiableMap(d);
	}

	private Stack<LineNumberReader> rd =
		new Stack<LineNumberReader>();
	private Stack<CpreIfState> ifstat = new Stack<CpreIfState>();
	private Stack<String> fns = new Stack<String>();
	private CpreMacros macros;
	private String line = null;
	private int ptr = 0;

	/**
	 * 
	 * @param fname
	 * @param date
	 * @param in
	 */
	public CPreprocessorReader(String fname, java.util.Date date,
			Reader in) {
		char[] df;

//		rd.push(new LineNumberReader(new RemoveCCommentReader(in)));
		rd.push(new LineNumberReader(in));
		fns.push(fname != null ? fname : "<no name>");
		ifstat.add(CpreIfState.TOP);

		// initialize macros
		macros = new CpreMacros();
		macros.define("__STDC__", "1");
		macros.define("__FILE__", fname);
		df = DATEFMT.format(date).toCharArray();
		if(df[4] == '0')  df[4] = ' ';
		macros.define("__DATE__", new String(df));
		macros.define("__TIME__", TIMEFMT.format(date));
		macros.define("__STDC_VERSION__", "199912");
	}

	/**
	 * 
	 * @param fname
	 * @param in
	 */
	public CPreprocessorReader(String fname, Reader in) {
		this(fname, new java.util.Date(), in);
	}

	/**
	 * 
	 * @param name
	 * @param value
	 */
	public void define(String name, String value) {
		macros.define(name, value);
	}

	static String substitute(Reader rd, CpreMacros macros,
			Set<String> done, int lineno,
			boolean sh) throws IOException {
		List<String> l0 = null;
		StringBuffer b0, b1 = null, b2 = null;
		String s, nm = null, s1;
		S0 st = S0.INI;
		int c, l = 0;

		b0 = new StringBuffer();
		while(true) {
			c = rd.read();
			switch(st) {
			case INI:
				if(CpreUtils.isCMacroIdentifierStart(c)) {
					b1 = new StringBuffer().appendCodePoint(c);
					st = S0.EX1;
				} else if(c == '#') {
					st = S0.SH1;
				} else if(c == '"') {
					b0.append('"');  st = S0.DQ1;
				} else if(c < 0) {
					return b0.toString();
				} else {
					b0.appendCodePoint(c);
				}
				break;
			case SH1:
				if(CpreUtils.isCMacroIdentifierStart(c)) {
					b1 = new StringBuffer().appendCodePoint(c);
					st = S0.EX2;
				} else if(c == '#') {
					b0.append('#').append('#');
					st = S0.INI;
				} else if(c < 0) {
					return b0.append('#').toString();
				} else {
					b0.append('#').appendCodePoint(c);
					st = S0.INI;
				}
				break;
			case EX1:
				if(CpreUtils.isCMacroIdentifierPart(c)) {
					b1.append((char)c);
				} else if(c == '(') {
					nm = b1.toString();
					l0 = new ArrayList<String>();
					b2 = new StringBuffer().append('(');
					b1 = new StringBuffer();
					l  = 0;
					st = S0.AR1;
				} else {
					if(!sh || done.contains(b1.toString().trim())) {
						b0.append(b1.toString());
					} else if(b1.toString().trim().equals(
							"__LINE__")) {
						b0.append(lineno);
					} else {
						done.add(b1.toString().trim());
						s = macros.substitute(b1.toString().trim());
						b0.append(s != null ? s : b1);
					}

					if(c < 0)  return b0.toString();
					if(c == '#') {
						st = S0.SH1;
					} else {
						b0.appendCodePoint(c);
						st = S0.INI;
					}
				}
				break;
			case EX2:
				if(CpreUtils.isCMacroIdentifierPart(c)) {
					b1.append((char)c);
				} else if(c == '(') {
					nm = b1.toString();
					l0 = new ArrayList<String>();
					b2 = new StringBuffer().append('(');
					b1 = new StringBuffer();
					st = S0.AR2;
				} else {
					if(!sh || done.contains(b1.toString().trim())) {
						b0.append(b1.toString());
					} else {
						done.add(b1.toString().trim());
						b0.append('\"');
						s = macros.substitute(b1.toString().trim());
						b0.append(s != null ? s: b1);
					}

					if(c < 0)  return b0.append('\"').toString();
					if(c == '#') {
						st = S0.SH1;
					} else {
						b0.append('\"').appendCodePoint(c);
						st = S0.INI;
					}
				}
				break;
			case AR1:
				if(c == ')' && l == 0) {
					if(done.contains(nm.trim()) ||
							nm.equals("defined")) {
						b0.append(nm).append(b2);
						b0.append(b1).append(')');
					} else {
						done.add(nm.trim());
						l0.add(b1.toString().trim());
						s1 = macros.substitute(nm.trim(), lineno, l0);
						if(!sh || s1 != null) {
							b0.append(s1);
						} else {
							b2.append(b1).append(')');
//							b0.append(nm);
							done.remove(nm.trim());
							b0.append(substitute(
									new StringReader(nm.trim()),
									macros,
									new HashSet<String>(done),
									lineno,
									true));
							b0.append(substitute(
									new StringReader(b2.toString()),
									macros,
									new HashSet<String>(done),
									lineno,
									true));
						}
						b1 = new StringBuffer();
					}
					st = S0.INI;
				} else if(c == ')') {
					l--;
					b1.appendCodePoint(c);
				} else if(c == ',' && l == 0) {
					l0.add(b1.toString().trim());
					b2.append(b1).appendCodePoint(c);
					b1 = new StringBuffer();
				} else if(c < 0) {
					b0.append(substitute(
							new StringReader(nm.trim()),
							macros,
							new HashSet<String>(done),
							lineno,
							true));
					return b0.append(b2).append(b1).toString();
				} else {
					if(c == '(')  l++;
					b1.appendCodePoint(c);
				}
				break;
			case AR2:
				if(c == ')') {
					if(done.contains(nm.trim()) ||
							nm.equals("defined")) {
						b0.append(nm).append(b2);
						b0.append(b1).append(')');
					} else {
						done.add(nm.trim());
						l0.add(b1.toString().trim());
						b0.append('\"');
						s1 = macros.substitute(nm.trim(), lineno, l0);
						if(!sh || s1 != null) {
							b0.append(s1);
						} else {
							b2.append(b1).append(')');
							b0.append(nm);
							b0.append(substitute(
									new StringReader(b2.toString()),
									macros,
									new HashSet<String>(done),
									lineno,
									true));
						}
						b0.append('\"');
						b1 = new StringBuffer();
					}
					st = S0.INI;
				} else if(c == ',') {
					l0.add(b1.toString().trim());
					b1 = new StringBuffer();
					b2.append(substitute(
							new StringReader(b1.toString()),
							macros,
							new HashSet<String>(done),
							lineno,
							true));
					b2.appendCodePoint(c);
				} else if(c < 0) {
					return b0.append(b2).toString();
				} else {
					b1.appendCodePoint(c);
				}
				break;
			case DQ1:
				if(c < 0)  return b0.toString();
				b0.appendCodePoint(c);
				if(c == '"') {
					st = S0.INI;
				} else if(c == '\\') {
					st = S0.DQ2;
				}
				break;
			case DQ2:
				if(c < 0)  return b0.toString();
				b0.appendCodePoint(c);
				st = S0.DQ1;
				break;
			}
		}
	}

	static String substitutep2(Reader rd) throws IOException {
		StringBuffer b0, b1 = null;
		S1 st = S1.INI;
		int c, d;

		b0 = new StringBuffer();
		while(true) {
			c = rd.read();
			switch(st) {
			case INI:
				if(c == '#') {
					st = S1.SH1;
				} else if(c == '"') {
					b0.appendCodePoint(c);
					st = S1.DQ1;
				} else if(c < 0) {
					return b0.toString();
				} else {
					b0.appendCodePoint(c);
				}
				break;
			case SH1:
				if(c == '#') {
					b1 = new StringBuffer();
					st = S1.SH2;
				} else if(c == '"') {
					b0.append('#').append('"');
					st = S1.DQ1;
				} else if(c < 0) {
					return b0.append('#').toString();
				} else {
					b0.append('#').appendCodePoint(c);
					st = S1.INI;
				}
				break;
			case SH2:
				if(c == ' ' || c == '\t') {
					b1.append((char)c);
				} else if(c < 0) {
					b0.append('#').append('#').append(b1);
					return b0.toString();
				} else {
					for(int i = b0.length() - 1; i >= 0; i--) {
						if((d = b0.charAt(i)) == ' ' || d == '\t') {
							b0.deleteCharAt(i);
						} else {
							break;
						}
					}
					b0.appendCodePoint(c);
					st = S1.INI;
				}
				break;
			case DQ1:
				if(c == '"') {
					st = S1.DQ2;
				} else if(c < 0) {
					return b0.toString();
				}
				b0.appendCodePoint(c);
				break;
			case DQ2:
				if(c == ' ' || c == '\t') {
					b0.append((char)c);
				} else if(c == '"') {
					for(int i = b0.length() - 1; i >= 0; i--) {
						if((d = b0.charAt(i)) == ' ' || d == '\t') {
							b0.deleteCharAt(i);
						} else {
							break;
						}
					}
					b0.deleteCharAt(b0.length() - 1);
					st = S1.DQ1;
				} else if(c < 0) {
					return b0.toString();
				} else {
					b0.appendCodePoint(c);
					st = S1.INI;
				}
				break;
			}
		}
	}

	/**
	 * 
	 * @param s
	 * @param macros
	 * @return
	 */
	public static String substitute(String s, int lineno,
			CpreMacros macros) {
		Set<String> done = new HashSet<String>();
		StringReader r;
		String t = null;

		try {
			while(!s.equals(t)) {
				r = new StringReader(t = s);
				s = substitute(r, macros, done, lineno, true);
				r.close();
				r = new StringReader(s);
				s = substitutep2(r);  r.close();
			}
			return s;
		} catch(IOException e) {
			throw new RuntimeException(e);
		}
	}

	String readLine0() throws IOException {
		String s, t;

		while((s = rd.peek().readLine()) == null) {
			if(rd.size() > 1) {
				rd.pop().close();
			} else {
				rd.pop();
				return null;
			}
		}

		if(!s.endsWith("\\")) {
			return s;
		} else if((t = readLine0()) == null) {
			return s.substring(0, s.length() - 1);
		} else {
			return s.substring(0, s.length() - 1) + t;
		}
	}

	String[] getincpath() {
		String[] r;
		String ps = System.getProperty("path.separator");
		String s0 = System.getenv("INCLUDE");

		s0 = s0 != null ? s0 : "";
		r  = s0.split(ps);
		return r;
	}

	void openinc1(File fn) throws IOException {
		rd.push(new LineNumberReader(new InputStreamReader(
				new FileInputStream(fn))));
		fns.push(fn.toString());
	}

	boolean openinc(String fn) throws IOException {
		String[] pt = getincpath();
		File f;

		for(String d : pt) {
			if((f = new File(d, fn)).isFile()) {
				openinc1(f);
				return true;
			}
		}
		return false;
	}

	String[] split2(String s) {
		String[] r = new String[2];

		r[1] = "";
		for(int i = 0; i < s.length(); i++) {
			if(s.charAt(i) == ' ' || s.charAt(i) == '\t') {
				r[0] = s.substring(0, i);
				for(int j = i; j < s.length(); j++) {
					if(s.charAt(j) != ' ' && s.charAt(j) != '\t') {
						r[1] = s.substring(j);
						return r;
					}
				}
				return r;
			}
		}
		r[0] = s;
		return r;
	}

	void processInclude(String s, int nm) throws IOException {
		String ps = System.getProperty("path.separator");
		Matcher m;
		String fn, t;
		File f;

		if((m = PI1.matcher(s)).matches()) {
			fn = m.group(1);
			if(ps.equals(":")) {
				// UNIX
				f = new File("/usr/include", fn);
				if(f.isFile()) {
					openinc1(f);
				} else {
					throw new CpreSyntaxException(
							getLineNumber(), fn + " not found");
				}
			} else if(!openinc(m.group(1))) {
				throw new CpreSyntaxException(getLineNumber(), ERR001);
			}
		} else if((m = PI2.matcher(s)).matches()) {
			fn = m.group(1);
			if((f = new File(fn)).isFile()) {
				openinc1(f);
			} else if(!openinc(m.group(1))) {
				throw new CpreSyntaxException(getLineNumber(), ERR001);
			}
		} else if(nm > 0) {
			throw new CpreSyntaxException(getLineNumber(), ERR001);
		} else {
			t = substitute(s, rd.peek().getLineNumber(), macros);
			processInclude(t, nm + 1);
		}
	}

	String processLine() throws IOException {
		String s = readLine0();
		CpreDirective d;
		CpreIfState ifs;
		String[] s2;
		int ln;

		if(s == null)  return null;
		s2 = split2(s);
		ln = rd.peek().getLineNumber();

		// #include
		if(s.startsWith("#include")) {
			processInclude(s, 0);
			return processLine();
		}

		// other directives
		if((d = DIRS.get(s2[0])) != null) {
			switch(ifs = d.execute(macros, s2[1], ln, ifstat.peek())) {
			case BEGIN:
				ifstat.push(CpreIfState.PROCESSING);  return "";
			case READY:
				ifstat.push(CpreIfState.UNPROCESSED);  return "";
			case UNPROCESSED:
			case PROCESSING:
			case PROCESSED:
			case PROCESSING_ELSE:
			case DONE:
				ifstat.pop();  ifstat.push(ifs);  return "";
			case RETURN:
				if(ifstat.size() <= 1) {
					throw new CpreSyntaxException(
							getLineNumber(), "unbalanced #endif");
				}
				ifstat.pop();  return "";
			case STAY:  return "";
			case TOP:  throw new RuntimeException();
			}
		}

		// other lines
		return ifstat.peek().isProcessing() ?
				substitute(s, ln, macros) : "";
	}

	/**
	 * 
	 * @return
	 * @throws IOException
	 */
	public String readLine() throws IOException {
		String r = line;

		ptr = 0;
		if(line != null) {
			line = null;
		} else {
			while(r == null && rd.size() > 0)  r = processLine();
		}

		if(r == null && ifstat.size() > 1) {
			throw new CpreSyntaxException(-1, "unbalanced #if/#ifdef");
		}
		return r;
	}

	/**
	 * 
	 * @return
	 */
	public int getLineNumber() {
		return rd.peek().getLineNumber();
	}

	@Override
	public int read() throws IOException {
		if(line == null)  ptr = 0;
		while(line == null)  line = processLine();
		if(ptr < line.length()) {
			return line.charAt(ptr++);
		} else {
			line = null;
			return '\n';
		}
	}

	@Override
	public int read(char[] b, int off, int len) throws IOException {
		int c;

		for(int i = off; i < off + len; i++) {
			if(!rd.peek().ready()) {
				return i - off;
			} else if((c = read()) < 0) {
				return i - off;
			} else {
				b[i] = (char)c;
			}
		}
		return len;
	}

	/* (non-Javadoc)
	 * @see java.io.FilterReader#skip(long)
	 */
	@Override
	public long skip(long n) throws IOException {
		for(long i = 0; i < n; i++) {
			if(!rd.peek().ready() || read() < 0) {
				return i;
			}
		}
		return n;
	}

	/* (non-Javadoc)
	 * @see java.io.FilterReader#markSupported()
	 */
	@Override
	public boolean markSupported() {
		return false;
	}

	/* (non-Javadoc)
	 * @see java.io.FilterReader#mark(int)
	 */
	@Override
	public void mark(int readAheadLimit) throws IOException {
		throw new IOException();
	}

	/* (non-Javadoc)
	 * @see java.io.FilterReader#reset()
	 */
	@Override
	public void reset() throws IOException {
		throw new IOException();
	}

	@Override
	public void close() throws IOException {
		while(!rd.isEmpty())  rd.pop().close();
	}

}
