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

import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;

import net.morilib.sh.ShEnvironment;
import net.morilib.sh.ShFileSystem;
import net.morilib.sh.ShProcess;

public class ShCat implements ShProcess {

	private static final int NUMBER = 1;
	private static final int NUMBER_NONBLANK = 2;
	private static final int SQUEEZE_BLANK = 4;
	private static final int SHOW_NONPRINTING = 8;
	private static final int SHOW_ENDS = 16;
	private static final int SHOW_TABS = 32;
	private static final int BINARY = 64;
	private static final char[] CARET = new char[] {
		'@', 'A',  'B', 'C', 'D', 'E', 'F', 'G', 'H',
		'I', 'J',  'K', 'L', 'M', 'N', 'O', 'P', 'Q',
		'R', 'S',  'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
		'[', '\\', ']', '^', '_'
	};

	private void printcaret(OutputStream out,
			int c) throws IOException {
		if(c < 0x20) {
			out.write('^');
			out.write(CARET[c]);
		} else if(c == 0x7f) {
			out.write('^');
			out.write('?');
		} else {
			out.write((char)c);
		}
	}

	private int analyzeopt(String s, int[] f) {
		for(int i = 1; i < s.length(); i++) {
			switch(s.charAt(i)) {
			case 'b':  f[0] |= NUMBER_NONBLANK;  break;
			case 'e':
				f[0] |= SHOW_NONPRINTING;
				f[0] |= SHOW_ENDS;
				break;
			case 'n':  f[0] |= NUMBER;  break;
			case 's':  f[0] |= SQUEEZE_BLANK;  break;
			case 't':
				f[0] |= SHOW_NONPRINTING;
				f[0] |= SHOW_TABS;
				break;
			case 'u':  break;
			case 'v':  f[0] |= SHOW_NONPRINTING;  break;
			case 'A':
				f[0] |= SHOW_NONPRINTING;
				f[0] |= SHOW_ENDS;
				f[0] |= SHOW_TABS;
				break;
			case 'B':  f[0] |= BINARY;  break;
			case 'E':  f[0] |= SHOW_ENDS;  break;
			case 'T':  f[0] |= SHOW_TABS;  break;
			default:  return s.charAt(i);
			}
		}
		return -1;
	}

	private boolean isblank(ByteArrayOutputStream b, int fl) {
		byte[] a;

		if(b.size() > 3) {
			return false;
		} else if(b.size() == 0) {
			return true;
		} else if((a = b.toByteArray()).length == 1) {
			return a[0] == '\n';
		} else if(a.length == 2 && (fl & SHOW_ENDS) == 0) {
			return a[0] == '\r' && a[1] == '\n';
		} else if(a.length == 2 && (fl & SHOW_ENDS) != 0) {
			return a[0] == '$' && a[1] == '\n';
		} else if(a.length == 3 && (fl & SHOW_ENDS) != 0) {
			return a[0] == '$' && a[1] == '\r' && a[2] == '\n';
		} else {
			return false;
		}
	}

	private void write1(OutputStream out, int fl,
			int c) throws IOException {
		if(c == '\t') {
			if((fl & SHOW_TABS) != 0) {
				out.write('^');
				out.write('I');
			} else {
				out.write((char)c);
			}
		} else if(c < 0x20 || c == 0x7f) {
			if((fl & SHOW_NONPRINTING) != 0) {
				printcaret(out, c);
			} else {
				out.write((char)c);
			}
		} else if(c >= 0x80) {
			if((fl & SHOW_NONPRINTING) != 0) {
				out.write('M');  out.write('-');
				printcaret(out, (char)(c - 0x80));
			} else {
				out.write((char)c);
			}
		} else {
			out.write((char)c);
		}
	}

