package com.esri.hadoop.hive;

import org.apache.hadoop.hive.ql.exec.UDFArgumentException;
import org.apache.hadoop.hive.ql.metadata.HiveException;
import org.apache.hadoop.hive.ql.udf.generic.GenericUDF.DeferredObject;
import org.apache.hadoop.hive.serde2.objectinspector.ObjectInspector;
import org.apache.hadoop.hive.serde2.objectinspector.ObjectInspectorUtils;
import org.apache.hadoop.hive.serde2.objectinspector.PrimitiveObjectInspector;
import org.apache.hadoop.hive.serde2.objectinspector.ObjectInspector.Category;
import org.apache.hadoop.io.BytesWritable;
import org.apache.hadoop.io.Text;
import org.apache.log4j.Logger;

import com.esri.core.geometry.ogc.OGCGeometry;
import com.esri.core.geometry.ogc.OGCPoint;

public class HiveGeometryOIHelper {
	
	static Logger LOG = Logger.getLogger(HiveGeometryOIHelper.class);
	
	private PrimitiveObjectInspector oi;
	private int argIndex;
	private boolean isConstant;
	
	OGCGeometry constantGeometry;
	
	private HiveGeometryOIHelper(ObjectInspector oi, int argIndex) {
		this.oi = (PrimitiveObjectInspector)oi;
		this.argIndex = argIndex;
		
		// constant geometries only need to be processed once and can
		// be optimized in certain operations
		isConstant = ObjectInspectorUtils.isConstantObjectInspector(oi);
	}
	
	public static HiveGeometryOIHelper create(ObjectInspector [] OIs, int argIndex) throws UDFArgumentException {
		return create(OIs[argIndex], argIndex);
	}
	
	public static HiveGeometryOIHelper create(ObjectInspector oi, int argIndex) throws UDFArgumentException {
		if (oi.getCategory() != Category.PRIMITIVE) {
			throw new UDFArgumentException("Geometry argument must be a primitive type");
		}
		
		return new HiveGeometryOIHelper(oi, argIndex);
	}
	
	public static boolean canCreate(ObjectInspector oi) {
		if (oi.getCategory() != Category.PRIMITIVE) {
			return false;
		}
		
		return true;
	}
	
	/**
	 * Gets whether this geometry argument is constant.
	 * 
	 * @return
	 */
	public boolean isConstant() {
		return isConstant;
	}
	
	/**
	 * Returns the cached constant geometry object.
	 * 
	 * @return cache geometry, or null if not constant
	 */
	public OGCGeometry getConstantGeometry() {
		return constantGeometry;
	}
	
	/**
	 * Reads the corresponding geometry from the deferred object list
	 * or returns the cached geometry if argument is constant. 
	 * 
	 * @param args
	 * @return OGCPoint or null if not a point
	 * @see #getGeometry(DeferredObject[])
	 */
	public OGCPoint getPoint(DeferredObject[] args) {
		OGCGeometry geometry = getGeometry(args);
		
		if (geometry instanceof OGCPoint) {
			return (OGCPoint)geometry;
		} else {
			return null;
		}
	}
	
	/**
	 * Reads the corresponding geometry from the deferred object list
	 * or returns the cached geometry if argument is constant. 
	 * 
	 * @param args
	 * @return
	 */
	public OGCGeometry getGeometry(DeferredObject[] args) {
		if (isConstant) {
			if (constantGeometry == null) {
				constantGeometry = getGeometry(args[argIndex]);
			} 
			
			return constantGeometry;
		} else {
			// not constant, so we have to rebuild the geometry
			// on every call
			return getGeometry(args[argIndex]);
		}
	}
	
	private OGCGeometry getGeometry(DeferredObject arg) {
		Object writable;
		try {
			writable = oi.getPrimitiveWritableObject(arg.get());
		} catch (HiveException e) {
			LOG.error("Failed to get writable", e);
			return null;
		}
		
		if (writable == null) {
			return null;
		}
		
		switch (oi.getPrimitiveCategory()) {
			case BINARY: return getGeometryFromBytes((BytesWritable)writable);
			case STRING: return OGCGeometry.fromText(((Text)writable).toString());
			default: return null;
		}
	}
	
	private BytesWritable last = null;
	
	// always assume bytes are reused until we determine they aren't
	private boolean bytesReused = true; 
	
	private OGCGeometry getGeometryFromBytes(BytesWritable writable) {
		
		if (bytesReused) {
			if (last != null && last != writable) {
				// this assumes that the source of these bytes will either always
				// reuse the bytes or never reuse the bytes.  
				bytesReused = false;
			} 
			last = writable;
		}
		
		return GeometryUtils.geometryFromEsriShape((BytesWritable)writable, bytesReused);
	}
	
	@Override
	public String toString() {
		StringBuilder builder = new StringBuilder();
		
		builder.append("HiveGeometryHelper(");
		builder.append("constant=" + isConstant + ";");
		builder.append(")");
		
		return builder.toString();
	}
}