/* Copyright (c) Jython Developers */
package org.python.modules._functools;

import java.util.Map;

import org.python.core.Py;
import org.python.core.PyDictionary;
import org.python.core.PyNewWrapper;
import org.python.core.PyObject;
import org.python.core.PyStringMap;
import org.python.core.PyTuple;
import org.python.core.PyType;
import org.python.expose.ExposedGet;
import org.python.expose.ExposedMethod;
import org.python.expose.ExposedNew;
import org.python.expose.ExposedSet;
import org.python.expose.ExposedType;
import org.python.util.Generic;

@ExposedType(name = "_functools.partial")
public class PyPartial extends PyObject {

    public static final PyType TYPE = PyType.fromClass(PyPartial.class);

    /** The wrapped callable. */
    @ExposedGet
    public PyObject func;

    /** Callable's args. */
    public PyObject[] args;

    /** Callable's keywords. */
    private String[] keywords;

    /** Lazily created dict for extra attributes. */
    private PyObject __dict__;

    public PyPartial() {
        super(TYPE);
    }

    public PyPartial(PyType subType) {
        super(subType);
    }

    @ExposedNew
    public static PyObject partial___new__(PyNewWrapper new_, boolean init, PyType subtype,
                                           PyObject[] args, String[] keywords) {
        if (args.length - keywords.length < 1) {
            throw Py.TypeError("type 'partial' takes at least one argument");
        }

        PyObject func = args[0];
        if (!func.isCallable()) {
            throw Py.TypeError("the first argument must be callable");
        }

        PyObject[] noFunc = new PyObject[args.length - 1];
        System.arraycopy(args, 1, noFunc, 0, args.length - 1);
        args = noFunc;

        PyPartial partial;
        if (new_.for_type == subtype) {
            partial = new PyPartial();
        } else {
            partial = new PyPartialDerived(subtype);
        }

        partial.func = func;
        partial.args = args;
        partial.keywords = keywords;
        return partial;
    }

    @Override
    public PyObject __call__(PyObject[] args, String[] keywords) {
        return partial___call__(args, keywords);
    }

    @ExposedMethod
    public PyObject partial___call__(PyObject[] args, String[] keywords) {
        PyObject[] argAppl;
        String[] kwAppl;
        int partialArgc = this.args.length - this.keywords.length;
        int argc = args.length - keywords.length;

        if (partialArgc == 0 && this.keywords.length == 0) {
            argAppl = args;
            kwAppl = keywords;
        } else if (argc == 0 && keywords.length == 0) {
            argAppl = this.args;
            kwAppl = this.keywords;
        } else {
            // first merge keywords to determine the keyword count
            Map<String, PyObject> merged = Generic.map();
            int i;
            for (i = 0; i < this.keywords.length; i++) {
                String keyword = this.keywords[i];
                PyObject value = this.args[partialArgc + i];
                merged.put(keyword, value);
            }
            for (i = 0; i < keywords.length; i++) {
                String keyword = keywords[i];
                PyObject value = args[argc + i];
                merged.put(keyword, value);
            }
            int keywordc = merged.size();

            // finally merge into args and keywords arrays
            argAppl = new PyObject[partialArgc + argc + keywordc];
            System.arraycopy(this.args, 0, argAppl, 0, partialArgc);
            System.arraycopy(args, 0, argAppl, partialArgc, argc);

            kwAppl = new String[keywordc];
            i = 0;
            int j = partialArgc + argc;
            for (Map.Entry<String, PyObject> entry : merged.entrySet()) {
                kwAppl[i++] = entry.getKey();
                argAppl[j++] = entry.getValue();
            }
        }
        return func.__call__(argAppl, kwAppl);
    }

    @ExposedGet(name = "args")
    public PyObject getArgs() {
        PyObject[] justArgs;
        if (keywords.length == 0) {
            justArgs = args;
        } else {
            int argc = args.length - keywords.length;
            justArgs = new PyObject[argc];
            System.arraycopy(args, 0, justArgs, 0, argc);
        }
        return new PyTuple(justArgs);
    }

    @ExposedGet(name = "keywords")
    public PyObject getKeywords() {
        if (keywords.length == 0) {
            return Py.None;
        }
        int argc = args.length - keywords.length;
        PyObject kwDict = new PyDictionary();
        for (int i = 0; i < keywords.length; i++) {
            kwDict.__setitem__(Py.newString(keywords[i]), args[argc + i]);
        }
        return kwDict;
    }

    @Override
    public void __setattr__(String name, PyObject value) {
    	partial___setattr__(name, value);
    }

    @ExposedMethod
    final void partial___setattr__(String name, PyObject value) {
        ensureDict();
        super.__setattr__(name, value);
    }

    @Override
    public PyObject fastGetDict() {
        return __dict__;
    }

    @Override
    @ExposedGet(name = "__dict__")
    public PyObject getDict() {
        ensureDict();
        return __dict__;
    }

    @Override
    @ExposedSet(name = "__dict__")
    public void setDict(PyObject val) {
        if (!(val instanceof PyStringMap) && !(val instanceof PyDictionary)) {
            throw Py.TypeError("setting partial object's dictionary to a non-dict");
        }
        __dict__ = val;
    }

    private void ensureDict() {
        if (__dict__ == null) {
            __dict__ = new PyStringMap();
        }
    }
}
