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

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import net.morilib.sh.arith.ShExprLexer;
import net.morilib.sh.arith.ShExprParser;
import net.morilib.sh.arith.ShExprSyntaxException;
import net.morilib.sh.misc.XtraceStream;
import net.morilib.unix.glob.Wildcard;

public class ShString extends ShToken {

	private static enum S1 {
		INIT, ESC1, SQUT, DOLR, PRAM, DQUT, ESC2, DDLR, DPRM
	}

	private static enum S2 {
		INIT, ESC1, SQUT, DOLR, COMD, CMD2, NUM1, NUM2, NUM3, NUM4
	}

	private static enum S3 { INIT, ESC1, SQUT, DQUT, ESC2, SPLT }
	private static enum S4 { INIT, ESC1, SQUT, DOLR, DLR2, ARIT, ARI2 }

	private static final InputStream NULL_INS = new InputStream() {

		@Override
		public int read() {
			return -1;
		}

		@Override
		public void close() {
			// do nothing
		}

		@Override
		public int read(byte[] b, int off, int len) {
			return -1;
		}

		@Override
		public int read(byte[] b) {
			return -1;
		}

		@Override
		public long skip(long n) {
			return 0;
		}

	};

	private static final PrintStream
	NULL_OUS = new PrintStream(new OutputStream() {

		@Override
		public void write(int b) {
			// do nothing
		}

		@Override
		public void close() {
			// do nothing
		}

		@Override
		public void flush() {
			// do nothing
		}

		@Override
		public void write(byte[] b, int off, int len) {
			// do nothing
		}

		@Override
		public void write(byte[] b) {
			// do nothing
		}

	});

	private static final String D_MSG = "parameter null or not set";
	private static final Pattern PTN_SLICE =
		Pattern.compile("(.+):([0-9]+):([0-9]+)");

	private String value;

	public ShString(String value) {
		this.value = value;
	}

	public String getValue() {
		return value;
	}

	@Override
	public boolean isString() {
		return true;
	}

	@Override
	public List<ShToken> replaceBracket() {
		return Collections.<ShToken>singletonList(this);
	}

	@Override
	public ShToken replaceTilde(ShEnvironment env) {
		return new ShString(ShToken.replaceTilde(env, value));
	}

	private int getargnum(ShEnvironment env) {
		try {
			return Integer.parseInt(env.find("#"));
		} catch(NumberFormatException e) {
			return 0;
		}
	}

