/* ************************************************************************
#
#  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.struct.builder;

import java.util.ArrayList;
import java.util.List;

import divconq.lang.Memory;
import divconq.struct.CompositeStruct;
import divconq.struct.CompositeParser;
import divconq.struct.scalar.AnyStruct;
import divconq.struct.scalar.BigIntegerStruct;
import divconq.struct.scalar.BooleanStruct;
import divconq.struct.scalar.DateTimeStruct;
import divconq.struct.scalar.DecimalStruct;
import divconq.struct.scalar.IntegerStruct;
import divconq.struct.scalar.NullStruct;
import divconq.struct.scalar.StringStruct;

public class MemoryBuilder extends Memory implements ICompositeBuilder {
	protected BuilderInfo cstate = null;
	protected List<BuilderInfo> bstate = new ArrayList<BuilderInfo>(); 
	protected boolean complete = false;
	
	@Override
	public BuilderState getState() {
		return (this.cstate != null) ? this.cstate.State : (this.complete) ? BuilderState.Complete : BuilderState.Ready;
	}
	
	@Override
	public void record(Object... props) throws BuilderStateException {
		this.startRecord();
		
		String name = null;
		
		for (Object o : props) {
			if (name != null) {
				this.field(name, o);				
				name = null;
			}
			else {
				if (o == null)
					throw new BuilderStateException("Null Field Name");
					
				name = o.toString();
			}
		}
		
		this.endRecord();
	}
	
	@Override
	public void startRecord() throws BuilderStateException {
		// if in a list and need comma
		if ((this.cstate != null) && (this.cstate.State == BuilderState.InList) && this.cstate.CommaNeeded)
			this.write(", ");
		
		// indicate we are in a record
		this.cstate = new BuilderInfo(BuilderState.InRecord);
		this.bstate.add(cstate);
		
		this.write(" { ");
	}
	
	@Override
	public void endRecord() throws BuilderStateException {
		// cannot call end rec with being in a record or field
		if ((this.cstate == null) || (this.cstate.State == BuilderState.InList))
			throw new BuilderStateException("Cannot end record when in list");
		
		// if in a field, finish it
		if (this.cstate.State == BuilderState.InField)
			this.endField();
		
		this.write(" } ");
		
		// return to parent
		this.popState();
		
		// mark the value complete, let parent container know we need commas
		this.completeValue();
	}

	// names may contain only alpha-numerics
	@Override
	public void field(String name, Object value) throws BuilderStateException {
		this.field(name);
		this.value(value);
	}

	/*
	 * OK to call if in an unnamed field already (then adds name to the field)
	 * or to call if in a record straight up
	 */
	@Override
	public void field(String name) throws BuilderStateException {
		// fields cannot occur outside of records
		if ((this.cstate == null) || (this.cstate.State == BuilderState.InList))
			throw new BuilderStateException("Cannot add field while in list");
		
		// if in a named field, finish it
		if ((this.cstate.State == BuilderState.InField) && this.cstate.IsNamed)
			this.endField();
		
		// if not yet in a field mark as such
		if (this.cstate.State == BuilderState.InRecord)
			this.field();
		
		this.value(name);
	}
	
	@Override
	public void field() throws BuilderStateException {
		// fields cannot occur outside of records
		if ((this.cstate == null) || (this.cstate.State == BuilderState.InList))
			throw new BuilderStateException("Cannot add field when in list");
		
		// if already in field then pop out of it
		if (this.cstate.State == BuilderState.InField)
			this.endField();
		
		// if pop leaves us hanging or not in record then bad
		if ((this.cstate == null) || (this.cstate.State != BuilderState.InRecord))
			throw new BuilderStateException("Cannot end field when not in record");
		
		// we should now be at record level, check for comma state
		if (this.cstate.CommaNeeded)
			this.write(", ");
			
		// note that we are in a field now, value not completed
		this.cstate = new BuilderInfo(BuilderState.InField);
		this.bstate.add(cstate);
	}
	
	private void endField() throws BuilderStateException {
		// cannot occur outside of field
		if ((this.cstate == null) || (this.cstate.State == BuilderState.InList) || (this.cstate.State == BuilderState.InRecord))
			throw new BuilderStateException("Cannot end field when not in a field");
		
		// end the field
		if (!this.cstate.ValueComplete)
			this.write("null");

		// return to the record state
		this.popState();
		
		// we should now be at record level, mark comma state
		this.cstate.CommaNeeded = true;
	}
	
	@Override
	public void list(Object... props) throws BuilderStateException {
		this.startList();
		
		for (Object o : props)
			this.value(o);
		
		this.endList();
	}
	
	@Override
	public void startList() throws BuilderStateException {
		// if in a list and need comma
		if ((this.cstate != null) && (this.cstate.State == BuilderState.InList) && this.cstate.CommaNeeded)
			this.write(", ");
		
		// mark that we are in a list
		this.cstate = new BuilderInfo(BuilderState.InList);
		this.bstate.add(cstate);
		
		// start out complete (an empty list is complete)
		this.cstate.ValueComplete = true;
		
		this.write(" [ ");
	}
	
	@Override
	public void endList() throws BuilderStateException {
		// must be in a list
		if ((this.cstate == null) || (this.cstate.State != BuilderState.InList))
			throw new BuilderStateException("Not in a list, cannot end list");
		
		if (!this.cstate.ValueComplete)
			this.endItem();
		
		// end list
		this.write(" ] ");

		// return to parent state
		this.popState();
		
		// mark the value complete, let parent container know we need commas
		this.completeValue();
	}
	
	private void endItem() throws BuilderStateException {
		// cannot occur outside of field
		if ((this.cstate == null) || (this.cstate.State != BuilderState.InList))
			throw new BuilderStateException("Cannot end list item when not in a list");
		
		// end the item
		if (!this.cstate.ValueComplete) {
			this.write("null");
			
			// tell list that the lastest entry is complete
			this.cstate.ValueComplete = true;
		}
		
		// note need for comma in parent
		this.cstate.CommaNeeded = true;
	}
	
	@Override
	public void scalar(Object value) throws BuilderStateException {
		this.scalar();
		this.value(value);
	}
	
	@Override
	public void scalar() throws BuilderStateException {
		// cannot occur outside of field or list
		if ((this.cstate == null) || (this.cstate.State == BuilderState.InRecord))
			throw new BuilderStateException("Cannot add scalar unless in a list or in a field");
		
		// if in a list, check if we need a comma 
		if (this.cstate.State == BuilderState.InList) {			
			if (!this.cstate.ValueComplete)
				this.endItem();
			
			this.cstate.ValueComplete = false;
			
			// we should now be at list level, check for comma state
			if (this.cstate.CommaNeeded)
				this.write(", ");
		}
	}
	
	@Override
	public boolean needFieldName() {
		if (this.cstate == null)
			return false;

		return ((this.cstate.State == BuilderState.InField) && !this.cstate.IsNamed);
	}
	
	@Override
	public void value(Object value) throws BuilderStateException {
		// cannot occur outside of field or list
		if ((this.cstate == null) || (this.cstate.State == BuilderState.InRecord))
			throw new BuilderStateException("Cannot add a value unless in a field or in a list");

		if ((this.cstate.State == BuilderState.InField) && !this.cstate.IsNamed) {
			// TODO check that name is valid
			this.write("\"" + value.toString() + "\": ");
			
			this.cstate.IsNamed = true;
		}
		else {
			// TODO handle other object types - reader, etc
			if (value instanceof AnyStruct)
				value = ((AnyStruct)value).getValue();
			
			if (value == null) 
				this.write("null");
			else if (value instanceof NullStruct)
				this.write("null");
			else if (value instanceof BooleanStruct)
				this.write(((BooleanStruct)value).getValue().toString());
			else if (value instanceof IntegerStruct)
				this.write(((IntegerStruct)value).getValue().toString());
			else if (value instanceof BigIntegerStruct)
				this.write(((BigIntegerStruct)value).getValue().toString());
			else if (value instanceof DecimalStruct)
				this.write(((DecimalStruct)value).getValue().toString());
			else if (value instanceof DateTimeStruct){
				this.write("\"");
				this.writeEscape(((DateTimeStruct)value).getValue().toString());
				this.write("\"");
			}
			else if (value instanceof StringStruct) {
				this.write("\"");
				this.writeEscape(((StringStruct)value).toString());	
				this.write("\"");
			}
			else if (value instanceof Boolean)
				this.write(value.toString());
			else if (value instanceof Number) 
				this.write(value.toString());
			else if (value instanceof CompositeStruct)
				this.write(value.toString());		// TODO like to use "toBuilder" here, test it with primitive and rec and list
			else if (value instanceof ICompositeOutput)
				this.write(value.toString());		
			//else if (value instanceof ByteBuffer) // TODO BinaryStruct
				//this.write("\"" + Base64.encodeBase64String(((ByteBuffer)value).array()) + "\"");		// TODO more efficient
			//else if (value instanceof byte[]) 
				//this.write("\"" + Base64.encodeBase64String((byte[])value) + "\"");		// TODO more efficient
			//else if (value instanceof Elastic)
				//((Elastic)value).toBuilder(this);
			else {
				this.write("\"");
				this.writeEscape(value.toString());		// TODO more efficient with memory/builder/etc
				this.write("\"");
			}
			
			// mark the value complete, let parent container know we need commas
			this.completeValue();
		}
	}
	
	@Override
	public void rawJson(Object value) throws BuilderStateException {
		// cannot occur outside of field or list
		if ((this.cstate == null) || (this.cstate.State == BuilderState.InRecord))
			throw new BuilderStateException("Cannot add JSON when not in field or in list");

		if ((this.cstate.State == BuilderState.InField) && !this.cstate.IsNamed) 
			throw new BuilderStateException("Cannot use JSON for field name");
		
		// TODO handle other object types - reader, etc
		this.write(value.toString());		// TODO more efficient
		
		// mark the value complete, let parent container know we need commas
		this.completeValue();
	}
	
	public void writeEscape(String str) {
		str = str.replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", "\\n").replace("\t", "\\t");		
		this.write(str);
	}
	
	public void writeEscape(char ch) {
		  switch (ch) {
		    case '\\':
		    	this.write("\\\\");
			break;
			case '"':
				this.write("\\\"");
				break;
			case '\n':
				this.write("\\n");
				break;
			case '\t':
				this.write("\\t");
			    break;
		    default:
		    	this.writeChar(ch);
		  }			
	}
	
	private void popState() throws BuilderStateException {
		if (this.cstate == null)
			throw new BuilderStateException("Cannot pop state when state is null");
		
		this.bstate.remove(this.bstate.size() - 1);
		
		if (this.bstate.size() == 0) {
			this.cstate = null;
			this.complete = true;
		}
		else
			this.cstate = this.bstate.get(this.bstate.size() - 1);
	}
	
	private void completeValue() throws BuilderStateException {
		// if parent, mark it a having a complete value
		if (this.cstate != null) {
			this.cstate.ValueComplete = true;
			
			if (this.cstate.State == BuilderState.InField)
				this.endField();
			else
				this.endItem();
		}
	}
	
	@Override
	public Memory toMemory() {
		return this;
	}
	
	@Override
	public CompositeStruct toLocal() {
		this.setPosition(0);
		return CompositeParser.parseJson(this);
	}
}
