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

import java.math.BigDecimal;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;

import org.joda.time.DateTime;
import org.joda.time.LocalDate;
import org.joda.time.LocalTime;

import divconq.lang.BigDateTime;
import divconq.lang.FuncResult;
import divconq.lang.OperationResult;
import divconq.schema.DataType;
import divconq.schema.Field;
import divconq.struct.builder.BuilderStateException;
import divconq.struct.builder.ICompositeBuilder;
import divconq.struct.scalar.NullStruct;
import divconq.xml.XElement;

/**
 * DivConq uses a specialized type system that provides type consistency across services 
 * (including web services), database fields and stored procedures, as well as scripting.
 * 
 * All scalars (including primitives) and composites (collections) are wrapped by some
 * subclass of Struct.  Map collections are expressed by this class - records have fields
 * and fields are a name value pair.  This class is analogous to an Object in JSON but may
 * contain type information as well, similar to Yaml.
 * 
 *  TODO link to blog entries.
 * 
 * @author Andy
 *
 */
public class RecordStruct extends CompositeStruct {
	protected Map<String,FieldStruct> fields = new HashMap<String,FieldStruct>();

	/**
	 * Provide data type info (schema for fields) and a list of initial fields
	 * 
	 * @param type field schema
	 * @param fields initial pairs
	 */
	public RecordStruct(DataType type, FieldStruct... fields) {
		super(type);
		this.setField(fields);
	}
	
	/**
	 * Optionally provide a list of initial fields
	 * 
	 * @param fields initial pairs
	 */
	public RecordStruct(FieldStruct... fields) {
		this.setField(fields);
	}
	
	/* (non-Javadoc)
	 * @see divconq.struct.CompositeStruct#select(divconq.struct.PathPart[])
	 */
	@Override
	public Struct select(PathPart... path) {
		if (path.length == 0)
			return this;
		
		PathPart part = path[0];
		
		if (!part.isField()) {			
			OperationResult log = part.getLog();
			
			if (log != null) 
				log.warnTr(504, this);
			
			return null;
		}
		
		String fld = part.getField();

		// selecting a field instead of a record ensures that you will create it if it is not present, not desired then select records/items only
		if (path.length == 1) 
			return this.getOrAllocateField(fld).getResult();			
		
		if (!this.fields.containsKey(fld)) {
			OperationResult log = part.getLog();
			
			if (log != null) 
				log.warnTr(505, fld);
			
			return null;
		}
		
		FieldStruct f = this.fields.get(fld);
		Struct o = f.getValue();
		
		if (o instanceof CompositeStruct) 
			return ((CompositeStruct)o).select(Arrays.copyOfRange(path, 1, path.length));		
		
		OperationResult log = part.getLog();
		
		if (log != null) 
			log.warnTr(503, o);
		
		return null;
	}
	
	/* (non-Javadoc)
	 * @see divconq.struct.CompositeStruct#isEmpty()
	 */
	@Override
	public boolean isEmpty() {
		return (this.fields.size() == 0);
	}
	
	/* (non-Javadoc)
	 * @see divconq.struct.builder.ICompositeOutput#toBuilder(divconq.struct.builder.ICompositeBuilder)
	 */
	@Override
	public void toBuilder(ICompositeBuilder builder) throws BuilderStateException {
		builder.startRecord();
		
		for (FieldStruct f : this.fields.values()) 
			f.toBuilder(builder);
		
		builder.endRecord();
	}

	/**
	 * Adds or replaces a list of fields within the record.
	 * 
	 * @param fields
	 * @return a log of messages about success of the call
	 */
	public OperationResult setField(FieldStruct... fields) {
		OperationResult or = new OperationResult(false);
		
		for (FieldStruct f : fields) {
			Struct svalue = f.getValue();
			
			if (!f.prepped) {
				// take the original value and convert to a struct, fields hold structures
				Object value = f.orgvalue;
				
				if (value instanceof ICompositeBuilder)
					value = ((ICompositeBuilder)value).toLocal();
				
				if (this.type != null) {
					Field fld = this.type.getField(f.getName());
					
					if (fld != null) {
						Struct sv = fld.wrap(value, or);
						
						if (sv != null)
							svalue = sv;
					}
				}
				
				if (svalue == null) 
					svalue = Struct.objectToStruct(value); 
				
				f.setValue(svalue);
				f.prepped = true;
			}
			
			//FieldStruct old = this.fields.get(f.getName());
			
			//if (old != null)
			//	old.dispose();
			
			this.fields.put(f.getName(), f);
		}
		
		return or;
	}
	