	private StringBuffer setvar(ShEnvironment env, String ifs,
			PrintStream err, StringBuffer b,
			List<ShToken> l, String q, String s) {
		String d, v;
		int m, x, y;
		Matcher t;
//		int[] a;

		d = "";
		if(s.equals("*")) {
			m = getargnum(env);
			for(int k = 1; k < m; k++) {
				b.append(d).append(env.find(k + ""));
				if(d.equals(""))  d = ifs.substring(0, 1);
			}
		} else if(s.equals("@")) {
			m = getargnum(env);
			for(int k = 1; k < m; k++) {
				if(k > 1)  b = new StringBuffer(q);
				b.append(env.find(k + ""));
				if(k < m - 1) {
					l.add(new ShString(b.append(q).toString()));
				}
			}
		} else if((x = s.indexOf(":-")) > 0) {
			v = env.find(s.substring(0, x));
			v = v == null || v.equals("") ? s.substring(x + 2) : v;
			b.append(v);
		} else if((x = s.indexOf(":=")) > 0) {
			v = env.find(s.substring(0, x));
			if(v == null || v.equals("")) {
				v = s.substring(x + 2);
				env.bind(s.substring(0, x), v);
			}
			b.append(v);
		} else if((x = s.indexOf(":?")) > 0) {
			v = env.find(s.substring(0, x));
			if(v != null && !v.equals("")) {
				b.append(v);
			} else {
				err.print(s.substring(0, x));
				err.print(": ");
				v = s.substring(x + 2);
				err.println(v.equals("") ? D_MSG : v);
				throw new ShRuntimeException();
			}
		} else if((x = s.indexOf(":+")) > 0) {
			v = env.find(s.substring(0, x));
			v = v == null || v.equals("") ? "" : s.substring(x + 2);
			b.append(v);
		} else if((t = PTN_SLICE.matcher(s)).matches()) {
			v = env.find(t.group(1));
			x = Integer.parseInt(t.group(2));
			y = Integer.parseInt(t.group(3));
			if(x >= v.length()) {
				return b;
			} else if(y + x >= v.length()) {
				y = v.length() - x;
			}
			b.append(v.substring(x, x + y));
		} else if((x = s.indexOf("##")) > 0) {
			v = env.find(s.substring(0, x));
			y = Wildcard.compile(
					s.substring(x + 2)).lookingAtLongest(v, 0);
			b.append(y < 0 ? v : v.substring(y));
		} else if((x = s.indexOf('#')) > 0) {
			v = env.find(s.substring(0, x));
			y = Wildcard.compile(
					s.substring(x + 1)).lookingAtShortest(v, 0);
			b.append(y < 0 ? v : v.substring(y));
		} else if((x = s.indexOf("%%")) > 0) {
			v = env.find(s.substring(0, x));
			y = Wildcard.compile(
					s.substring(x + 2)).lastLookingAtLongest(v);
			b.append(y < 0 ? v : v.substring(0, y));
		} else if((x = s.indexOf('%')) > 0) {
			v = env.find(s.substring(0, x));
			y = Wildcard.compile(
					s.substring(x + 1)).lastLookingAtShortest(v);
			b.append(y < 0 ? v : v.substring(0, y));
		} else {
			b.append((v = env.find(s)) != null ? v : "");
		}
		return b;
	}

	@Override
	public List<ShToken> replaceParameter(ShEnvironment env,
			PrintStream err) {
		List<ShToken> l = new ArrayList<ShToken>();
		StringBuffer b = new StringBuffer(), n = null;
		String ifs = env.find("IFS");
		S1 stat = S1.INIT;
		int c;

		for(int i = 0; i < value.length(); i++) {
			c = value.charAt(i);
			switch(stat) {
			case INIT:
				if(c == '\'') {
					b.append((char)c);
					stat = S1.SQUT;
				} else if(c == '\"') {
					b.append((char)c);
					stat = S1.DQUT;
				} else if(c == '$') {
					stat = S1.DOLR;
				} else {
					b.append((char)c);
					if(c == '\\')  stat = S1.ESC1;
				}
				break;
			case ESC1:
				b.append((char)c);
				stat = S1.INIT;
				break;
			case SQUT:
				b.append((char)c);
				if(c == '\'')  stat = S1.INIT;
				break;
			case DOLR:
				if(c == '{') {
					n = new StringBuffer();
					stat = S1.PRAM;
				} else {
					b.append('$').append((char)c);
					stat = S1.INIT;
				}
				break;
			case PRAM:
				if(c == '}') {
					b = setvar(env, ifs, err, b, l, "", n.toString());
					stat = S1.INIT;
				} else {
					n.append((char)c);
				}
				break;
			case DQUT:
				if(c == '$') {
					stat = S1.DDLR;
				} else {
					b.append((char)c);
					if(c == '\"')  stat = S1.INIT;
					if(c == '\\')  stat = S1.ESC2;
				}
				break;
			case ESC2:
				b.append((char)c);
				stat = S1.DQUT;
				break;
			case DDLR:
				if(c == '{') {
					n = new StringBuffer();
					stat = S1.DPRM;
				} else {
					b.append('$').append((char)c);
					stat = S1.DQUT;
				}
				break;
			case DPRM:
				if(c == '}') {
					b = setvar(env, ifs, err, b, l, "\"",
							n.toString());
					stat = S1.INIT;
				} else {
					n.append((char)c);
				}
				break;
			}
		}

		if(stat.equals(S1.PRAM)) {
			b.append("${").append(n.toString());
		}
//		l.add(new ShString(b.toString()));
		if(b.length() > 0)  l.add(new ShString(b.toString()));
		return l;
	}

