package org.bench4q.agent.plugin.basic.csvprovider;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.bench4q.agent.plugin.ParameterBarn;
import org.bench4q.agent.plugin.Constructor;
import org.bench4q.agent.plugin.Parameter;
import org.bench4q.agent.plugin.Plugin;
import org.bench4q.agent.plugin.behavior.Behavior;
import org.bench4q.agent.plugin.behavior.BehaviorType;
import org.bench4q.agent.utils.ParameterParser;
import org.bench4q.agent.utils.Type.SupportTypes;

@Plugin(value = "CsvProvider")
public class CsvProvider extends ParameterBarn {
	private static final String ENABLE_CheckBox = "enable";
	static final String LINES_GET = "#";

	public static final char DEFAULT_SEPARATOR = ',';
	private static final Pattern pattern = Pattern.compile("\\[(\\d+)\\]");
	private String filename;
	private boolean macintoshLineSeparator;
	private char separator;
	private List<String> commentPrefixes;
	private List<String[]> allValues;
	private String[] currentValues;
	private String[] fieldNames;
	private int position = 0;
	private Boolean shared;
	private boolean loop;
	private boolean bigFile;

	private Reader input;
	private CsvProvider parent;

	@Constructor
	public CsvProvider(
			@Parameter(type = SupportTypes.Field, value = "testId") String testId,
			@Parameter(type = SupportTypes.Field, value = "fileName") String fileName,
			@Parameter(type = SupportTypes.Field, value = "separator") String separator,
			@Parameter(type = SupportTypes.NField, value = "fieldNames") String fieldNames,
			@Parameter(type = SupportTypes.CheckBox, value = "shared") String shared,
			@Parameter(type = SupportTypes.CheckBox, value = "loop") String loop,
			@Parameter(type = SupportTypes.CheckBox, value = "loadAtRunTime") String loadAtRunTime,
			@Parameter(type = SupportTypes.NField, value = "commentPrefixes") String commentPrefixes) {
		super(testId);
		this.filename = fileName;
		if (fieldNames == null) {
			this.fieldNames = null;
			return;
		}
		this.separator = separator != null && separator.length() >= 1 ? separator
				.charAt(0) : DEFAULT_SEPARATOR;

		try {
			this.fieldNames = fieldNames != null ? split(new StringReader(
					fieldNames), null, this.separator, false) : null;
		} catch (IOException e) {
			throw new RuntimeException("Incorrect field names in CsvProvider: "
					+ fieldNames, e);
		}
		this.shared = ParameterParser.buildCheckBox(shared).contains(
				ENABLE_CheckBox);
		this.loop = ParameterParser.buildCheckBox(loop).contains(
				ENABLE_CheckBox);
		this.bigFile = ParameterParser.buildCheckBox(loadAtRunTime).contains(
				ENABLE_CheckBox);
		this.commentPrefixes = commentPrefixes == null ? null : ParameterParser
				.buildNField(commentPrefixes);
		try {
			if (this.shared || !this.bigFile) {
				this.input = new InputStreamReader(new FileInputStream(
						this.getFileFullPath(this.filename)));
			}
			if (!bigFile) {
				allValues = new ArrayList<String[]>();
				String[] line;
				while ((line = split(input, this.commentPrefixes,
						this.separator, macintoshLineSeparator)) != null) {
					allValues.add(line);
				}
				input.close();
			}
			position = -1;
		} catch (Exception ex) {
			throw new RuntimeException("CsvProvider can't parse file "
					+ this.filename, ex);
		}
	}

	public Object createNewCsvProvider() {
		return new CsvProvider(this);
	}

	private CsvProvider(CsvProvider csvProvider) {
		super(csvProvider.getTestId().toString());
		allValues = csvProvider.allValues;
		filename = csvProvider.filename;
		fieldNames = csvProvider.fieldNames;
		macintoshLineSeparator = csvProvider.macintoshLineSeparator;
		commentPrefixes = csvProvider.commentPrefixes;
		separator = csvProvider.separator;
		shared = csvProvider.shared;
		loop = csvProvider.loop;
		bigFile = csvProvider.bigFile;
		if (shared) {
			parent = csvProvider;
			input = csvProvider.input;
		}
		reset();
	}