	/**
	 * Add or replace a specific field with a value.
	 * 
	 * @param name of field
	 * @param value to store with field
	 * @return a log of messages about success of the call
	 */
	public OperationResult setField(String name, Object value) {
		return this.setField(new FieldStruct(name, value));
	}
	
	/**
	 * 
	 * @return collection of all the fields this record holds
	 */
	public Iterable<FieldStruct> getFields() {
		return this.fields.values();
	}
	
	/**
	 * 
	 * @param name of the field desired
	 * @return the struct for that field
	 */
	public Struct getField(String name) {
		if (!this.fields.containsKey(name)) 
			return null;
		
		return this.fields.get(name).value;
	}
	
	/**
	 * Unlike getField, this returns the value (inner) rather than struct wrapping 
	 * the value.
	 * 
	 * @param name of the field desired
	 * @return field's "inner" value as an Object
	 */
	public Object getFieldAsAny(String name) {
		if (!this.fields.containsKey(name)) 
			return null;
		
		Struct st = this.fields.get(name).value;
		
		if ((st != null) && (st instanceof ScalarStruct)) 
			return ((ScalarStruct)st).getGenericValue();
		
		return null;
	}
	
	/**
	 * If the record has schema, lookup the schema for a given field.
	 * 
	 * @param name of the field desired
	 * @return field's schema
	 */
	public DataType getFieldType(String name) {
		// look first at the field value, if it has schema return
		Struct fs = this.getField(name);
		
		if ((fs != null) && (fs.getType() != null))
				return fs.getType();
		
		// look next at this records schema
		if (this.type != null) {
			Field fld = this.type.getField(name);
			
			if (fld != null)
				return fld.getPrimaryType();
		}
		
		// give up, we don't know the schema
		return null;
	}
	
	/**
	 * Like getField, except if the field does not exist it will be created and added
	 * to the Record (unless that field name violates the schema).
	 * 
	 * @param name of the field desired
	 * @return log of messages from call plus the requested structure
	 */
	public FuncResult<Struct> getOrAllocateField(String name) {
		FuncResult<Struct> fr = new FuncResult<Struct>(false);
		
		if (!this.fields.containsKey(name)) {
			Struct value = null;
			
			if (this.type != null) {
				Field fld = this.type.getField(name);
				
				if (fld != null) 
					value = fld.create(fr);
				else if (this.type.isAnyRecord()) 
					value = new NullStruct();
			}
			else
				value = new NullStruct();
			
			if (value != null) {
				FieldStruct f = new FieldStruct(name, value);
				f.value = value;
				this.fields.put(name, f);
				
				fr.setResult(value);
			}
		}
		else
			fr.setResult(this.fields.get(name).value);
		
		return fr;
	}
	
	/**
	 * 
	 * @param name of field
	 * @return true if field exists 
	 */
	public boolean hasField(String name) {
		return this.fields.containsKey(name);
	}
	
	/**
	 * 
	 * @param name of field
	 * @return true if field does not exist or if field is string and its value is empty 
	 */
	public boolean isFieldEmpty(String name) {
		if (!this.fields.containsKey(name))
			return true;
		
		FieldStruct f = this.fields.get(name);
		
		if ((f == null) || (f.value == null))
			return true;
		
		if (f.value instanceof CharSequence)
			return f.value.toString().isEmpty();
		
		return false;
	}
	
	/**
	 * Unlike getField, this returns the value (inner) rather than struct wrapping 
	 * the value.
	 * 
	 * @param name of the field desired
	 * @return field's "inner" value as Integer (DivConq thinks of integers as 64bit)
	 */
	public Long getFieldAsInteger(String name) {
		return Struct.objectToInteger(this.getField(name));
	}
	
	/**
	 * Unlike getField, this returns the value (inner) rather than struct wrapping 
	 * the value.
	 * 
	 * @param name of the field desired
	 * @return field's "inner" value as BigDecimal
	 */
	public BigDecimal getFieldAsDecimal(String name) {
		return Struct.objectToDecimal(this.getField(name));
	}
	
	/**
	 * Unlike getField, this returns the value (inner) rather than struct wrapping 
	 * the value.
	 * 
	 * @param name of the field desired
	 * @return field's "inner" value as Boolean
	 */
	public Boolean getFieldAsBoolean(String name) {
		return Struct.objectToBoolean(this.getField(name));
	}
	
	/**
	 * Unlike getField, this returns the value (inner) rather than struct wrapping 
	 * the value.
	 * 
	 * @param name of the field desired
	 * @return field's "inner" value as DateTime
	 */
	public DateTime getFieldAsDateTime(String name) {
		return Struct.objectToDateTime(this.getField(name));
	}
	
