/*
 * 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.awk.printf;

import java.io.IOException;
import java.io.StringWriter;
import java.io.Writer;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

import net.morilib.awk.value.AwkUndefined;
import net.morilib.awk.value.AwkValue;

/**
 * printfのフォーマットを解析して実行します。
 * 
 * 
 * @author MORIGUCHI, Yuichiro 2013/05/11
 */
public class Printf {

	static enum S1 { INIT, FLAG, WIDT, WID2, PREC, PRC2 }

	static final int ZERO = 1;
	static final int SIGN = 2;
	static final int SIGN_BLANK = 4;
	static final int LEFT = 8;
	static final int SIGN_MASK = 6;

	private Map<Integer, PrintfAction> actions;
	private int fchr;

	/**
	 * printf解析器を生成します。
	 * 
	 * @param fchr    書式の文字
	 * @param actions アクションのMap
	 */
	public Printf(int fchr, Map<Integer, PrintfAction> actions) {
		this.fchr = fchr;
		this.actions = new HashMap<Integer, PrintfAction>(actions);
	}

	//
	Printf(int fchr, Map<Integer, PrintfAction> actions, boolean d) {
		this.fchr = fchr;
		this.actions = actions;
	}

	/**
	 * デフォルトのprintf解析器を生成します。
	 * 
	 * @return デフォルトのprintf解析器
	 */
	public static Printf getInstance() {
		Map<Integer, PrintfAction> a;

		a = new HashMap<Integer, PrintfAction>();
		a.put((int)'s', new PrintfActionS());
		a.put((int)'d', new PrintfActionInteger(10, false));
		a.put((int)'x', new PrintfActionInteger(16, false));
		a.put((int)'X', new PrintfActionInteger(16, true));
		a.put((int)'o', new PrintfActionInteger( 8, false));
		a.put((int)'e', new PrintfActionE());
		a.put((int)'f', new PrintfActionF());
		a.put((int)'g', new PrintfActionG());
		a.put((int)'c', new PrintfActionC());
		return new Printf('%', a, false);
	}

	/**
	 * 書式をフォーマットします。
	 * 
	 * @param b      Writer
	 * @param format 書式
	 * @param args   引数
	 * @throws IOException IOエラー
	 */
	public void format(Writer b, String format,
			final Object... args) throws IOException {
		int c, flg = 0, w = -1, p = -1, t = -1;
		PrintfAction a;
		S1 s = S1.INIT;
		Iterator<AwkValue> ai = new Iterator<AwkValue>() {
			
			int p0 = 0;

			public boolean hasNext() {
				return true;
			}

			public AwkValue next() {
				return p0 < args.length ?
						(AwkValue)args[p0++] : AwkUndefined.UNDEF;
			}

			public void remove() {
				throw new UnsupportedOperationException();
			}

		};

		for(int i = 0; i < format.length(); i += c > 0xffff ? 2 : 1) {
			c = format.charAt(i);
			switch(s) {
			case INIT:
				if(c == fchr) {
					t = i;
					s = S1.FLAG;
				} else {
					b.append((char)c);
				}
				break;
			case FLAG:
				if(c == fchr) {
					b.append((char)c);
					s = S1.INIT;
				} else if(c == '0' && (flg & ZERO) == 0) {
					flg |= ZERO;
				} else if(c == '+' && (flg & SIGN_MASK) == 0) {
					flg |= SIGN;
				} else if(c == ' ' && (flg & SIGN_MASK) == 0) {
					flg |= SIGN_BLANK;
				} else if(c == '-' && (flg & LEFT) == 0) {
					flg |= LEFT;
				} else if(c == '*') {
					w = ai.next().toInteger().intValue();
					s = S1.WID2;
				} else if(c >= '1' && c <= '9') {
					w = c - '0';
					s = S1.WIDT;
				} else if(c == '.') {
					p = 0;
					s = S1.PREC;
				} else {
					if((a = actions.get(c)) == null) {
						b.append(format.subSequence(t, i + 1));
					} else {
						a.action(b, flg, w, p, ai.next());
					}
					s = S1.INIT;
				}
				break;
			case WIDT:
				if(c >= '0' && c <= '9') {
					w = w * 10 + (c - '0');
					break;
				}
				// CONITINUE
			case WID2:
				if(c == '.') {
					p = 0;
					s = S1.PREC;
				} else {
					if((a = actions.get(c)) == null) {
						b.append(format.subSequence(t, i + 1));
					} else {
						a.action(b, flg, w, p, ai.next());
					}
					s = c == fchr ? S1.FLAG : S1.INIT;
				}
				break;
			case PREC:
				if(c >= '0' && c <= '9') {
					p = p * 10 + (c - '0');
					break;
				} else if(c == '*') {
					p = ai.next().toInteger().intValue();
					s = S1.PRC2;
					break;
				}
				// CONITINUE
			case PRC2:
				if((a = actions.get(c)) == null) {
					b.append(format.subSequence(t, i + 1));
				} else {
					a.action(b, flg, w, p, ai.next());
				}
				s = c == fchr ? S1.FLAG : S1.INIT;
				break;
			}
		}
	}

	/**
	 * 書式をフォーマットし、結果の文字列を返します。
	 * 
	 * @param format 書式
	 * @param args   引数
	 * @return 結果
	 */
	public String format(String format, Object... args) {
		StringWriter b = new StringWriter();

		try {
			format(b, format, args);
			return b.toString();
		} catch (IOException e) {
			throw new RuntimeException(e);
		}
	}

	private static void fill(Writer b, char c, int s,
			int e) throws IOException {
		for(int i = s; i < e; i++)  b.append(c);
	}

	static void indent(Writer b, String s, char c, int flags,
			int e) throws IOException {
		if((flags & Printf.LEFT) != 0) {
			b.append(s);
			fill(b, c, s.length(), e);
		} else {
			fill(b, c, s.length(), e);
			b.append(s);
		}
	}

	static void indentNumber(Writer b, String s, int flags,
			int w) throws IOException {
		char f;

		if(w < 0 && w <= s.length()) {
			b.append(s);
		} else if((flags & LEFT) == 0 && (flags & ZERO) != 0) {
			if((f = s.charAt(0)) == '+' || f == '-' || f == ' ') {
				s = s.substring(1);
				b.append(f);
				w--;
			}
			Printf.indent(b, s, '0', flags, w);
		} else {
			Printf.indent(b, s, ' ', flags, w);
		}
	}

	private static void flagsToString(StringBuffer b, int flags) {
		if((flags & ZERO) != 0)  b.append('0');
		if((flags & SIGN) != 0)  b.append('+');
		if((flags & SIGN_BLANK) != 0)  b.append(' ');
		if((flags & LEFT) != 0)  b.append('-');
	}

	static String formatToString(int flags, int w, int p) {
		StringBuffer b = new StringBuffer("%");

		flagsToString(b, flags);
		if(w >= 0)  b.append(w);
		if(p >= 0)  b.append(".").append(p);
		return b.toString();
	}

}
