/* ************************************************************************
#
#  DivConq
#
#  http://divconq.com/
#
#  Copyright:
#    Copyright 2012 eTimeline, LLC. All rights reserved.
#
#  License:
#    See the license.txt file in the project's top-level directory for details.
#
#  Authors:
#    * Andy White
#
************************************************************************ */
package divconq.db.mumps;

import java.io.InputStream;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.nio.ByteBuffer;

import divconq.db.DatabaseResult;
import divconq.lang.chars.Special;
import divconq.lang.chars.Utf8Decoder;
import divconq.struct.ListStruct;
import divconq.struct.RecordStruct;
import divconq.struct.Struct;
import divconq.struct.builder.BuilderStateException;
import divconq.struct.builder.ObjectBuilder;
import divconq.struct.builder.ICompositeBuilder;
import divconq.lang.Memory;
import divconq.lang.StringBuilder32;
import divconq.log.DebugLevel;
import divconq.util.HexUtil;
import divconq.util.StringUtil;
import divconq.util.TimeUtil;

/**
 * Internal class used to parse the results from a database call.
 * 
 * @author Andy
 *
 */
public class ResultParser {
	protected Utf8Decoder decoder = new Utf8Decoder();
	protected byte[] buffer = new byte[8 * 1024];		// tested 64 bytes - TODO try others, config, lower?
	protected int readcnt = 0;
	protected ByteBuffer bb = null;
	protected int special = 0;
	
	/**
	 * Load a Json like structure from M, but M uses its own codes to indicate such structures.
	 * 
	 * @param input stream to read and parse
	 * @param result object to process and hold results
	 */
	public void load(InputStream input, DatabaseResult result, Connection conn) {
		// parse the result
		try {
			this.parseStruct(input, result.getBuilder(), conn);		// a Record or a List
		}
		catch (Exception x) {
			result.errorTr(321, x);
			result.trace(0, "Structure state: " + result.getBuilder().toString());
		}
	}
	
	/**
	 * Load a Json like structure from M, but M uses its own codes to indicate such structures.
	 * 
	 * @param input stream to read and parse
	 * @param result object to process and hold results - specifically the messages section, not the "result" section
	 */
	public void loadMessages(InputStream input, DatabaseResult result, Connection conn) {
		// parse the messages
		try {
			ObjectBuilder msgbuilder = new ObjectBuilder();
			this.parseStruct(input, msgbuilder, conn);		// a List of Records		
			
			for (Struct s : ((ListStruct)msgbuilder.getRoot()).getItems()) {
				RecordStruct msg = (RecordStruct)s;
	
				String kind = msg.getFieldAsString("Level");
				String msgtext = msg.getFieldAsString("Message");
				long code = msg.getFieldAsInteger("Code");
				
				result.log(DebugLevel.valueOf(kind), code, msgtext);
			}
		}
		catch (Exception x) {
			result.errorTr(322, x);
			result.trace(0, "Structure state: " + result.getBuilder().toString());
		}
	}
	
	/**
	 * Load a Json like structure from M, but M uses its own codes to indicate such structures.
	 * 
	 * @param input stream to read and parse
	 * @param rb object to process results
	 */
	public void parseStruct(InputStream input, ICompositeBuilder rb, Connection conn) throws Exception {
		while (true) {
			StringBuilder32 str = this.decoder.processBytesUntilSpecial(this.bb);
		
			if (str != null) { 
				if (str.length() > 0) {
					if (rb.needFieldName())
						rb.value(str.toString());
					else
						this.toScalar(rb, str);
				}
				
				this.special = decoder.getLastSpecialCharacter();
				
				// at the end (there should be no outstanding text
				if (this.special == Special.End.getCode())
					break;
				
				if (this.special == Special.StartRec.getCode()) 
					rb.startRecord();
				else if (this.special == Special.EndRec.getCode()) 
					rb.endRecord();
				else if (this.special == Special.StartList.getCode()) 
					rb.startList();
				else if (this.special == Special.EndList.getCode()) 
					rb.endList();
				else if (this.special == Special.Scalar.getCode()) 
					rb.scalar();
				else if (this.special == Special.Field.getCode())
					rb.field();
			}
			else {
				// let people know you are alive - if long running with out output then proc must output periodic "." process chars
				conn.lastStamp = System.currentTimeMillis();
				this.readcnt = input.read(this.buffer);
				this.bb = ByteBuffer.wrap(this.buffer, 0, this.readcnt);
			}
		}
	}
	
