package org.dynalang.mop.jython;

import java.util.Iterator;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Map.Entry;

import org.dynalang.mop.CallProtocol;
import org.dynalang.mop.ClassBasedMetaobjectProtocol;
import org.dynalang.mop.impl.MetaobjectProtocolBase;
import org.python.core.Py;
import org.python.core.PyException;
import org.python.core.PyObject;
import org.python.core.PyString;

/**
 * @author Attila Szegedi
 * @version $Id: $
 */
public class JythonMetaobjectProtocol extends MetaobjectProtocolBase
implements ClassBasedMetaobjectProtocol {

    public Result delete(Object target, Object propertyId) {
	String name = getInternedPropertyName(propertyId);
	if(name == null) {
	    return Result.doesNotExist;
	}
	try {
	    ((PyObject)target).__delattr__(name);
	    return Result.ok;
	}
	catch(PyException e) {
	    return pyExceptionToResult(e);
	}
    }

    public Boolean has(Object target, Object propertyId) {
	return get(target, propertyId) == null ? null : Boolean.TRUE;
    }

    @Override
    public Iterator<? extends Object> propertyIds(Object target) {
	final PyObject pyTarget = (PyObject)target;
	final PyObject iter = pyTarget.__dir__().__iter__();
	return new Iterator<Object>() {
	    private PyObject next = iter.__iternext__();
	    private PyObject lastNext;
	    
	    public boolean hasNext() {
		return next != null;
	    }

	    public Object next() {
		if(next == null) {
		    throw new NoSuchElementException();
		}
		lastNext = next;
		next = iter.__iternext__();
		return lastNext;
	    }

	    public void remove() {
		pyTarget.__delattr__(getInternedPropertyName(lastNext));
	    }
	};
    }
    
    public Iterator<Entry> properties(Object target) {
	final PyObject pyTarget = (PyObject)target;
	final Iterator<? extends Object> keys = propertyIds(target);
	return new Iterator<Entry>() {
	    public boolean hasNext() {
		return keys.hasNext();
	    }

	    public Entry next() {
		final PyObject key = (PyObject)keys.next();
		return new Entry() {
		    public Object getKey() {
			return key;
		    }

		    public Object getValue() {
			return pyTarget.__getattr__(getInternedPropertyName(key));
		    }

		    public Object setValue(Object value) {
			String name = getInternedPropertyName(key);
			PyObject oldValue = pyTarget.__getattr__(name);
			pyTarget.__setattr__(name, (PyObject)representAs(value, PyObject.class));
			return oldValue;
		    }
		};
	    }

	    public void remove() {
		keys.remove();
	    }
	};
    }

    public Result put(Object target, Object propertyId, Object value,
	    CallProtocol callProtocol) {
	String name = getInternedPropertyName(propertyId);
	if(name == null) {
	    return Result.doesNotExist;
	}
	try {
	    value = callProtocol.representAs(value, PyObject.class);
	    if(value instanceof PyObject) {
		((PyObject)target).__setattr__(name, (PyObject)value);
		return Result.ok;
	    }
	    return Result.noRepresentation;
	}
	catch(PyException e) {
	    return pyExceptionToResult(e);
	}
    }
    
    public Object call(Object callable, CallProtocol callProtocol, Map args) {
	PyObject pyCallable = (PyObject)callable;
	if(!pyCallable.isCallable()) {
	    return Result.notCallable;
	}
	String[] names = new String[args.size()];
	PyObject[] values = new PyObject[args.size()];
	int i = 0;
	for (Iterator<Map.Entry> it = args.entrySet().iterator(); it.hasNext();) {
	    Map.Entry entry = it.next();
	    Object value = callProtocol.representAs(entry.getValue(), PyObject.class);
	    if(value instanceof PyObject) {
		names[i] = String.valueOf(entry.getKey());
		values[i++] = (PyObject)value;
	    }
	    else {
		return Result.noRepresentation;
	    }
	}
	return pyCallable.__call__(values, names);
    }

    public Object call(Object callable, CallProtocol callProtocol, Object... args) {
	PyObject pyCallable = (PyObject)callable;
	if(!pyCallable.isCallable()) {
	    return Result.notCallable;
	}
	// Use (theoretically) faster path for 0-2 arguments
	switch(args.length) {
	    case 0: {
		return pyCallable.__call__();
	    }
	    case 1: {
		Object arg0 = callProtocol.representAs(args[0], PyObject.class);
		if(arg0 instanceof PyObject) {
		    return pyCallable.__call__((PyObject)arg0);
		}
		return Result.noRepresentation;
	    }
	    case 2: {
		Object arg0 = callProtocol.representAs(args[0], PyObject.class);
		if(arg0 instanceof PyObject) {
		    Object arg1 = callProtocol.representAs(args[1], PyObject.class);
		    if(arg1 instanceof PyObject) {
			return pyCallable.__call__((PyObject)arg0, (PyObject)arg1);
		    }
		}
		return Result.noRepresentation;
	    }
	    default: {
		PyObject[] pyArgs = new PyObject[args.length];
		for (int i = 0; i < args.length; i++) {
		    Object arg = callProtocol.representAs(args[i], PyObject.class);
		    if(!(arg instanceof PyObject)) {
			return Result.noRepresentation;
		    }
		    pyArgs[i] = (PyObject)arg;
		}
		return pyCallable.__call__(pyArgs);
	    }
	}
    }

    public Object get(Object target, Object propertyId) {
	String name = getInternedPropertyName(propertyId);
	if(name == null) {
	    return Result.doesNotExist;
	}
	Object value = ((PyObject)target).__findattr__(name);
	// TODO: what do we do with PyNone?
	return value == null ? Result.doesNotExist : value;
    }

    public Object representAs(Object object, Class targetClass) {
	if(object instanceof PyObject) {
	    Object obj = ((PyObject)object).__tojava__(targetClass);
	    return obj != Py.NoConversion ? obj : Result.noAuthority;
	}
	else if(targetClass == PyObject.class) {
	    return Py.java2py(object);
	}
	return Result.noAuthority;
    }

    public boolean isAuthoritativeForClass(Class clazz) {
	return PyObject.class.isAssignableFrom(clazz);
    }

    private String getInternedPropertyName(Object propertyId) {
	if(propertyId instanceof PyString) {
	    return ((PyString)propertyId).internedString();
	}
	else if(propertyId instanceof String) {
	    return ((String)propertyId).intern();
	}
	return null;
    }

    private static Result pyExceptionToResult(PyException e) {
        if(e.type == Py.AttributeError) {
 	    String msg = e.getMessage();
    	    if(msg.endsWith(" is read-only")) {
	        return Result.notWritable;
	    }
	    else if(msg.lastIndexOf("has no attribute") != -1) {
		return Result.doesNotExist;
	    }
	}
    	throw e;
    }
}