//Copyright (c) Corporation for National Research Initiatives
package org.python.core;

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

/**
 * The Python slice object.
 */
@ExposedType(name = "slice", isBaseType = false, doc = BuiltinDocs.slice_doc)
public class PySlice extends PyObject {

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

    @ExposedGet(doc = BuiltinDocs.slice_start_doc)
    public PyObject start = Py.None;

    @ExposedGet(doc = BuiltinDocs.slice_stop_doc)
    public PyObject stop = Py.None;

    @ExposedGet(doc = BuiltinDocs.slice_step_doc)
    public PyObject step = Py.None;

    public PySlice() {
        super(TYPE);
    }

    public PySlice(PyObject start, PyObject stop, PyObject step) {
        super(TYPE);
        if (start != null) {
            this.start = start;
        }
        if (stop != null) {
            this.stop = stop;
        }
        if (step != null) {
            this.step = step;
        }
    }

    @ExposedNew
    static PyObject slice_new(PyNewWrapper new_, boolean init, PyType subtype, PyObject[] args,
                              String[] keywords) {
        if (args.length == 0) {
            throw Py.TypeError("slice expected at least 1 arguments, got " + args.length);
        } else if (args.length > 3) {
            throw Py.TypeError("slice expected at most 3 arguments, got " + args.length);
        }
        ArgParser ap = new ArgParser("slice", args, keywords, "start", "stop", "step");
        PySlice slice = new PySlice();
        if (args.length == 1) {
            slice.stop = ap.getPyObject(0);
        } else if (args.length == 2) {
            slice.start = ap.getPyObject(0);
            slice.stop = ap.getPyObject(1);
        } else if (args.length == 3) {
            slice.start = ap.getPyObject(0);
            slice.stop = ap.getPyObject(1);
            slice.step = ap.getPyObject(2);
        }
        return slice;
    }

    @Override
    public int hashCode() {
        return slice___hash__();
    }

    @ExposedMethod(doc = BuiltinDocs.slice___hash___doc)
    final int slice___hash__() {
        throw Py.TypeError(String.format("unhashable type: '%.200s'", getType().fastGetName()));
    }

    @Override
    public PyObject __eq__(PyObject o) {
        if (getType() != o.getType() && !(getType().isSubType(o.getType()))) {
            return null;
        }
        if (this == o) {
            return Py.True;
        }
        PySlice oSlice = (PySlice)o;
        return Py.newBoolean(eq(getStart(), oSlice.getStart()) && eq(getStop(), oSlice.getStop())
                             && eq(getStep(), oSlice.getStep()));
    }

    private static final boolean eq(PyObject o1, PyObject o2) {
        return o1._cmp(o2) == 0;
    }

    @Override
    public PyObject __ne__(PyObject o) {
        return __eq__(o).__not__();
    }

    public PyObject indices(PyObject len) {
        return slice_indices(len);
    }

    @ExposedMethod(doc = BuiltinDocs.slice_indices_doc)
    final PyObject slice_indices(PyObject len) {
        int[] indices = indicesEx(len.asIndex(Py.OverflowError));
        return new PyTuple(Py.newInteger(indices[0]), Py.newInteger(indices[1]),
                           Py.newInteger(indices[2]));
    }

    /**
     * Calculates the actual indices of a slice with this slice's start, stop, step and
     * slicelength values for a sequence of length <code>len</code>.
     *
     * @return an array with the start at index 0, stop at index 1, step at index 2 and
     *         slicelength at index 3
     */
    public int[] indicesEx(int len) {
        int start;
        int stop;
        int step;
        int slicelength;

        if (getStep() == Py.None) {
            step = 1;
        } else {
            step = calculateSliceIndex(getStep());
            if (step == 0) {
                throw Py.ValueError("slice step cannot be zero");
            }
        }

        if (getStart() == Py.None) {
            start = step < 0 ? len - 1 : 0;
        } else {
            start = calculateSliceIndex(getStart());
            if (start < 0) {
                start += len;
            }
            if (start < 0) {
                start = step < 0 ? -1 : 0;
            }
            if (start >= len) {
                start = step < 0 ? len - 1 : len;
            }
        }

        if (getStop() == Py.None) {
            stop = step < 0 ? -1 : len;
        } else {
            stop = calculateSliceIndex(getStop());
            if (stop < 0) {
                stop += len;
            }
            if (stop < 0) {
                stop = -1;
            }
            if (stop > len) {
                stop = len;
            }
        }

        if ((step < 0 && stop >= start) || (step > 0 && start >= stop)) {
            slicelength = 0;
        } else if (step < 0) {
            slicelength = (stop - start + 1) / (step) + 1;
        } else {
            slicelength = (stop - start - 1) / (step) + 1;
        }

        return new int[] {start, stop, step, slicelength};
    }

    /**
     * Calculate indices for the deprecated __get/set/delslice__ methods.
     *
     * @param obj the object being sliced
     * @param start the slice operation's start
     * @param stop the slice operation's stop
     * @return an array with start at index 0 and stop at index 1
     */
    public static PyObject[] indices2(PyObject obj, PyObject start, PyObject stop) {
        PyObject[] indices = new PyObject[2];
        int istart = (start == null || start == Py.None) ? 0 : calculateSliceIndex(start);
        int istop = (stop == null || stop == Py.None)
                ? PySystemState.maxint : calculateSliceIndex(stop);
        if (istart < 0 || istop < 0) {
            try {
                int len = obj.__len__();
                if (istart < 0) {
                    istart += len;
                }
                if (istop < 0) {
                    istop += len;
                }
            } catch (PyException pye) {
                if (!pye.match(Py.TypeError)) {
                    throw pye;
                }
            }
        }
        indices[0] = Py.newInteger(istart);
        indices[1] = Py.newInteger(istop);
        return indices;
    }

    public static int calculateSliceIndex(PyObject v) {
        if (v.isIndex()) {
            return v.asIndex();
        }
        throw Py.TypeError("slice indices must be integers or None or have an __index__ method");
    }

    @Override
    public String toString() {
        return slice_toString();
    }

    @ExposedMethod(names = "__repr__", doc = BuiltinDocs.slice___repr___doc)
    final String slice_toString() {
        return String.format("slice(%s, %s, %s)", getStart(), getStop(), getStep());
    }

    public final PyObject getStart() {
        return start;
    }

    public final PyObject getStop() {
        return stop;
    }

    public final PyObject getStep() {
        return step;
    }
}