	private boolean catline(InputStream in, OutputStream out,
			int fl) throws IOException {
		int c, k;

		for(k = 0; (c = in.read()) >= 0; k++) {
			if(c == '\n') {
				if((fl & SHOW_ENDS) != 0)  out.write('$');
				out.write((char)c);
				return true;
			} else if(c != '\r' || (fl & BINARY) != 0) {
				write1(out, fl, (char)c);
			} else if((c = in.read()) == '\n') {
				if((fl & SHOW_ENDS) != 0)  out.write('$');
				out.write((char)c);
				return true;
			} else {
				write1(out, fl, '\r');
				write1(out, fl, (char)c);
			}
		}
		return k > 0;
	}

	private void cat(InputStream ins, PrintStream out,
			int fl, int[] n) throws IOException {
		ByteArrayOutputStream b = new ByteArrayOutputStream(), d;
		BufferedInputStream in = new BufferedInputStream(ins);
		boolean s = false, t = false;

		d = null;
		while(t || catline(in, b, fl)) {
			if(t) {
				t = false;
				b = d;  d = null;
			} else if((fl & SQUEEZE_BLANK) != 0 && isblank(b, fl)) {
				b = new ByteArrayOutputStream();
				s = true;
				continue;
			} else if(s) {
				t = s;  s = false;
				d = b;
				b = new ByteArrayOutputStream();
				if((fl & SHOW_ENDS) != 0)  out.write('$');
				b.write('\n');
			}

			if(((fl & NUMBER_NONBLANK) != 0 && !isblank(b, fl)) ||
					(fl & NUMBER) != 0) {
				out.print(String.format("%7d ", n[0]));
				n[0]++;
			}
			out.write(b.toByteArray());
			b = new ByteArrayOutputStream();
		}
	}

	public int main(ShEnvironment env,
			ShFileSystem fs,
			InputStream in,
			PrintStream out,
			PrintStream err,
			String... args) throws IOException {
		InputStream ins = null;
		int[] f = new int[1];
		int[] n = new int[1];
		int k = 1;

		f[0] = 0;
		for(; k < args.length; k++) {
			if(args[k].startsWith("-") &&
					!args[k].startsWith("--")) {
				if(analyzeopt(args[k], f) >= 0) {
					err.println("cat: invalid option");
					return 2;
				}
			} else if(args[k].equals("--number")) {
				f[0] |= NUMBER;
			} else if(args[k].equals("--number-nonblank")) {
				f[0] |= NUMBER_NONBLANK;
			} else if(args[k].equals("--squeeze-blank")) {
				f[0] |= SQUEEZE_BLANK;
			} else if(args[k].equals("--show-nonprinting")) {
				f[0] |= SHOW_NONPRINTING;
			} else if(args[k].equals("--show-all")) {
				f[0] |= SHOW_NONPRINTING;
				f[0] |= SHOW_ENDS;
				f[0] |= SHOW_TABS;
			} else if(args[k].equals("--show-ends")) {
				f[0] |= SHOW_ENDS;
			} else if(args[k].equals("--show-tabs")) {
				f[0] |= SHOW_TABS;
			} else if(args[k].equals("--binary")) {
				f[0] |= BINARY;
			} else if(args[k].equals("--")) {
				k++;
				break;
			} else if(args[k].equals("-") ||
					!args[k].startsWith("-")) {
				break;
			} else {
				err.println("cat: invaild arguments");
				return 2;
			}
		}

		n[0] = 1;
		if(k >= args.length) {
			cat(in, out, f[0], n);
		} else {
			for(; k < args.length; k++) {
				if(args[k].equals("-")) {
					cat(in, out, f[0], n);
				} else {
					try {
						ins = fs.getFile(args[k]).getInputStream();
						if(ins == null) {
							err.print("cat: ");
							err.print(args[k]);
							err.println(": file not found");
							return 2;
						}
						cat(ins, out, f[0], n);
					} finally {
						if(ins != null)  ins.close();
						ins = null;
					}
				}
			}
		}
		return 0;
	}

}
