package org.persvr.datasource;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.StringWriter;
import java.io.Writer;
import java.util.AbstractCollection;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.persvr.data.DataSourceHelper;
import org.persvr.data.DataSourceManager;
import org.persvr.data.FunctionUtils;
import org.persvr.data.Identification;
import org.persvr.data.ObjectId;
import org.persvr.data.ObjectNotFoundException;
import org.persvr.data.Persistable;
import org.persvr.data.PersistableObject;
import org.persvr.remote.DataSerializer;
import org.persvr.remote.JSONFunction;
import org.persvr.remote.JavaScriptSerializer;
import org.persvr.remote.JsonReceiver;
import org.persvr.util.JSONParser.JSONException;

/**
 * A base class for JSON-based data sources (files and HTTP sources)
 * @author Kris
 *
 */
public abstract class AbstractJsonSource extends JsonReceiver implements DataSource, ListDataSource {
	Object SOURCE_URL_KEY = new Object();
	Object ACCESS_LEVEL_MAP_KEY = new Object();
	//TODO: Create a job to clear these out
	Map<String,Object> cachedJson = new HashMap<String,Object>();
	Map<String,Long> cacheExpiration = new HashMap<String,Long>();
	public abstract boolean isTrusted();
	boolean trustJavaScript = false;
	/**
	 * Gets the JSON string for a particular object id. This is called when an object needs to be initialized, 
	 * and this AbstractJsonSource/JsonReceiver will handle the conversion of the JSON
	 * string to an object 
	 * @param objectId
	 * @return the JSON string
	 * @throws Exception
	 */
	protected abstract Object getJson(String resourceName) throws Exception;

	String id;
	public String getId() {
		return id;
	}