	//  ` string
	// TODO convert this to use a configurable date-time, that way each database can configure how it wishes to store dates
	// including BCE support and year padding for more than 20000 year support (also resolve Gregorian/Julian dates) 
	public void toScalar(ICompositeBuilder rb, CharSequence val) throws BuilderStateException {
		if (StringUtil.isEmpty(val)) {
			rb.value(null);
			return;
		}
		
		if (val.charAt(0) == '`') {
			// unescape the string
			StringBuilder sb = new StringBuilder();
			boolean escape = false;
			
			for (int i = 1; i < val.length(); i++) {
				char v = val.charAt(i);
				
				if (v == '\\') {
					if (escape) {
						sb.append('\\');
						escape = false;
					}
					else {
						escape = true;
					}
				}
				else if (v == 'n') {
					if (escape) {
						sb.append('\n');
						escape = false;
					}
					else {
						sb.append('n');
					}
				}
				else if (v == 't') {
					if (escape) {
						sb.append('\t');
						escape = false;
					}
					else {
						sb.append('t');
					}
				}
				//else if (escape && ((v == 'b') || (v == 'r') || (v == 'f') || (v == 'u') || Character.isDigit(v))) {
				else if (escape) {  // only t and n allowed
					throw new BuilderStateException("Invalid escape character: " + v);
				} 
				else {
					sb.append(v);
					escape = false;
				}
			}			
			
			rb.value(sb);
			return;
		}
		
		if (val.charAt(0) == '*') {
			rb.rawJson(val.subSequence(1, val.length()));
			return;
		}
		
		if (val.charAt(0) == '(') {
			rb.value(new BigDecimal(val.subSequence(1, val.length()).toString()));
			return;
		}
		
		if (val.charAt(0) == ')') {
			try {
				rb.value(new Long(val.subSequence(1, val.length()).toString()));
				return;
			}
			catch (Exception x) {				
			}
			
			try {
				rb.value(new BigInteger(val.subSequence(1, val.length()).toString()));
			}
			catch (Exception x) {				
			}
			
			return;
		}
		
		if (val.charAt(0) == '@') {
			rb.value(TimeUtil.stampFmt.parseDateTime(val.subSequence(1, val.length()).toString()));
			return;
		}
		
		if (val.charAt(0) == '&') {
			rb.value(val.subSequence(1, val.length()));
			return;
		}
		
		if (val.charAt(0) == '!') {
			rb.value(new Memory(HexUtil.decodeHex(val.subSequence(1, val.length()))));
			return;
		}
		
		if (val.charAt(0) == '#') {
			rb.value(TimeUtil.stampFmt.parseDateTime(val.subSequence(1, val.length()).toString()));
			return;
		}
		
		if (val.charAt(0) == '$') {
			rb.value(TimeUtil.parseBigDateTime(val.subSequence(1, val.length()).toString()));
			return;
		}
		
		String val2 = val.toString().toLowerCase();
		
		if (val2.equals("null")) {
			rb.value(null);
			return;
		}
		
		if (val2.equals("true")) {
			rb.value(true);
			return;
		}
		
		if (val2.equals("false")) {
			rb.value(false);
			return;
		}
		
		try {
			rb.value(val2);
		}
		catch (Exception x) {				
		}
		
		return;
	}
}
