import numpy as np
from numpy import ndarray, void
from . import (
    Scalar,
    _array_types,
    _integer_types,
    _float_types,
    _string_types,
    _unsigned_types,
)

_string_types = (str,)  ##JSON cannot deal with bytes

dt_builtins = (
    np.dtype("bool"),
    np.dtype("int8"),
    np.dtype("int16"),
    np.dtype("int32"),
    np.dtype("int64"),
    np.dtype("uint8"),
    np.dtype("uint16"),
    np.dtype("uint32"),
    np.dtype("uint64"),
    np.dtype("float32"),
    np.dtype("float64"),
)


def is_contiguous(data):
    """Returns if the data is C-contiguous
    The last stride must be itemsize (bytesize),
     the second-last must be itemsize * shape[-1], etc.
    These criteria are stricter than Numpy's c_contiguous:
      see the documentation of numpy.ndarray.flags for an explanation
    """
    if not data.flags["C_CONTIGUOUS"]:
        return False
    stride = data.itemsize
    contiguous = False
    for n in range(data.ndim - 1, -1, -1):
        if data.strides[n] != stride:
            break
        stride *= data.shape[n]
    else:
        contiguous = True
    return contiguous


def is_unsigned(dt):
    return any([dt == t for t in _unsigned_types])


def get_typedef_scalar(value):
    if isinstance(value, bool):
        typedef = "boolean"
    elif isinstance(value, _integer_types):  # type: ignore
        typedef = "integer"
    elif isinstance(value, _float_types):  # type: ignore
        typedef = "number"
    elif isinstance(value, _string_types):  # type: ignore
        typedef = "string"
    elif value is None:
        typedef = "null"
    else:
        raise TypeError(type(value))
    return typedef


def get_form_python_inside_numpy(data):
    if isinstance(data, list):
        return get_form_list_plain(data)
    elif isinstance(data, dict):
        return get_form_dict_plain(data)
    elif isinstance(data, _string_types):
        return "pure-plain", "string"
    elif data is None:
        return "pure-plain", "null"
    else:
        raise TypeError(
            type(data)
        )  # must be list, dict, string or None; scalar Python objects not allowed


def get_tform_numpy_builtin(dt):
    dtb = dt.base
    if dtb == object:
        raise TypeError("mixed-binary (PyObject inside Numpy) is not supported")

    if dtb == bool:
        typedef0 = "boolean"
    elif any([dtb == t for t in _integer_types]):
        typedef0 = "integer"
    elif any([dtb == t for t in _float_types]):
        typedef0 = "number"
    elif is_np_str(dtb):
        typedef0 = "string"
    else:
        raise TypeError(dtb)

    typedef = {
        "type": typedef0,
        "bytesize": dt.base.itemsize,
    }
    if typedef0 == "integer":
        unsigned = is_unsigned(dt)
        typedef["unsigned"] = unsigned
    if dt.ndim:
        typedef = {
            "type": "tuple",
            "items": typedef,
            "identical": True,
            "shape": dt.shape,
        }
    return typedef


def get_tform_numpy_struct(dt):
    if not dt.isalignedstruct:
        raise TypeError("Composite dtypes must be memory-aligned", dt)
    if not dt.isnative:
        raise TypeError("Composite dtypes must be native")
    props = {}
    typedef = {"type": "object", "properties": props, "bytesize": dt.itemsize}
    typedef["order"] = list(dt.fields)
    for fieldname in dt.fields:
        ctypedef = get_tform_numpy(dt[fieldname])
        props[fieldname] = ctypedef
    if dt.ndim:
        typedef["storage"] = "pure-binary"
        typedef = {
            "type": "tuple",
            "shape": dt.shape,
            "items": typedef,
            "identical": True,
        }
    return typedef


def is_np_str(dt):
    return dt == np.dtype("S%d" % dt.itemsize) or dt == np.dtype(
        "U%d" % (dt.itemsize / 4)
    )


def get_tform_numpy(dt):
    if dt.base.isbuiltin or is_np_str(dt) or dt in dt_builtins:
        return get_tform_numpy_builtin(dt)
    return get_tform_numpy_struct(dt)