	public void setId(String id) {
		this.id = id;
		
	}
	public void mapSchema(PersistableInitializer initializer) throws Exception {
		initializer.finished();
	}
	protected class RootAndResolved {
		Object root;
		Object resolved;
	}
	protected String getPathSeparator(){
		return "\udeaf";
	}
	public long defaultCacheLength = 10000;
	public void clearCache(){
		cachedJson.clear();
	}
	/**
	 * gets the parsed JSON for a given object id 
	 * @param objectId
	 * @return
	 * @throws Exception
	 */
	protected RootAndResolved getMap(String objectId) throws Exception {
		//TODO: in LocalJsonFileSource, we should check the file modification time
		String[] paths = objectId.split(getPathSeparator());
		String resourceName = paths[0];
		Object object = cachedJson.get(resourceName);
		if (object == null || cacheExpiration.get(resourceName) < new Date().getTime()){
			object = getJson(resourceName);
			cachedJson.put(resourceName, object);
			cacheExpiration.put(resourceName, new Date().getTime() + defaultCacheLength);
		}
		RootAndResolved rar = new RootAndResolved();
		rar.root = object;
		Object last = null;
		for (int i = 1; i < paths.length; i++){
			String pathPart = paths[i];
			if (object instanceof Map)
				object = ((Map) object).get(pathPart);
			else if (object instanceof List)
				try{
					object = ((List) object).get(Integer.parseInt(pathPart));
				}catch(IndexOutOfBoundsException e){
					throw new ObjectNotFoundException(this, objectId);
				}
			if (object == null) {
				if ("schema".equals(pathPart))// a little exception so we can bootstrap
					((Map)last).put("schema",object = new HashMap());
				else
					throw new ObjectNotFoundException(this,objectId);
			}
			last = object;
		}
		rar.resolved = object;
		return rar;
	}
	DataSerializer serializer = new JavaScriptSerializer(){
		public void serialize(Object value,Request request, Writer writer) {
			Serialization serialization = new ConfigSerialization();
			serialization.request = request;
			try {
				serialization.writeValue(writer, value, false);
			} catch (IOException e) {
				throw new RuntimeException(e);
			}
		}

		class ConfigSerialization extends JavaScriptSerialization {
			protected boolean canReference(Persistable obj, Persistable referrer) {
				return obj.getId().source != AbstractJsonSource.this && obj.getId().source != null; 
			}
		}

	};
	boolean useIds(){
		return true;
	}
	protected Object convertJsonToJavaScript(Object value, String id) {
		String fullId = (id.startsWith("http://") || id.startsWith("https://")) ? id : (getId() + '/' + id); 
		if (value instanceof Map || value instanceof List){
			if (value instanceof Map && ((Map)value).containsKey("$ref")){
	    		Identification refId = Identification.idForRelativeString(fullId, (String) ((Map)value).get("$ref"));
	    		if(refId instanceof ObjectId && ((ObjectId)refId).subObjectId == null){
	    			refId = ObjectId.idForObject(DataSourceManager.getMetaClassSource(), ((ObjectId)refId).source.getId());
	    		}
	    		return refId;
			}
			Identification objId;
			if (value instanceof Map && ((Map)value).containsKey("id") && useIds())
	    		objId= Identification.idForRelativeString(fullId, (String) ((Map)value).get("id"));
			else
				objId = ObjectId.idForObject(this, id, true);
			if(!(objId instanceof ObjectId))
				throw new RuntimeException("An canonical id must be used for object identification");
			synchronized(objId){
				PersistableInitializer initializer = DataSourceHelper.initializeObject((ObjectId) objId);
				if(!PersistableObject.checkForExistingDirty(initializer)){
					Object result = mapJson(initializer,value, objId.subObjectId);
					if(result != null)
					return result;
				}
				
			}
			return objId;
		}
		else if (value instanceof JSONFunction){
			if(isTrusted()){
				value = FunctionUtils.createFunction(((JSONFunction) value).toString(), "function");
			}else{
				value = FunctionUtils.createFunction("function(){throw new AccessError(\"Server code not trusted from " + getId() + "\");}", "function");
			}
		}
		return value;
	}
	//TODO: mapArray
	public void mapObject(PersistableInitializer initializer, final String objectId) throws Exception {
		ObjectId objId = ObjectId.idForObject(this, objectId);

		final Object object = getMap(objectId).resolved;
		mapJson(initializer,object,objectId);
	}
	protected Object mapJson(PersistableInitializer initializer, final Object object, final String objectId) {
		if (object instanceof Map){ 
			for (Map.Entry<String,Object> entry : ((Map<String,Object>)object).entrySet()){
				String key = entry.getKey();
				if (!"id".equals(key))
					initializer.setProperty(key, convertJsonToJavaScript(entry.getValue(),objectId + getPathSeparator() + key));
			}
		}
		else if (object instanceof List){
			initializer.initializeList(new AbstractCollection<Object>(){
			    public Iterator<Object> iterator(){
			    	return new Iterator(){
						int i = 0;
						public boolean hasNext() {
							return i < ((List)object).size();
						}

						public Object next() {
							try {
								return convertJsonToJavaScript(((List)object).get(i),objectId + getPathSeparator() + i++);
								
								
							} catch (JSONException e) {
								throw new RuntimeException(e);
							}
						}

						public void remove() {
							throw new UnsupportedOperationException("Not  implemented yet");
						}
					};	    	
			    }

			    public int size(){
					return ((List)object).size();	
			    }

			});
		}
		if(objectId.indexOf(getPathSeparator()) != -1) {
			int lastPathStart = objectId.lastIndexOf(getPathSeparator());
			initializer.setParent(ObjectId.idForObject(this, objectId.substring(0,lastPathStart), true));
		}
		return null;
	}
	protected static String getResourceAsString(InputStream is) throws IOException {
		BufferedReader in = new BufferedReader(new InputStreamReader(is));
		StringBuffer buffer = new StringBuffer();
		String line;
		while ((line = in.readLine()) != null) {
			buffer.append(line);
			buffer.append("\n");
		}
		return buffer.toString();
	}
	InheritableThreadLocal<Set<String>> dirty = new InheritableThreadLocal<Set<String>>();
	InheritableThreadLocal<Set<Persistable>> newObjects = new InheritableThreadLocal<Set<Persistable>>();
	public void abortTransaction() throws Exception {
	}
	protected abstract void setJson(String resourceName, String json) throws Exception;
	// TODO: May need to include the target table when doing path based guesses for POSTs
	protected abstract void newJson(String json) throws Exception;
	/**
	 * suppress writes during startup
	 */
	public static boolean suppressWrites = true ;
	/**
	 * serializes all the dirty objects
	 * @throws Exception
	 */
	public void commitTransaction() throws Exception {
		if(suppressWrites)
			return;
		for (String resourceName : dirty.get()){
			setJson(resourceName,serialize(ObjectId.idForObject(this, resourceName, true).getTarget()));
		}
		for (Persistable newObject : newObjects.get()){
			newJson(serialize(newObject));
		}
	}
	public NewObjectPersister recordNewObject(Persistable object) throws Exception {
		newObjects.get().add(object);
		return new StartAsEmptyPersister(){
			String id = (Math.random() +"").substring(2);
			public String getObjectId() {
				return id;
			}
			public boolean isHiddenId() {
				return true;
			}

			public DataSource getSource() {
				return AbstractJsonSource.this;
			}

			@Override
			public void recordProperty(String name, Object value) throws Exception {
			}
			
		};
	}