	private void appendcommand(ShEnvironment env,
			ShFileSystem fs, ShRuntime run, XtraceStream prompt,
			StringBuffer b,
			String n) throws IOException, ShSyntaxException {
		ByteArrayOutputStream stdout;
		byte[] a;
		int k;

		stdout = new ByteArrayOutputStream();
		prompt.upLevel();
		run.eval(env,
				fs,
				NULL_INS,
				new PrintStream(stdout),
				NULL_OUS,
				prompt,
				n);
		prompt.downLevel();
		a = stdout.toByteArray();
		for(k = a.length; --k >= 0 && (a[k] == '\n' || a[k] == '\r'););
		b.append(new String(stdout.toByteArray(), 0, k + 1,
				env.getCharset()));
	}

	@Override
	public ShToken replaceCommand(ShEnvironment env,
			ShFileSystem fs, ShRuntime run,
			XtraceStream p) throws IOException, ShSyntaxException {
		StringBuffer b, n = null;
		S2 stat = S2.INIT;
		int c;

		b = new StringBuffer();
		for(int i = 0; i < value.length(); i++) {
			c = value.charAt(i);
			switch(stat) {
			case INIT:
				if(c == '\'') {
					b.append((char)c);
					stat = S2.SQUT;
				} else if(c == '$') {
					stat = S2.DOLR;
				} else {
					b.append((char)c);
					if(c == '\\')  stat = S2.ESC1;
				}
				break;
			case ESC1:
				b.append((char)c);
				stat = S2.INIT;
				break;
			case SQUT:
				b.append((char)c);
				if(c == '\'')  stat = S2.INIT;
				break;
			case DOLR:
				if(c == '(') {
					n = new StringBuffer();
					stat = S2.COMD;
				} else {
					b.append('$').append((char)c);
					stat = S2.INIT;
				}
				break;
			case COMD:
				if(c == '(') {
					b.append("$((");
					stat = S2.NUM1;
				} else if(c == ')') {
					stat = S2.INIT;
				} else {
					n.append((char)c);
					stat = S2.CMD2;
				}
				break;
			case CMD2:
				if(c == ')') {
					appendcommand(env, fs, run, p, b, n.toString());
					stat = S2.INIT;
				} else {
					n.append((char)c);
				}
				break;
			case NUM1:
				if(c == ')') {
					b.append(')');
					stat = S2.NUM2;
				} else if(c == '$') {
					stat = S2.NUM3;
				} else {
					b.append((char)c);
				}
				break;
			case NUM2:
				if(c == ')') {
					b.append(')');
					stat = S2.INIT;
				} else if(c == '$') {
					stat = S2.NUM3;
				} else {
					b.append((char)c);
					stat = S2.INIT;
				}
				break;
			case NUM3:
				if(c == '(') {
					n = new StringBuffer();
					stat = S2.NUM4;
				} else {
					b.append('$').append((char)c);
					stat = S2.NUM1;
				}
				break;
			case NUM4:
				if(c == ')') {
					appendcommand(env, fs, run, p, b, n.toString());
					stat = S2.NUM1;
				} else {
					n.append((char)c);
				}
				break;
			}
		}

		if(stat.equals(S2.COMD) || stat.equals(S2.CMD2)) {
			b.append("$(").append(n.toString());
		}
		return new ShString(b.toString());
	}