def get_form_dict_plain(data):
    typedef = {"type": "object"}
    if not len(data):
        return "pure-plain", typedef
    props = {}
    typedef["properties"] = props  # type: ignore
    storages = {}
    for k, v in data.items():
        cstorage, ctypedef = get_form(v)
        props[k] = ctypedef
        storages[k] = cstorage
    storage_set = set(storages.values())
    if len(storage_set) == 1 and storage_set.pop() == "pure-plain":
        storage = "pure-plain"
    else:
        storage = "mixed-plain"
        for fieldname in data:
            cstorage = storages[fieldname]
            if cstorage == "pure-plain":
                continue
            ctypedef = props[fieldname]
            if isinstance(ctypedef, str):
                ctypedef = {"type": ctypedef}
                props[fieldname] = ctypedef
            ctypedef["storage"] = cstorage
    return storage, typedef


def get_form_dict(data):
    if isinstance(data, Scalar):  # type: ignore
        raise TypeError
    elif isinstance(data, void):
        dt = data.dtype
        storage = "pure-binary"
        typedef = get_tform_numpy(dt)
    elif isinstance(data, _array_types):
        raise TypeError
    elif isinstance(data, dict):
        storage, typedef = get_form_dict_plain(data)
    else:
        raise TypeError
    return storage, typedef


def get_form_items_numpy(data):
    dt = data.dtype
    item_typedef = get_tform_numpy(dt)
    identical = True
    items = item_typedef
    return items, identical


def get_form_items_list_plain(data):
    identical = True
    storages = []
    items2 = []
    for d in data:
        item_storage, item_typedef = get_form(d)
        storages.append(item_storage)
        items2.append(item_typedef)

    storage_set = set(storages)
    if len(storage_set) > 1:
        different = True
    else:
        different = False
        for item_typedef in items2[1:]:
            if item_typedef != items2[0]:
                different = True
                break
    if different:
        items = items2
        identical = False
        if len(storage_set) == 1 and storage_set.pop() == "pure-plain":
            storage = "pure-plain"
        else:
            storage = "mixed-plain"
        for n in range(len(data)):
            s = storages[n]
            if s == "pure-plain":
                continue
            ctypedef = items[n]
            if isinstance(ctypedef, str):
                ctypedef = {"type": ctypedef}
                items[n] = ctypedef
            items[n]["storage"] = s
    else:
        if not len(items2):
            storage = "pure-plain"
            items = None
        else:
            items = items2[0]
            child_storage = storages[0]
            if child_storage == "pure-plain":
                storage = "pure-plain"
            else:
                storage = "mixed-plain"
                if isinstance(items, str):
                    items = {"type": items}
                items["storage"] = child_storage
    return storage, items, identical


def get_form_list(data):
    extra = {}
    if isinstance(data, ndarray):
        dt = data.dtype
        if not dt.isnative:
            raise TypeError("dtypes must be native")
        storage = "pure-binary"
        items, identical = get_form_items_numpy(data)
        extra = {
            "shape": data.shape,
        }
        strides = data.strides
        contiguous = is_contiguous(data)
        extra["contiguous"] = contiguous
        if not contiguous:
            extra["strides"] = strides
    elif isinstance(data, _array_types):
        storage, items, identical = get_form_items_list_plain(data)
        extra = {
            "shape": (len(data),),
        }
    elif isinstance(data, dict):
        raise TypeError
    elif isinstance(data, Scalar):  # type: ignore
        raise TypeError
    elif isinstance(data, void):
        raise TypeError
    else:
        raise TypeError

    typedef = {"type": "array", "items": items, "identical": identical}
    typedef.update(extra)
    return storage, typedef


def get_form_list_plain(data):
    # data must be a plain list
    storage, items, identical = get_form_items_list_plain(data)
    typedef = {
        "type": "array",
        "items": items,
        "identical": identical,
        "shape": (len(data),),
    }
    return storage, typedef


def get_form(data):
    if isinstance(data, bytes):
        data = np.array(data)
    if isinstance(data, Scalar):  # type: ignore
        storage, typedef = "pure-plain", get_typedef_scalar(data)
    elif isinstance(data, void):
        dt = data.dtype
        storage = "pure-binary"
        typedef = get_tform_numpy(dt)
    elif isinstance(data, _array_types):
        storage, typedef = get_form_list(data)
    elif isinstance(data, dict):
        storage, typedef = get_form_dict_plain(data)
    else:
        raise TypeError(type(data))
    assert storage in (
        None,
        "pure-plain",
        "mixed-plain",
        "pure-binary",
    ), storage
    return storage, typedef
