package org.python.jsr223;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.script.Bindings;
import javax.script.ScriptContext;
import javax.script.ScriptEngine;
import org.python.core.Py;
import org.python.core.PyDictionary;
import org.python.core.PyIterator;
import org.python.core.PyList;
import org.python.core.PyObject;
import org.python.core.PyString;
import org.python.core.PyType;
import org.python.core.Visitproc;
import org.python.core.Untraversable;
import org.python.expose.ExposedType;
import org.python.expose.ExposedGet;
import org.python.expose.ExposedMethod;

/**
 * JSR 223 does not map well to Jython's concept of "locals" and "globals".
 * Instead, SimpleScriptContext provides ENGINE_SCOPE and GLOBAL_SCOPE, each
 * with its own bindings.  We adapt this multi-scope object for use as both
 * a local and global dictionary.
 */
@Untraversable
@ExposedType(name = "scope", isBaseType = false)
public final class PyScriptEngineScope extends PyObject {
    public static final PyType TYPE = PyType.fromClass(PyScriptEngineScope.class);

    private final ScriptContext context;
    private final ScriptEngine engine;

    PyScriptEngineScope(ScriptEngine engine, ScriptContext context) {
        this.context = context;
        this.engine = engine;
    }

    @ExposedGet(name = "context")
    public PyObject pyGetContext() {
        return Py.java2py(context);
    }

    @ExposedGet(name = "engine")
    public PyObject pyGetEngine() {
        return Py.java2py(engine);
    }

    @ExposedMethod
    public PyObject scope_keys() {
        PyList members = new PyList();
        List<Integer> scopes = context.getScopes();
        for (int scope : scopes) {
            Bindings bindings = context.getBindings(scope);
            if (bindings == null)
                continue;
            for (String key : bindings.keySet())
                members.append(new PyString(key));
        }
        members.sort();
        return members;
    }

    // satisfy mapping and lookup
    @ExposedMethod
    @Override
    public PyObject __getitem__(PyObject key) {
        return __finditem__(key);
    }

    // satisfy iterable
    @ExposedMethod
    @Override
    public PyObject __iter__() {
        return new ScopeIterator(this);
    }

    @ExposedMethod(defaults = "Py.None")
    final PyObject scope_get(PyObject keyObj, PyObject defaultObj) {
        String key = keyObj.asString();
        int scope = context.getAttributesScope(key);
        return scope == -1 ? defaultObj : Py.java2py(context.getAttribute(key, scope));
    }

    @ExposedMethod
    final boolean scope_has_key(PyObject key) {
        return context.getAttributesScope(key.asString()) != -1;
    }

    @Override
    public boolean __contains__(PyObject obj) {
        return scope___contains__(obj);
    }

    @ExposedMethod
    final boolean scope___contains__(PyObject obj) {
        return scope_has_key(obj);
    }

    @ExposedMethod(defaults = "Py.None")
    final PyObject scope_setdefault(PyObject keyObj, PyObject failObj) {
        PyObject result;
        String key = keyObj.asString();
        int scope = context.getAttributesScope(key);
        if (scope == -1) {
            scope = ScriptContext.ENGINE_SCOPE;
            context.setAttribute(key,
                                 failObj instanceof PyType
                                 ? failObj : failObj.__tojava__(Object.class),
                                 scope);
            result = failObj;
        } else {
            result = Py.java2py(context.getAttribute(key, scope));
        }
        return result;
    }

    @Override
    public String toString() {
        return getDictionary().toString();
    }

    @Override
    public PyObject __finditem__(PyObject key) {
        return __finditem__(key.asString());
    }

    @Override
    public PyObject __finditem__(String key) {
        int scope = context.getAttributesScope(key);
        if (scope == -1)
            return null;
        return Py.java2py(context.getAttribute(key, scope));
    }

    @ExposedMethod
    @Override
    public void __setitem__(PyObject key, PyObject value) {
        __setitem__(key.asString(), value);
    }

    @Override
    public void __setitem__(String key, PyObject value) {
        int scope = context.getAttributesScope(key);
        if (scope == -1)
            scope = ScriptContext.ENGINE_SCOPE;
        context.setAttribute(key,
                             value instanceof PyType ? value : value.__tojava__(Object.class),
                             scope);
    }

    @ExposedMethod
    @Override
    public void __delitem__(PyObject key) {
        __delitem__(key.asString());
    }

    @Override
    public void __delitem__(String key) {
        int scope = context.getAttributesScope(key);
        if (scope == -1)
            throw Py.KeyError(key);
        context.removeAttribute(key, scope);
    }

    private Map<PyObject, PyObject> getMap() {
        ScopeIterator iterator = new ScopeIterator(this);
        Map<PyObject, PyObject> map = new HashMap<PyObject, PyObject>(iterator.size());
        PyObject key = iterator.__iternext__();
        while (key != null) {
            map.put(key, __finditem__(key));
            key = iterator.__iternext__();
        }
        return map;
    }

    private PyDictionary getDictionary() {
        return new PyDictionary(getMap());
    }

    public class ScopeIterator extends PyIterator {
        private int _index;
        private int _size;
        private PyObject _keys;

        ScopeIterator(PyScriptEngineScope scope) {
            _keys = scope.scope_keys();
            _size = _keys.__len__();
            _index = -1;
        }

        public int size() {
            return _size;
        }

        @Override
        public PyObject __iternext__() {
            PyObject result = null;
            _index++;
            if (_index < size()) {
                result = _keys.__getitem__(_index);
            }
            return result;
        }


        /* Traverseproc implementation */
        @Override
        public int traverse(Visitproc visit, Object arg) {
            int retVal = super.traverse(visit, arg);
            if (retVal != 0) {
                return retVal;
            }
            return _keys != null ? visit.visit(_keys, arg) : 0;
        }

        @Override
        public boolean refersDirectlyTo(PyObject ob) {
            return ob != null && (ob == _keys || super.refersDirectlyTo(ob));
        }
    }
}
