
from rpython.rlib.objectmodel import specialize, we_are_translated
from rpython.rlib.rarithmetic import intmask

from hippy.objects.base import W_Object
from hippy.objects.reference import W_Reference, VirtualReference
from hippy.objects.convert import force_float_to_int_in_any_way
from hippy.error import ConvertError
from collections import OrderedDict
from rpython.rlib.rstring import StringBuilder


class CannotConvertToIndex(Exception):
    pass


def new_rdict():
    return OrderedDict()


def try_convert_str_to_int(key):
    # try to convert 'key' from a string to an int, but carefully:
    # we must not remove any space, make sure the result does not
    # overflows, etc.  In general we have to make sure that the
    # result, when converted back to a string, would give exactly
    # the original string.
    try:
        i = intmask(int(key))     # XXX can be done a bit more efficiently
    except (ValueError, OverflowError):
        raise ValueError
    if str(i) != key:
        raise ValueError
    return i


def compute_next_idx(dct_w):
    next_idx = 0
    for key in dct_w.keys():
        try:
            intkey = try_convert_str_to_int(key)
        except ValueError:
            continue
        if intkey >= next_idx:
            next_idx = intkey + 1
    return next_idx


def wrap_array_key(space,  key):
    try:
        intkey = try_convert_str_to_int(key)
    except ValueError:
        return space.newstr(key)
    return space.newint(intkey)


def convert_to_index(space, w_arg, allow_bogus=False):
    "Returns a pair (int, str), where only one of the two is meaningful"
    if w_arg.tp == space.tp_int:
        return space.int_w(w_arg), None
    elif w_arg.tp == space.tp_str:
        return 0, space.str_w(w_arg)
    elif w_arg.tp == space.tp_float:
        return force_float_to_int_in_any_way(space.float_w(w_arg)), None
    elif w_arg.tp == space.tp_null:
        return 0, ""
    elif w_arg.tp == space.tp_bool:
        if space.is_w(w_arg, space.w_False):
            if allow_bogus:
                return 0, ""
            else:
                return 0, None
        return 1, None
    if allow_bogus:
        if w_arg.tp == space.tp_file_res:
            return 0, space.str_w(w_arg)
        elif w_arg.tp == space.tp_object:
            return 0, space.str_w(w_arg)
        elif w_arg.tp == space.tp_array:
            space.ec.notice("Array to string conversion")
            return 0, "Array"
        else:
            space.ec.warn("Illegal offset type")
            raise CannotConvertToIndex
    else:
        # XXX make a real warning
        space.ec.warn("Illegal offset type")
        raise CannotConvertToIndex


