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

import org.python.expose.ExposedGet;
import org.python.expose.ExposedMethod;
import org.python.expose.ExposedNew;
import org.python.expose.ExposedType;

@ExposedType(name = "property", doc = BuiltinDocs.property_doc)
public class PyProperty extends PyObject implements Traverseproc {

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

    @ExposedGet(doc = BuiltinDocs.property_fget_doc)
    protected PyObject fget;

    @ExposedGet(doc = BuiltinDocs.property_fset_doc)
    protected PyObject fset;

    @ExposedGet(doc = BuiltinDocs.property_fdel_doc)
    protected PyObject fdel;

    /** Whether this property's __doc__ was copied from its getter. */
    protected boolean docFromGetter;

    @ExposedGet(name = "__doc__")
    protected PyObject doc;

    public PyProperty() {
        this(TYPE);
    }

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

    @ExposedNew
    @ExposedMethod(doc = BuiltinDocs.property___init___doc)
    public void property___init__(PyObject[] args, String[] keywords) {
        ArgParser ap = new ArgParser("property", args, keywords,
                                     new String[] {"fget", "fset", "fdel", "doc"}, 0);
        fget = ap.getPyObject(0, null);
        fget = fget == Py.None ? null : fget;
        fset = ap.getPyObject(1, null);
        fset = fset == Py.None ? null : fset;
        fdel = ap.getPyObject(2, null);
        fdel = fdel == Py.None ? null : fdel;
        doc = ap.getPyObject(3, null);

        // if no docstring given and the getter has one, use fget's
        if ((doc == null || doc == Py.None) && fget != null) {
            PyObject getDoc = fget.__findattr__("__doc__");
            if (getType() == TYPE) {
                doc = getDoc;
            } else {
                // Put __doc__ in dict of the subclass instance instead, otherwise it gets
                // shadowed by class's __doc__
                __setattr__("__doc__", getDoc);
            }
            docFromGetter = true;
        }
    }

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

    @Override
    public PyObject __get__(PyObject obj, PyObject type) {
        return property___get__(obj,type);
    }

    @ExposedMethod(defaults = "null", doc = BuiltinDocs.property___get___doc)
    final PyObject property___get__(PyObject obj, PyObject type) {
        if (obj == null || obj == Py.None) {
            return this;
        }
        if (fget == null) {
            throw Py.AttributeError("unreadable attribute");
        }
        return fget.__call__(obj);
    }

    @Override
    public void __set__(PyObject obj, PyObject value) {
        property___set__(obj, value);
    }

    @ExposedMethod(doc = BuiltinDocs.property___set___doc)
    final void property___set__(PyObject obj, PyObject value) {
        if (fset == null) {
            throw Py.AttributeError("can't set attribute");
        }
        fset.__call__(obj, value);
    }

    @Override
    public void __delete__(PyObject obj) {
        property___delete__(obj);
    }

    @ExposedMethod(doc = BuiltinDocs.property___delete___doc)
    final void property___delete__(PyObject obj) {
        if (fdel == null) {
            throw Py.AttributeError("can't delete attribute");
        }
        fdel.__call__(obj);
    }

    public PyObject getter(PyObject getter) {
        return property_getter(getter);
    }

    @ExposedMethod(doc = BuiltinDocs.property_getter_doc)
    final PyObject property_getter(PyObject getter) {
        return propertyCopy(getter, null, null);
    }

    public PyObject setter(PyObject setter) {
        return property_setter(setter);
    }

    @ExposedMethod(doc = BuiltinDocs.property_setter_doc)
    final PyObject property_setter(PyObject setter) {
        return propertyCopy(null, setter, null);
    }

    public PyObject deleter(PyObject deleter) {
        return property_deleter(deleter);
    }

    @ExposedMethod(doc = BuiltinDocs.property_deleter_doc)
    final PyObject property_deleter(PyObject deleter) {
        return propertyCopy(null, null, deleter);
    }

    /**
     * Return a copy of this property with the optional addition of a get/set/del. Helper
     * method for the getter/setter/deleter methods.
     */
    private PyObject propertyCopy(PyObject get, PyObject set, PyObject del) {
        if (get == null) {
            get = fget != null ? fget : Py.None;
        }
        if (set == null) {
            set = fset != null ? fset : Py.None;
        }
        if (del == null) {
            del = fdel != null ? fdel : Py.None;
        }

        PyObject doc;
        if (docFromGetter) {
            // make _init use __doc__ from getter
            doc = Py.None;
        } else {
            doc = this.doc != null ? this.doc : Py.None;
        }

        return getType().__call__(get, set, del, doc);
    }


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

    @Override
    public boolean refersDirectlyTo(PyObject ob) {
        return ob != null && (ob == fget || ob == fset || ob == fdel || ob == doc);
    }
}