	public void close() {
		position = -1;
		currentValues = null;
		if (bigFile && !shared) {
			try {
				input.close();
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
	}

	@Behavior(value = "reset", type = BehaviorType.CONTROL_BEHAVIOR)
	public void reset() {
		position = -1;
		currentValues = null;
		if (bigFile && !shared) {
			try {
				if (input != null) {
					input.close();
				}
				input = new InputStreamReader(new FileInputStream(
						this.getFileFullPath(this.filename)));
			} catch (Exception ex) {
				input = null;
				throw new RuntimeException(
						"Can't (re)set CsvProvider session object with file \""
								+ filename + "\".", ex);
			}
		}
	}

	@Behavior(value = "next", type = BehaviorType.CONTROL_BEHAVIOR)
	public void doNext() {
		if (shared) {
			position = parent.position;
			input = parent.input;
		}
		++position;
		if (bigFile) {
			try {
				currentValues = split(input, commentPrefixes, separator,
						macintoshLineSeparator);
			} catch (IOException ex) {
				throw new RuntimeException("CsvProvider can't read file \""
						+ filename + "\".", ex);
			}
		} else if (position < allValues.size()) {
			currentValues = allValues.get(position);
		} else {
			currentValues = null;
		}
		// if there is no next line
		if (currentValues == null) {
			if (loop) {
				position = -1;
				if (shared) {
					parent.position = -1;
				}
				if (bigFile) {
					try {
						if (input != null) {
							input.close();
						}
						input = new InputStreamReader(new FileInputStream(
								this.getFileFullPath(this.filename)));
					} catch (Exception ex) {
						input = null;
						throw new RuntimeException(
								"CsvProvider session object can't access file \""
										+ filename + "\".", ex);
					}
					if (shared) {
						parent.input = input;
					}
				}
				doNext();
			}
		} else if (shared) {
			parent.position = position;
		}
	}

	static private String[] split(Reader enumeration,
			List<String> commentPrefixes, char separator,
			boolean macintoshLineSeparator) throws IOException {
		final List<String> l = new LinkedList<String>();
		final StringBuilder s = new StringBuilder();
		int c;
		int state = 0;
		boolean again = true;

		/* CSV parser automaton */
		while (again) {
			c = enumeration.read();
			switch (state) {
			case 0: // normal character reading
				switch (c) {
				case -1:
					l.add(new String(s));
					again = false;
					break;
				case '\r':
					if (macintoshLineSeparator) {
						l.add(new String(s));
						again = false;
					}
					break;
				case '\n':
					if (!macintoshLineSeparator) {
						l.add(new String(s));
						again = false;
					}
					break;
				case '"':
					state = 2;
					break;
				default:
					if (isComment(s.toString() + (char) c, l, commentPrefixes)) {
						s.setLength(0);
						state = 1;
					} else if (c == separator) {
						l.add(new String(s));
						s.setLength(0);
					} else {
						s.append((char) c);
					}
				}
				break;
			case 1: // discard comment line
				switch (c) {
				case -1:
					again = false;
					break;
				case '\r':
					if (macintoshLineSeparator) {
						state = 0;
					}
					break;
				case '\n':
					if (!macintoshLineSeparator) {
						state = 0;
					}
					break;
				}
				break;
			case 2: // reading a "-delimited sequence
				switch (c) {
				case -1:
					throw new IOException(
							"Ending \" character is missing at end of file.");
				case '"':
					state = 3;
					break;
				default:
					s.append((char) c);
				}
				break;
			case 3: // found a " character in a "-delimited sequence
				switch (c) {
				case -1:
					l.add(new String(s));
					again = false;
					break;
				case '"': // "" is the escape sequence for "
					s.append('"');
					state = 2;
					break;
				case '\r':
					state = 0;
					if (macintoshLineSeparator) {
						l.add(new String(s));
						again = false;
					}
					break;
				case '\n':
					state = 0;
					if (!macintoshLineSeparator) {
						l.add(new String(s));
						again = false;
					}
					break;
				default:
					state = 0;
					if (c == separator) {
						l.add(new String(s));
						s.setLength(0);
					} else {
						s.append((char) c);
					}
				}
			}
		}
		return (l.size() == 1 && l.get(0).length() == 0) ? null : l
				.toArray(new String[l.size()]);
	}

	private static boolean isComment(String field, List<String> l,
			List<String> commentPrefixes) {
		if (commentPrefixes != null && l.isEmpty()) {
			for (String prefix : commentPrefixes) {
				if (field.startsWith(prefix)) {
					return true;
				}
			}
		}
		return false;
	}

	@Override
	public String getValue(String var) {
		if (var.equals(LINES_GET)) {
			if (bigFile) {
				throw new RuntimeException(
						"CsvProvider for file \""
								+ filename
								+ "\": the number of lines is not available when configured to load lines at runtime.");
			} else {
				return String.valueOf(allValues.size());
			}
		} else if (position == -1) {
			throw new RuntimeException("Can't get variable " + var
					+ " from plug-in CsvProvider for file \"" + filename
					+ "\": missing initial call to control \"next\".");
		} else if (currentValues == null) {
			throw new RuntimeException("End of file \"" + filename
					+ "\" reached in CsvProvider.");
		} else {
			Matcher m;
			int i, n;

			if (fieldNames != null)
				for (i = 0, n = fieldNames.length; i < n; i++)
					if (fieldNames[i].equals(var))
						return (i < currentValues.length) ? currentValues[i]
								: null;
			m = pattern.matcher(var);
			return m.matches()
					&& (i = Integer.parseInt(m.group(1))) < currentValues.length ? currentValues[i]
					: null;
		}
	}

}