	/**
	 * Unlike getField, this returns the value (inner) rather than struct wrapping 
	 * the value.
	 * 
	 * @param name of the field desired
	 * @return field's "inner" value as BigDateTime
	 */
	public BigDateTime getFieldAsBigDateTime(String name) {
		return Struct.objectToBigDateTime(this.getField(name));
	}
	
	/**
	 * Unlike getField, this returns the value (inner) rather than struct wrapping 
	 * the value.
	 * 
	 * @param name of the field desired
	 * @return field's "inner" value as Date
	 */
	public LocalDate getFieldAsDate(String name) {
		return Struct.objectToDate(this.getField(name));
	}
	
	/**
	 * Unlike getField, this returns the value (inner) rather than struct wrapping 
	 * the value.
	 * 
	 * @param name of the field desired
	 * @return field's "inner" value as Time
	 */
	public LocalTime getFieldAsTime(String name) {
		return Struct.objectToTime(this.getField(name));
	}
	
	/**
	 * Unlike getField, this returns the value (inner) rather than struct wrapping 
	 * the value.
	 * 
	 * @param name of the field desired
	 * @return field's "inner" value as a String
	 */
	public String getFieldAsString(String name) {
		return Struct.objectToString(this.getField(name));
	}
	
	/**
	 * Unlike getField, this returns the value (inner) rather than struct wrapping 
	 * the value.
	 * 
	 * @param name of the field desired
	 * @return field's "inner" value as a Record
	 */
	public RecordStruct getFieldAsRecord(String name) {
		return Struct.objectToRecord(this.getField(name));
	}
	
	/**
	 * Unlike getField, this returns the value (inner) rather than struct wrapping 
	 * the value.
	 * 
	 * @param name of the field desired
	 * @return field's "inner" value as a List
	 */
	public ListStruct getFieldAsList(String name) {
		return Struct.objectToList(this.getField(name));
	}
	
	/**
	 * Unlike getField, this returns the value (inner) rather than struct wrapping 
	 * the value.
	 * 
	 * @param name of the field desired
	 * @return field's "inner" value as CompositeStruct
	 */
	public CompositeStruct getFieldAsComposite(String name) {
		return Struct.objectToComposite(this.getField(name));
	}
	
	/**
	 * Unlike getField, this returns the value (inner) rather than struct wrapping 
	 * the value.
	 * 
	 * @param name of the field desired
	 * @return field's "inner" value as Struct
	 */
	public Struct getFieldAsStruct(String name) {
		return Struct.objectToStruct(this.getField(name));
	}
	
	/**
	 * Unlike getField, this returns the value (inner) rather than struct wrapping 
	 * the value.
	 * 
	 * @param name of the field desired
	 * @return field's "inner" value as Xml (will parse if value is string)
	 */
	public XElement getFieldAsXml(String name) {
		return Struct.objectToXml(this.getField(name));
	}
	
	/**
	 * 
	 * @return number of fields held by this record
	 */
	public int getFieldCount() {
		return this.fields.size();
	}
	
	/*
	public String checkRequiredFields(String... fields) {
		for (String fld : fields) {
			if (this.isFieldBlank(fld))
				return fld;
		}
		
		return null;
	}
	
	public String checkRequiredIfPresentFields(String... fields) {
		for (String fld : fields) {
			if (this.hasField(fld) && this.isFieldBlank(fld))
				return fld;
		}
		
		return null;
	}
	
	public String checkFieldRange(String... fields) {
		for (FieldStruct fld : this.getFields()) {
			boolean fnd = false;
			
			for (String fname : fields) {
				if (fld.getName().equals(fname)) {
					fnd = true;
					break;
				}
			}
			
			if (!fnd)
				return fld.getName();
		}
		
		return null;
	}
	*/

	/**
	 * 
	 * @param name of field to remove
	 */
	public void removeField(String name) {
		//FieldStruct fld = this.fields.get(name);
		
		//if (fld != null)
		//	fld.dispose();
		
		this.fields.remove(name);
	}

	/**
	 * @return
	 */
	@Override
	public Object clone() {
		// TODO Auto-generated method stub
		return null;
	}

	/**
	 * Remove all child fields.
	 */
	@Override
	public void clear() {		
		//for (FieldStruct var : this.fields.values()) 
		//	var.dispose();
		
		this.fields.clear();
	}
	
	/**
	 * Remove all child fields and mark record for disposal.
	 */
	@Override
	public void dispose() {
		//this.clear();
		super.dispose();		
	}
}