	protected String serialize(Persistable target){
		StringWriter writer = new StringWriter();
		serializer.serialize(target, new DataSerializer.Request(){

			public int[] getIndexRange(List obj) {
				return new int[]{0,obj.size()};
			}

			public String idString(Identification id) {
				return id.toString(AbstractJsonSource.this, "");
			}
			

			public boolean shouldSerialize(Persistable obj) {
				return obj.getId().source == AbstractJsonSource.this || obj.getId().source == null;
			}

			public boolean getFeature(SerializerFeature feature) {
				return feature == SerializerFeature.IncludeServerMethods;
			}
			
		}, writer);
		return writer.toString();
	}
	public void makeDirty(String objectId){
		dirty.get().add(objectId.split(getPathSeparator())[0]);
	}
	public void startTransaction() throws Exception {
		dirty.set(new HashSet<String>());
		newObjects.set(new HashSet<Persistable>());
	}
	public boolean hiddenId(String id) {
		return id == null || id.indexOf(getPathSeparator()) > -1;
	}
	public void recordList(String objectId, List<? extends Object> values)throws Exception {
		int i = 0;
		for(Object value : values)
			handleNewObject(value, "" + (i++), objectId);
		makeDirty(objectId);
	}
	public void recordPropertyAddition(String objectId, String name, Object value, int attributes) throws Exception {
		handleNewObject(value,name,objectId);
		makeDirty(objectId);
		//recordPropertyChange(objectId, name, value);
	}
	/*Object getDirtyObject(String objectId)throws Exception{
		RootAndResolved rar = getMap(objectId);
		dirtyJson.put(objectId.split(pathSeparator)[0], rar.root);
		return rar.resolved;
	}*/
	public void recordPropertyChange(String objectId, String name, Object value, int attributes) throws Exception {
		//((Map)getDirtyObject(objectId)).put(name, convertJavaScriptToJson(value,objectId + pathSeparator + name));
		//saveChange(objectId);
		handleNewObject(value,name,objectId);
		makeDirty(objectId);
	}
	public void recordPropertyRemoval(String objectId, String name) throws Exception {
		makeDirty(objectId);
	}

	protected void handleNewObject(Object value, final String key, final String parentId){
		if (value instanceof ObjectId) {
			ObjectId id = (ObjectId) value;
				id.persistIfNeeded(new StartAsEmptyPersister(){

					@Override
					public void initializeAsList(List<? extends Object> values) throws Exception {
						int i = 0;
						for(Object value : values){
							handleNewObject(value instanceof Persistable ? ((Persistable) value).getId() : value, "" + (i++), getObjectId());
						}
					}

					public boolean isHiddenId() {
						return true;
					}

					@Override
					public void recordProperty(String name, Object value) throws Exception {
						handleNewObject(value instanceof Persistable ? ((Persistable) value).getId() : value, name, getObjectId());
					}

					public String getObjectId() {
						return parentId + getPathSeparator() + key;
					}

					public ObjectId getParent() {
						return ObjectId.idForObject(AbstractJsonSource.this, parentId, true);
					}

					public DataSource getSource() {
						return AbstractJsonSource.this;
					}


				});
		}
	}
}