class W_ArrayObject(W_Object):
    """Abstract base class.  Concrete subclasses use various strategies.
    This base class defines the general methods that can be implemented
    without needing to call (too often) the arraylen(), _getitem_str()
    and _getitem_int() methods.
    """

    @staticmethod
    def new_array_from_list(space, lst_w):
        return W_ListArrayObject(space, lst_w)

    @staticmethod
    def new_array_from_rdict(space, dct_w):
        return W_RDictArrayObject(space, dct_w, compute_next_idx(dct_w))

    @staticmethod
    def new_array_from_pairs(space, pairs_ww, allow_bogus=False):
        rdct_w = new_rdict()
        next_idx = 0
        for w_key, w_value in pairs_ww:
            if w_key is not None:
                try:
                    as_int, as_str = convert_to_index(space, w_key,
                                                      allow_bogus=allow_bogus)
                except CannotConvertToIndex:
                    continue
                if as_str is not None:
                    # it was string, maybe it can be integer
                    try:
                        as_int = try_convert_str_to_int(as_str)
                        # yes it can
                        as_str = None
                    except ValueError:
                        pass
            # no key, just increment next_idx
            else:
                as_int, as_str = next_idx, None
            if as_str is None:
                if as_int >= next_idx:
                    next_idx = as_int + 1
                as_str = str(as_int)
            rdct_w[as_str] = w_value

        return W_RDictArrayObject(space, rdct_w, next_idx=next_idx)

    def copy_item(self):
        return self.copy()

    def is_true(self, space):
        return self.arraylen() > 0

    def int_w(self, space):
        return int(self.is_true(space))

    def as_int_arg(self, space):
        raise ConvertError('cannot use array as integer argument')

    def as_number(self, space):
        return space.wrap(self.is_true(space))

    def abs(self, space):
        return space.w_False

    def _lookup_item_ref(self, space, w_arg, give_notice=False):
        """Return a possibly virtual reference to the item,
        or None if the lookup fails
        """
        try:
            as_int, as_str = convert_to_index(space, w_arg)
        except CannotConvertToIndex:
            space.ec.warn("Illegal offset type")
            return None
        if as_str is None:
            r_item = self._getitem_int(as_int)
            if r_item is None:
                if give_notice:
                    space.ec.notice("Undefined offset: %d" % as_int)
                return None
        else:
            r_item = self._getitem_str(as_str)
            if r_item is None:
                if give_notice:
                    space.ec.notice("Undefined index: %s" % as_str)
                return None
        return r_item

    def getitem(self, space, w_arg, give_notice=False):
        try:
            as_int, as_str = convert_to_index(space, w_arg)
        except CannotConvertToIndex:
            space.ec.warn("Illegal offset type")
            return space.w_Null
        if as_str is None:
            r_item = self._getitem_int(as_int)
            if r_item is None:
                if give_notice:
                    space.ec.notice("Undefined offset: %d" % as_int)
                return space.w_Null
        else:
            r_item = self._getitem_str(as_str)
            if r_item is None:
                if give_notice:
                    space.ec.notice("Undefined index: %s" % as_str)
                return space.w_Null
        if isinstance(r_item, VirtualReference):
            return r_item.deref()
        else:
            return r_item

    def setitem2_maybe_inplace(self, space, w_arg, w_value, unique_item=False):
        try:
            as_int, as_str = convert_to_index(space, w_arg)
        except CannotConvertToIndex:
            space.ec.warn("Illegal offset type")
            return self, space.w_Null
        if as_str is None:
            w_arr = self._setitem_int(as_int, w_value, False, unique_item)
        else:
            w_arr = self._setitem_str(as_str, w_value, False, unique_item)
        return w_arr, w_value

    def _setitem_ref(self, space, w_arg, w_ref):
        try:
            as_int, as_str = convert_to_index(space, w_arg)
        except CannotConvertToIndex:
            space.ec.warn("Illegal offset type")
            return self
        if as_str is None:
            return self._setitem_int(as_int, w_ref, True)
        else:
            return self._setitem_str(as_str, w_ref, True)

    def appenditem_inplace(self, space, w_item, as_ref=False):
        # For now this always succeeds in appending the item in-place.
        # It may need to be reconsidered if we add more strategies.
        self._appenditem(w_item, as_ref)
        return w_item

    def packitem_maybe_inplace(self, space, w_arg, w_value):
        as_int, as_str = convert_to_index(space, w_arg)
        if as_str is not None:
            try:
                try_convert_str_to_int(as_str)
            except ValueError:
                # really not an int
                return self._setitem_str(as_str, w_value, False)
        self._appenditem(w_value)
        return self

    def _unsetitem(self, space, w_arg):
        as_int, as_str = convert_to_index(space, w_arg)
        if as_str is None:
            return self._unsetitem_int(as_int)
        else:
            return self._unsetitem_str(as_str)

    def isset_index(self, space, w_index):
        as_int, as_str = convert_to_index(space, w_index)
        if as_str is None:
            return self._isset_int(as_int)
        else:
            return self._isset_str(as_str)

    def _getitem_int(self, index):
        raise NotImplementedError("abstract")

    def _getitem_str(self, key):
        raise NotImplementedError("abstract")

    def _appenditem(self, w_obj, as_ref=False):
        raise NotImplementedError("abstract")

    def _setitem_int(self, index, w_value, as_ref, unique_item=False):