package ch.wlkl.shell;

import java.util.ArrayList;
import java.util.Collection;

/**
 * Cat creates {@link OpenClose} from a specification or manages a list of {@link OpenClose} for concatenated reading or writing:
 * <ul>
 * <li>{@link #make(String, Object)} creates an {@link OpenClose} from a specification
 * <li> an instance of Cat contains a list of {@link OpenClose}. It may either read one after the other or write to the first. open and close options may vary for each member in the list.
 * </ul>
 * 
 * @author walter keller
 *
 */
public class Cat<T> extends Top implements Read<T>, Write<T> {
	public final static char eNoOpenClose = '-';
	public final static char eContinue = '+';
	public final static char eObject = '£';
	public final static char eExtFD = '&';
	public final static String eExtFDStr = String.valueOf(eExtFD);
	public final static char eBufVar = '#';

	private final ArrayList<OpenClose> ios = new ArrayList<OpenClose>();
	private final ArrayList<String> opts = new ArrayList<String>();

	private Read<T> reader = null;
	int readIx = -9;
	private Write<T> writer = null;

	public Cat () {
	}

	/**
	 * return a new Cat as the concatenation of args, using {@link #reset(Object[])}
	 */
	public Cat (Object... args) {
		this();
		reset(args);
	}

	/**
	 * return a newly created OpenClose derived from argument String fn.
	 * <ul>
	 * <li> if opt starts with # {@link #eBufVar} and obj is not defined in the variable pool, return a new {@link Buf} after putting in the variable pool and opening it with argument opt.
	 * <li> if opt starts with # {@link #eBufVar} and obj is defined in the variable pool, return its value from the variable pool after opening it with argument opt.
	 * <li> if opt starts with & {@link #eExtFD} return an external FD reader/writer using {@link ExtFD#ExtFD(Object[])}.
	 * <li> otherwise if opt is a String return a {@link FileRW} using {@link FileRW#FileRW(String)}rn
	 * <li> otherwise if opt contains a £ {@link #eObject} return
	 * <ul><li>obj, if obj is instanceof {@link OpenClose}.
	 * <li>{@link Buf#Buf(Collection)}, if obj is instanceof {@link Collection}.
	 * <li> otherwise return a {@link FileRW} using {@link FileRW#FileRW(String)}.
	 * </ul>
	 */

	@SuppressWarnings("unchecked")
	public static OpenClose<?> make(String opt, Object obj) {
		String oo = opt == null || opt.length() < 1 ? " " : opt; 
		String fn;
		if (oo.indexOf(eBufVar) >= 0) {
			return EnvMan.env().isDefined(obj) ? make(oo.replace(eBufVar, '£'), EnvMan.env().via(obj)) : EnvMan.env().put(obj, new Buf<String>());
		} else if (oo.indexOf(eExtFD) >= 0) {
			return new ExtFD(opt, obj);
		} else  if (oo.indexOf(eObject) >= 0) {
			if (obj instanceof OpenClose)
				return (OpenClose<?>) obj;
			else if (obj instanceof Collection)
				return new Buf<Object>((Collection<Object>) obj);
		} else if ((obj instanceof String) && null != (fn = (String) obj)) {
			return new FileRW(fn);
		}
		sFail("make(" + opt + ", " + obj + ") not implemented");
		return null;
	}

	public static String normalizeOpt(Object arg) {
		if (arg == null)
			return " ";
		else if (! (arg instanceof String))
			sFail("option not a String: " + arg);
		String opt = (String) arg;
		if (opt.length() < 1)
			return " ";
		return opt.startsWith(">>") ? "a" + opt.substring(2) : opt.charAt(0) == '>' ? "w" + opt.substring(1) : opt.charAt(0) == '<' ? "r" + opt.substring(1) : "rwa ".indexOf(opt.charAt(0)) < 0 ? " " + opt : opt; 		
	}

	/**
	 * open oc with option opt, if opt does not contain a - ({@link #eNoOpenClose}
	 * @param <T>
	 * @param opt
	 * @param oc
	 * @return oc
	 */
	public static <T> OpenClose<T> open(String opt, OpenClose<T> oc) {
		if (opt.indexOf(eNoOpenClose) < 0)
			oc.open(normalizeOpt(opt).substring(0, 1));
		return oc;
	}
	/**
	 * @return a Read<String> using {@link #make(String, Object) and #open(String) }
	 */
	@SuppressWarnings("unchecked")
	public static Read<String> makeRead(String opt, Object o) {
		return (Read<String>) open(opt, make(opt, o));
	}

	/**
	 * reset the receiver, initialise the list with args using {@link #addIo(Object[])}
	 */
	public void reset(Object... args) {
		close();
		opts.clear();
		ios.clear();
		addIo(args);
	}

	/**
	 * add further pairs of option, specification to the lists of this Cat.
	 * <br>if an element of args is an option ({@link Option#single(Object)} use it as current option ohterwise add the current option and element as specifiction. Start with current option empty.
	 * Option and specification pairs are enventually passed to {@link #make(String, Object)}
	 */
	public void addIo(Object... args) {
		String opt = " ";
		String o1 = " ";
		for (Object obj : args) {
			if (null != (o1 = Option.single(obj))) {
				opt = normalizeOpt(o1);
			} else {
				opts.add(opt);
				ios.add(make(opt, obj));
			}
		}
	}

	/**
	 * open the receiver. For
	 * <br><ul>
	 * <li>read the list of the receiver may specify any number of readers (including 0)
	 * <li>write the list of the receiver at least one object. Only the first is used
	 * </ul>
	 * @param opt the readers or writers arg opened with this option (as first character).
	 */
	@SuppressWarnings("unchecked")
	public void open(String opt) {
		close();
		if ("r".equals(opt)) {
			readIx = -1;
			nextReader();
		} else if ("w".equals(opt) || "a".equals(opt)) {
			if (ios.isEmpty())
				fail("cannot write without ios");
			readIx = -7;
			writer = (Write<T>) open(opt.substring(0,1) + opts.get(0).substring(1), ios.get(0));
		} else {
			fail("open(" + opt + ") with bad opt");
		}
	}

	public void close() {
		if (reader != null && opts.get(readIx).indexOf(eNoOpenClose) < 0)
			reader.close();
		if (writer != null && opts.get(0).indexOf(eNoOpenClose) < 0)
			writer.close();
		reader = null;
		writer = null;
		readIx = -9;
	}

	/**
	 * open the concatenation, if args is nonEmpty they define the concatentation of readers. 
	 * @param line
	 */
	public void write(T line) {
		writer.write(line);
	}

	/**
	 * read and return the next line.
	 * at the end of the current reader try the next one. return null only at end of all readers.
	 * fail (currently with null exception, but unspecified how) if not opened for read.
	 */
	public T read() {
		T line;
		while (true) {
			if (reader != null && null != (line = reader.read())) 
				return line;
			if (! nextReader())
				return null;
		}
	}

	/**
	 * close the current reader and open the next one
	 * @return false after last reader otherwise true
	 */
	@SuppressWarnings("unchecked")
	public boolean nextReader() {
		if (readIx < -1)
			fail("nextReader while closed");
		if (reader != null && opts.get(readIx).indexOf(eNoOpenClose) < 0)
			reader.close();
		reader = null;
		if (++readIx >= ios.size())
			return false;
		if (" r".indexOf(opts.get(readIx).charAt(0)) < 0)
			fail("open read on io with opt " + opts.get(readIx));
		reader = (Read<T>) open("r" + opts.get(readIx).substring(1), ios.get(readIx));
		return true;
	}	
}


