/*
 * 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.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Cプリプロセッサのマクロの空間です。
 *
 *
 * @author MORIGUCHI, Yuichiro 2013/04/13
 */
public class CpreMacros {

	private static enum S2 { INI, EX1, EX2, DQ1, DQ2 };

	private Map<String, String> macros;
	private Map<String, List<String>> macroArgs;

	/**
	 * マクロ空間を生成します。
	 */
	public CpreMacros() {
		this.macros = new HashMap<String, String>();
		this.macroArgs = new HashMap<String, List<String>>();
	}

	/**
	 * マクロ空間をコピーします。
	 * 
	 * @param m マクロ空間
	 */
	public CpreMacros(CpreMacros m) {
		this.macros = new HashMap<String, String>(m.macros);
		this.macroArgs =
			new HashMap<String, List<String>>(m.macroArgs);
	}

	String substituteArgs(Reader rd,
			Map<String, String> args) throws IOException {
		StringBuffer b0, b1 = null;
		S2 st = S2.INI;
		String t;
		int c;

		b0 = new StringBuffer();
		while(true) {
			c = rd.read();
			switch(st) {
			case INI:
				if(CpreUtils.isCMacroIdentifierStart(c)) {
					b1 = new StringBuffer().appendCodePoint(c);
					st = S2.EX1;
				} else if(c == '"') {
					b0.appendCodePoint(c);
					st = S2.DQ1;
				} else if(c == '#') {
					b1 = new StringBuffer();
					st = S2.EX2;
				} else if(c < 0) {
					return b0.toString();
				} else {
					b0.appendCodePoint(c);
				}
				break;
			case EX1:
				if(CpreUtils.isCMacroIdentifierPart(c)) {
					b1.append((char)c);
				} else {
					t = args.get(b1.toString().trim());
					b0.append(t != null ? t : b1);
					if(c < 0)  return b0.toString();
					if(c == '"') {
						b0.appendCodePoint(c);
						st = S2.DQ1;
					} else if(c == '#') {
						b1 = new StringBuffer();
						st = S2.EX2;
					} else {
						b0.appendCodePoint(c);
						st = S2.INI;
					}
				}
				break;
			case EX2:
				if(CpreUtils.isCMacroIdentifierPart(c)) {
					b1.append((char)c);
				} else if(c == '#') {
					b0.append('#').append('#');
					st = S2.INI;
				} else if(b1.length() > 0) {
					b0.append('"');
					t = args.get(b1.toString());
					if(t != null) {
						t = t.replaceAll("\\\\", "\\\\\\\\");
						t = t.replaceAll("\"", "\\\\\"");
					} else {
						b0.append(b1);
					}
					b0.append(t);
					b0.append('"');
					if(c < 0)  return b0.toString();
					if(c == '"') {
						b0.appendCodePoint(c);
						st = S2.DQ1;
					} else if(c == '#') {
						b1 = new StringBuffer();
						st = S2.EX2;
					} else {
						b0.appendCodePoint(c);
						st = S2.INI;
					}
				} else {
					b0.append('#');
					if(c < 0)  return b0.toString();
					if(c == '"') {
						b0.appendCodePoint(c);
						st = S2.DQ1;
					} else if(c == '#') {
						b1 = new StringBuffer();
						st = S2.EX2;
					} else {
						b0.appendCodePoint(c);
						st = S2.INI;
					}
				}
				break;
			case DQ1:
				if(c == '"') {
					st = S2.INI;
				} else if(c == '\\') {
					st = S2.DQ2;
				} else if(c < 0) {
					return b0.toString();
				}
				b0.appendCodePoint(c);
				break;
			case DQ2:
				if(c < 0)  return b0.toString();
				b0.appendCodePoint(c);
				st = S2.DQ1;
				break;
			}
		}
	}

	/**
	 * マクロを定義します。
	 * 
	 * @param name  名称
	 * @param value 値
	 */
	public void define(String name, String value) {
		macros.put(name, value);
	}

	/**
	 * 引数月ののマクロを定義します。
	 * 
	 * @param name  名称
	 * @param value 値
	 * @param args  引数
	 */
	public void define(String name, String value, List<String> args) {
		macros.put(name, value);
		if(args != null) {
			macroArgs.put(name, new ArrayList<String>(args));
		}
	}

	/**
	 * マクロを置き換えます。
	 * 
	 * @param name 名称
	 * @return
	 */
	public String substitute(String name) {
		return macros.get(name);
	}

	/**
	 * マクロを置き換えます。
	 * 
	 * @param name   名称
	 * @param lineno 行番号
	 * @param args   引数
	 * @return
	 */
	public String substitute(String name, int lineno,
			List<String> args) {
		Map<String, String> m;
		StringReader r;
		List<String> a;
		String s;

		if(name.equals("defined") ||
				(a = macroArgs.get(name)) == null) {
			return null;
		} else if(a.size() != args.size()) {
			throw new CpreSyntaxException(
					lineno,
					("wrong number of arguments: require " +
							a.size() + " but " + args.size()));
		} else {
			try {
				m = new HashMap<String, String>();
				for(int i = 0; i < a.size(); i++) {
					m.put(a.get(i), args.get(i));
				}
				r = new StringReader(macros.get(name));
				s = substituteArgs(r, m);
				return s;
			} catch(IOException e) {
				throw new RuntimeException(e);
			}
		}
	}

	/**
	 * マクロの定義を解除します。
	 * 
	 * @param name 名称
	 */
	public void undef(String name) {
		macros.remove(name);
	}

	/**
	 * マクロが定義されているかを調べます。
	 * 
	 * @param name 名称
	 * @return 定義されているときにtrue
	 */
	public boolean isDefined(String name) {
		return macros.containsKey(name);
	}

}