	@Override
	public ShToken replaceArithmetic(
			ShEnvironment env) throws ShSyntaxException {
		StringBuffer b = new StringBuffer(), n = null;
		S4 stat = S4.INIT;
		ShExprLexer r;
		int c;

		try {
			for(int i = 0; i < value.length(); i++) {
				c = value.charAt(i);
				switch(stat) {
				case INIT:
					if(c == '\'') {
						b.append((char)c);
						stat = S4.SQUT;
					} else if(c == '$') {
						stat = S4.DOLR;
					} else {
						b.append((char)c);
						if(c == '\\')  stat = S4.ESC1;
					}
					break;
				case ESC1:
					b.append((char)c);
					stat = S4.INIT;
					break;
				case SQUT:
					b.append((char)c);
					if(c == '\'')  stat = S4.INIT;
					break;
				case DOLR:
					if(c == '(') {
						n = new StringBuffer();
						stat = S4.DLR2;
					} else {
						b.append('$').append((char)c);
						stat = S4.INIT;
					}
					break;
				case DLR2:
					if(c == '(') {
						n = new StringBuffer();
						stat = S4.ARIT;
					} else {
						b.append('$').append('(').append((char)c);
						stat = S4.INIT;
					}
					break;
				case ARIT:
					if(c == ')') {
						stat = S4.ARI2;
					} else {
						n.append((char)c);
					}
					break;
				case ARI2:
					if(c == ')') {
						r = new ShExprLexer(
								new StringReader(n.toString()));
						b.append(ShExprParser.parseExpression(
								env, r).toString());
						stat = S4.INIT;
					} else {
						n.append(')').append((char)c);
						stat = S4.ARIT;
					}
					break;
				}
			}
			return new ShString(b.toString());
		} catch(ShExprSyntaxException e) {
			throw new ShSyntaxException();
		} catch (IOException e) {
			throw new ShSyntaxException();
		}
	}

	@Override
	public List<String> splitWord(String ifs) {
		S3 stat = S3.INIT;
		StringBuffer b;
		List<String> l;
		int c;

		if(ifs.length() > 0) {
			b = new StringBuffer();
			l = new ArrayList<String>();
			for(int i = 0; i < value.length(); i++) {
				c = value.charAt(i);
				switch(stat) {
				case INIT:
					if(ifs.indexOf(c) < 0) {
						b.append((char)c);
						if(c == '\'')       stat = S3.SQUT;
						else if(c == '\"')  stat = S3.DQUT;
						else if(c == '\\')  stat = S3.ESC1;
						else                stat = S3.SPLT;
					}
					break;
				case SPLT:
					if(ifs.indexOf(c) >= 0) {
						l.add(b.toString());
						b = new StringBuffer();
						stat = S3.INIT;
					} else {
						b.append((char)c);
						if(c == '\'')       stat = S3.SQUT;
						else if(c == '\"')  stat = S3.DQUT;
						else if(c == '\\')  stat = S3.ESC1;
					}
					break;
				case ESC1:
					b.append((char)c);
					stat = S3.SPLT;
					break;
				case SQUT:
					b.append((char)c);
					if(c == '\'')  stat = S3.SPLT;
					break;
				case DQUT:
					b.append((char)c);
					if(c == '\"')       stat = S3.SPLT;
					else if(c == '\\')  stat = S3.ESC2;
					break;
				case ESC2:
					b.append((char)c);
					stat = S3.DQUT;
					break;
				}
			}
			if(b.length() > 0)  l.add(b.toString());
			return l;
		} else {
			return Collections.singletonList(value);
		}
	}

	@Override
	public boolean isWildcard() {
		return ShToken.isWildcard(value);
	}

	@Override
	public boolean isBind() {
		return ShToken.isBind(value);
	}

	@Override
	public void bindVariable(ShEnvironment env,
			ShFileSystem fs, ShRuntime run, PrintStream err,
			XtraceStream p) throws IOException, ShSyntaxException {
		ShToken.bindVariable(env, fs, run, err, p, value);
	}

	@Override
	public int hashCode() {
		return value.hashCode();
	}

	@Override
	public boolean equals(Object o) {
		return (o instanceof ShString &&
				value.equals(((ShString)o).value));
	}

	@Override
	public String toString() {
		return value;
	}

}
