/*
 * libmtp.c
 * Copyright (C) 2012 Kovid Goyal <kovid at kovidgoyal.net>
 *
 * Distributed under terms of the GPL3 license.
 */


#define PY_SSIZE_T_CLEAN
#define UNICODE
#include <Python.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <stdbool.h>
#include <libmtp.h>

#include "devices.h"

// Macros and utilities {{{
static PyObject *MTPError = NULL;

#define ENSURE_DEV(rval)                                                    \
    if (self->device == NULL) {                                             \
        PyErr_SetString(MTPError, "This device has not been initialized."); \
        return rval;                                                        \
    }

#define ENSURE_STORAGE(rval)                                                 \
    if (self->device->storage == NULL) {                                     \
        PyErr_SetString(MTPError, "The device has no storage information."); \
        return rval;                                                         \
    }

// Storage types
#define ST_Undefined 0x0000
#define ST_FixedROM 0x0001
#define ST_RemovableROM 0x0002
#define ST_FixedRAM 0x0003
#define ST_RemovableRAM 0x0004

// Storage Access capability
#define AC_ReadWrite 0x0000
#define AC_ReadOnly 0x0001
#define AC_ReadOnly_with_Object_Deletion 0x0002


typedef struct {
    PyObject *obj;
    PyObject *extra;
    PyThreadState *state;
} ProgressCallback;

static int
report_progress(uint64_t const sent, uint64_t const total, void const *const data) {
    PyObject *res;
    ProgressCallback *cb;

    cb = (ProgressCallback *)data;
    if (cb->obj != NULL) {
        PyEval_RestoreThread(cb->state);
        res = PyObject_CallFunction(cb->obj, "KK", (unsigned long long)sent, (unsigned long long)total);
        Py_XDECREF(res);
        cb->state = PyEval_SaveThread();
    }
    return 0;
}

static void
dump_errorstack(LIBMTP_mtpdevice_t *dev, PyObject *list) {
    LIBMTP_error_t *stack;
    PyObject *err;

    for (stack = LIBMTP_Get_Errorstack(dev); stack != NULL; stack = stack->next) {
        err = Py_BuildValue("is", stack->errornumber, stack->error_text);
        if (err == NULL) break;
        PyList_Append(list, err);
        Py_DECREF(err);
    }

    LIBMTP_Clear_Errorstack(dev);
}

static uint16_t
data_to_python(void *params, void *priv, uint32_t sendlen, unsigned char *data, uint32_t *putlen) {
    PyObject *res;
    ProgressCallback *cb;
    uint16_t ret = LIBMTP_HANDLER_RETURN_OK;

    cb = (ProgressCallback *)priv;
    *putlen = sendlen;
    PyEval_RestoreThread(cb->state);
    Py_ssize_t psz = sendlen;
    res = PyObject_CallMethod(cb->extra, "write", "y#", data, psz);
    if (res == NULL) {
        ret = LIBMTP_HANDLER_RETURN_ERROR;
        *putlen = 0;
        PyErr_Print();
    } else Py_DECREF(res);

    cb->state = PyEval_SaveThread();
    return ret;
}

static uint16_t
data_from_python(void *params, void *priv, uint32_t wantlen, unsigned char *data, uint32_t *gotlen) {
    PyObject *res;
    ProgressCallback *cb;
    char *buf = NULL;
    Py_ssize_t len = 0;
    uint16_t ret = LIBMTP_HANDLER_RETURN_ERROR;

    *gotlen = 0;

    cb = (ProgressCallback *)priv;
    PyEval_RestoreThread(cb->state);
    res = PyObject_CallMethod(cb->extra, "read", "k", (unsigned long)wantlen);
    if (res != NULL && PyBytes_AsStringAndSize(res, &buf, &len) != -1 && len <= wantlen) {
        memcpy(data, buf, len);
        *gotlen = len;
        ret = LIBMTP_HANDLER_RETURN_OK;
    } else PyErr_Print();

    Py_XDECREF(res);
    cb->state = PyEval_SaveThread();
    return ret;
}

static PyObject *
build_file_metadata(LIBMTP_file_t *nf, uint32_t storage_id) {
    PyObject *ans = NULL;

    ans = Py_BuildValue(
        "{s:s, s:k, s:k, s:k, s:K, s:L, s:O}",
        "name",
        nf->filename,
        "id",
        (unsigned long)nf->item_id,
        "parent_id",
        (unsigned long)nf->parent_id,
        "storage_id",
        (unsigned long)storage_id,
        "size",
        nf->filesize,
        "modified",
        (PY_LONG_LONG)nf->modificationdate,
        "is_folder",
        (nf->filetype == LIBMTP_FILETYPE_FOLDER) ? Py_True : Py_False);

    return ans;
}

static PyObject *
file_metadata(LIBMTP_mtpdevice_t *device, PyObject *errs, uint32_t item_id, uint32_t storage_id) {
    LIBMTP_file_t *nf;
    PyObject *ans = NULL;

    Py_BEGIN_ALLOW_THREADS;
    nf = LIBMTP_Get_Filemetadata(device, item_id);
    Py_END_ALLOW_THREADS;
    if (nf == NULL) dump_errorstack(device, errs);
    else {
        ans = build_file_metadata(nf, storage_id);
        LIBMTP_destroy_file_t(nf);
    }
    return ans;
}
// }}}

// Device object definition {{{
typedef struct {
    PyObject_HEAD
        // Type-specific fields go here.
        LIBMTP_mtpdevice_t *device;
    PyObject *ids;
    PyObject *friendly_name;
    PyObject *manufacturer_name;
    PyObject *model_name;
    PyObject *serial_number;
    PyObject *device_version;

} Device;

// Device.__init__() {{{
static void
Device_dealloc(Device *self) {
    if (self->device != NULL) {
        Py_BEGIN_ALLOW_THREADS;
        LIBMTP_Release_Device(self->device);
        Py_END_ALLOW_THREADS;
    }
    self->device = NULL;

    Py_XDECREF(self->ids);
    self->ids = NULL;
    Py_XDECREF(self->friendly_name);
    self->friendly_name = NULL;
    Py_XDECREF(self->manufacturer_name);
    self->manufacturer_name = NULL;
    Py_XDECREF(self->model_name);
    self->model_name = NULL;
    Py_XDECREF(self->serial_number);
    self->serial_number = NULL;
    Py_XDECREF(self->device_version);
    self->device_version = NULL;

    Py_TYPE(self)->tp_free((PyObject *)self);
}

static int
Device_init(Device *self, PyObject *args, PyObject *kwds) {
    unsigned long busnum;
    unsigned char devnum;
    unsigned short vendor_id, product_id;
    PyObject *usb_serialnum;
    char *vendor, *product, *friendly_name, *manufacturer_name, *model_name, *serial_number, *device_version;
    LIBMTP_raw_device_t *rawdevs = NULL, rdev;
    int numdevs, c, tried_count = 0;
    LIBMTP_mtpdevice_t *dev = NULL;
    LIBMTP_error_number_t err;

    if (!PyArg_ParseTuple(args, "kBHHssO", &busnum, &devnum, &vendor_id, &product_id, &vendor, &product, &usb_serialnum)) return -1;

    // We have to build and search the rawdevice list instead of creating a
    // rawdevice directly as otherwise, dynamic bug flag assignment in libmtp
    // does not work
    Py_BEGIN_ALLOW_THREADS;
    err = LIBMTP_Detect_Raw_Devices(&rawdevs, &numdevs);
    Py_END_ALLOW_THREADS;
    if (err == LIBMTP_ERROR_NO_DEVICE_ATTACHED) {
        PyErr_SetString(MTPError, "No raw devices found");
        return -1;
    }
    if (err == LIBMTP_ERROR_CONNECTING) {
        PyErr_SetString(MTPError, "There has been an error connecting");
        return -1;
    }
    if (err == LIBMTP_ERROR_MEMORY_ALLOCATION) {
        PyErr_NoMemory();
        return -1;
    }
    if (err != LIBMTP_ERROR_NONE) {
        PyErr_SetString(MTPError, "Failed to detect raw MTP devices");
        return -1;
    }

    for (c = 0; c < numdevs; c++) {
        rdev = rawdevs[c];
        if (rdev.bus_location == (uint32_t)busnum && rdev.devnum == (uint8_t)devnum) {
            tried_count += 1;
            Py_BEGIN_ALLOW_THREADS;
            dev = LIBMTP_Open_Raw_Device_Uncached(&rdev);
            Py_END_ALLOW_THREADS;
            if (dev != NULL) break;
        }
    }

    if (rawdevs != NULL) free(rawdevs);
    if (dev == NULL) {
        if (tried_count == 0) PyErr_Format(MTPError, "No device with busnum=%lu and devnum=%u found", busnum, devnum);
        else PyErr_Format(MTPError, "Unable to open MTP device with busnum=%lu and devnum=%u, tried %d such devices", busnum, devnum, tried_count);
        return -1;
    }

    self->device = dev;
    self->ids = Py_BuildValue("kBHHO", busnum, devnum, vendor_id, product_id, usb_serialnum);
    if (self->ids == NULL) return -1;

    Py_BEGIN_ALLOW_THREADS;
    friendly_name = LIBMTP_Get_Friendlyname(self->device);
    manufacturer_name = LIBMTP_Get_Manufacturername(self->device);
    model_name = LIBMTP_Get_Modelname(self->device);
    serial_number = LIBMTP_Get_Serialnumber(self->device);
    device_version = LIBMTP_Get_Deviceversion(self->device);
    Py_END_ALLOW_THREADS;

    if (friendly_name != NULL) {
        self->friendly_name = PyUnicode_FromString(friendly_name);
        free(friendly_name);
    }
    if (self->friendly_name == NULL) {
        self->friendly_name = Py_None;
        Py_INCREF(Py_None);
    }

    if (manufacturer_name != NULL) {
        self->manufacturer_name = PyUnicode_FromString(manufacturer_name);
        free(manufacturer_name);
    }
    if (self->manufacturer_name == NULL) {
        self->manufacturer_name = Py_None;
        Py_INCREF(Py_None);
    }

    if (model_name != NULL) {
        self->model_name = PyUnicode_FromString(model_name);
        free(model_name);
    }
    if (self->model_name == NULL) {
        self->model_name = Py_None;
        Py_INCREF(Py_None);
    }

    if (serial_number != NULL) {
        if (serial_number[0]) self->serial_number = PyUnicode_FromString(serial_number);
        free(serial_number);
    }
    if (self->serial_number == NULL) {
        self->serial_number = usb_serialnum == NULL ? Py_None : usb_serialnum;
        Py_INCREF(self->serial_number);
    }

    if (device_version != NULL) {
        self->device_version = PyUnicode_FromString(device_version);
        free(device_version);
    }
    if (self->device_version == NULL) {
        self->device_version = Py_None;
        Py_INCREF(Py_None);
    }

    return 0;
}
// }}}

// Device.friendly_name {{{
static PyObject *
Device_friendly_name(Device *self, void *closure) {
    Py_INCREF(self->friendly_name);
    return self->friendly_name;
} // }}}

// Device.manufacturer_name {{{
static PyObject *
Device_manufacturer_name(Device *self, void *closure) {
    Py_INCREF(self->manufacturer_name);
    return self->manufacturer_name;
} // }}}

// Device.model_name {{{
static PyObject *
Device_model_name(Device *self, void *closure) {
    Py_INCREF(self->model_name);
    return self->model_name;
} // }}}

// Device.serial_number {{{
static PyObject *
Device_serial_number(Device *self, void *closure) {
    Py_INCREF(self->serial_number);
    return self->serial_number;
} // }}}

// Device.device_version {{{
static PyObject *
Device_device_version(Device *self, void *closure) {
    Py_INCREF(self->device_version);
    return self->device_version;
} // }}}

// Device.ids {{{
static PyObject *
Device_ids(Device *self, void *closure) {
    Py_INCREF(self->ids);
    return self->ids;
} // }}}

// Device.update_storage_info() {{{
static PyObject *
Device_update_storage_info(Device *self, PyObject *args) {
    ENSURE_DEV(NULL);
    if (LIBMTP_Get_Storage(self->device, LIBMTP_STORAGE_SORTBY_NOTSORTED) < 0) {
        PyErr_SetString(MTPError, "Failed to get storage info for device.");
        return NULL;
    }
    Py_RETURN_NONE;
}
// }}}

// Device.storage_info {{{
static PyObject *
Device_storage_info(Device *self, void *closure) {
    PyObject *ans, *loc;
    LIBMTP_devicestorage_t *storage;
    int ro = 0;
    ENSURE_DEV(NULL);
    ENSURE_STORAGE(NULL);

    ans = PyList_New(0);
    if (ans == NULL) {
        PyErr_NoMemory();
        return NULL;
    }

    for (storage = self->device->storage; storage != NULL; storage = storage->next) {
        ro = 0;
        // Check if read only storage
        if (storage->StorageType == ST_FixedROM || storage->StorageType == ST_RemovableROM || (storage->id & 0x0000FFFFU) == 0x00000000U ||
            storage->AccessCapability == AC_ReadOnly || storage->AccessCapability == AC_ReadOnly_with_Object_Deletion)
            ro = 1;

        loc = Py_BuildValue(
            "{s:k,s:O,s:K,s:K,s:K,s:s,s:s,s:O}",
            "id",
            (unsigned long)storage->id,
            "removable",
            ((storage->StorageType == ST_RemovableRAM) ? Py_True : Py_False),
            "capacity",
            (unsigned long long)storage->MaxCapacity,
            "freespace_bytes",
            (unsigned long long)storage->FreeSpaceInBytes,
            "freespace_objects",
            (unsigned long long)storage->FreeSpaceInObjects,
            "name",
            storage->StorageDescription,
            "volume_id",
            storage->VolumeIdentifier,
            "rw",
            (ro) ? Py_False : Py_True);

        if (loc == NULL) return NULL;
        if (PyList_Append(ans, loc) != 0) return NULL;
        Py_DECREF(loc);
    }

    return ans;
} // }}}

// Device.get_filesystem {{{

static int
recursive_get_files(LIBMTP_mtpdevice_t *dev, uint32_t storage_id, uint32_t parent_id, PyObject *ans, PyObject *errs, PyObject *callback, unsigned int level) {
    LIBMTP_file_t *f, *files;
    PyObject *entry, *r;
    int ok = 1, recurse;

    Py_BEGIN_ALLOW_THREADS;
    files = LIBMTP_Get_Files_And_Folders(dev, storage_id, parent_id);
    Py_END_ALLOW_THREADS;

    if (files == NULL) return ok;

    for (f = files; ok && f != NULL; f = f->next) {
        entry = build_file_metadata(f, storage_id);
        if (entry == NULL) {
            ok = 0;
        } else {
            r = PyObject_CallFunction(callback, "OI", entry, level);
            recurse = (r != NULL && PyObject_IsTrue(r)) ? 1 : 0;
            Py_XDECREF(r);
            if (PyList_Append(ans, entry) != 0) { ok = 0; }
            Py_DECREF(entry);
        }

        if (ok && recurse && f->filetype == LIBMTP_FILETYPE_FOLDER) {
            if (!recursive_get_files(dev, storage_id, f->item_id, ans, errs, callback, level + 1)) { ok = 0; }
        }
    }

    // Release memory
    f = files;
    while (f != NULL) {
        files = f;
        f = f->next;
        LIBMTP_destroy_file_t(files);
    }

    return ok;
}

static PyObject *
Device_get_filesystem(Device *self, PyObject *args) {
    PyObject *ans, *errs, *callback;
    unsigned long storage_id;
    int ok = 0;

    ENSURE_DEV(NULL);
    ENSURE_STORAGE(NULL);

    if (!PyArg_ParseTuple(args, "kO", &storage_id, &callback)) return NULL;
    if (!PyCallable_Check(callback)) {
        PyErr_SetString(PyExc_TypeError, "callback is not a callable");
        return NULL;
    }
    ans = PyList_New(0);
    errs = PyList_New(0);
    if (errs == NULL || ans == NULL) {
        PyErr_NoMemory();
        return NULL;
    }

    LIBMTP_Clear_Errorstack(self->device);
    ok = recursive_get_files(self->device, (uint32_t)storage_id, LIBMTP_FILES_AND_FOLDERS_ROOT, ans, errs, callback, 0);
    dump_errorstack(self->device, errs);
    if (!ok) {
        Py_DECREF(ans);
        Py_DECREF(errs);
        return NULL;
    }

    return Py_BuildValue("NN", ans, errs);

} // }}}

// Device.get_file {{{
static PyObject *
get_file_impl(Device *self, PyObject *stream, PyObject *callback, unsigned long fileid) {
    int ret;
    PyObject *errs = PyList_New(0);
    if (errs == NULL) {
        PyErr_NoMemory();
        return NULL;
    }

    ProgressCallback cb = {0};
    cb.obj = callback;
    cb.extra = stream;
    Py_XINCREF(callback);
    Py_INCREF(stream);
    cb.state = PyEval_SaveThread();
    ret = LIBMTP_Get_File_To_Handler(self->device, (uint32_t)fileid, data_to_python, &cb, report_progress, &cb);
    PyEval_RestoreThread(cb.state);
    Py_XDECREF(callback);
    Py_DECREF(stream);

    if (ret != 0) { dump_errorstack(self->device, errs); }
    PyObject *pret = PyObject_CallMethod(stream, "flush", NULL);
    if (pret == NULL) PyErr_Clear();
    else Py_DECREF(pret);
    return Py_BuildValue("ON", (ret == 0) ? Py_True : Py_False, errs);
}

static PyObject *
Device_get_file(Device *self, PyObject *args) {
    PyObject *stream, *callback = NULL;
    unsigned long fileid;
    ENSURE_DEV(NULL);
    ENSURE_STORAGE(NULL);
    if (!PyArg_ParseTuple(args, "kO|O", &fileid, &stream, &callback)) return NULL;
    if (callback == NULL || !PyCallable_Check(callback)) callback = NULL;
    return get_file_impl(self, stream, callback, fileid);
}

static bool
find_in_parent(Device *self, unsigned long storage_id, unsigned long parent_id, PyObject *name, unsigned long *fileid) {
    LIBMTP_file_t *f, *files;
    *fileid = 0;
    bool found = false;
    Py_BEGIN_ALLOW_THREADS;
    files = LIBMTP_Get_Files_And_Folders(self->device, storage_id, parent_id);
    Py_END_ALLOW_THREADS;

    for (f = files; f != NULL; f = f->next) {
        if (!f->filename) continue;
        PyObject *k = PyUnicode_FromString(f->filename);
        if (!k) {
            PyErr_Clear();
            continue;
        }
        PyObject *l = PyObject_CallMethod(k, "lower", NULL);
        Py_DECREF(k);
        if (!l) break;
        bool matches = PyUnicode_Compare(l, name) == 0;
        Py_DECREF(l);
        if (matches) {
            *fileid = f->item_id;
            found = true;
            break;
        }
    }
    f = files;
    while (f != NULL) {
        files = f;
        f = f->next;
        LIBMTP_destroy_file_t(files);
    }
    return found;
}

static PyObject *
Device_get_file_by_name(Device *self, PyObject *args) {
    PyObject *stream, *callback = NULL, *names;
    unsigned long fileid = 0, storageid, parentid;
    ENSURE_DEV(NULL);
    ENSURE_STORAGE(NULL);
    if (!PyArg_ParseTuple(args, "kkO!O|O", &storageid, &parentid, &PyTuple_Type, &names, &stream, &callback)) return NULL;
    if (callback == NULL || !PyCallable_Check(callback)) callback = NULL;
    if (!PyTuple_GET_SIZE(names)) Py_RETURN_NONE;

    for (Py_ssize_t i = 0; i < PyTuple_GET_SIZE(names); i++) {
        PyObject *k = PyTuple_GET_ITEM(names, i);
        if (!PyUnicode_Check(k)) {
            PyErr_SetString(PyExc_TypeError, "names must contain only unicode strings");
            return NULL;
        }
        PyObject *l = PyObject_CallMethod(k, "lower", NULL);
        if (!l) return NULL;
        bool found = find_in_parent(self, storageid, parentid, l, &fileid);
        Py_DECREF(l);
        if (!found) {
            if (PyErr_Occurred()) return NULL;
            Py_RETURN_NONE;
        }
        parentid = fileid;
    }
    return get_file_impl(self, stream, callback, fileid);
}

// }}}

// Device.get_metadata_by_name {{{
static PyObject *
Device_get_metadata_by_name(Device *self, PyObject *args) {
    unsigned long parent_id, storage_id, folder_id = 0;
    PyObject *names;
    bool found = false;
    ENSURE_DEV(NULL);
    ENSURE_STORAGE(NULL);

    if (!PyArg_ParseTuple(args, "kkO!", &storage_id, &parent_id, &PyTuple_Type, &names)) return NULL;

    for (Py_ssize_t i = 0; i < PyTuple_GET_SIZE(names); i++) {
        PyObject *k = PyTuple_GET_ITEM(names, i);
        if (!PyUnicode_Check(k)) {
            PyErr_SetString(PyExc_TypeError, "names must contain only unicode strings");
            return NULL;
        }
        PyObject *l = PyObject_CallMethod(k, "lower", NULL);
        if (!l) return NULL;
        found = find_in_parent(self, storage_id, parent_id, l, &folder_id);
        Py_DECREF(l);
        if (!found) {
            if (PyErr_Occurred()) return NULL;
            Py_RETURN_NONE;
        }
        parent_id = folder_id;
    }
    if (!found) Py_RETURN_NONE;
    PyObject *errs = PyList_New(0);
    if (!errs) return NULL;
    PyObject *ans = file_metadata(self->device, errs, folder_id, storage_id);
    if (ans == NULL) {
        ans = Py_None;
        Py_INCREF(ans);
    }
    return Py_BuildValue("NN", ans, errs);
} // }}}

// Device.list_folder_by_name {{{
static PyObject *
Device_list_folder_by_name(Device *self, PyObject *args) {
    unsigned long parent_id, storage_id, folder_id = 0;
    PyObject *names;
    bool found = false;
    ENSURE_DEV(NULL);
    ENSURE_STORAGE(NULL);

    if (!PyArg_ParseTuple(args, "kkO!", &storage_id, &parent_id, &PyTuple_Type, &names)) return NULL;

    for (Py_ssize_t i = 0; i < PyTuple_GET_SIZE(names); i++) {
        PyObject *k = PyTuple_GET_ITEM(names, i);
        if (!PyUnicode_Check(k)) {
            PyErr_SetString(PyExc_TypeError, "names must contain only unicode strings");
            return NULL;
        }
        PyObject *l = PyObject_CallMethod(k, "lower", NULL);
        if (!l) return NULL;
        found = find_in_parent(self, storage_id, parent_id, l, &folder_id);
        Py_DECREF(l);
        if (!found) {
            if (PyErr_Occurred()) return NULL;
            Py_RETURN_NONE;
        }
        parent_id = folder_id;
    }
    if (!found) Py_RETURN_NONE;
    LIBMTP_file_t *f, *files;
    PyObject *entry;
    Py_BEGIN_ALLOW_THREADS;
    files = LIBMTP_Get_Files_And_Folders(self->device, storage_id, folder_id);
    Py_END_ALLOW_THREADS;
    if (files == NULL) Py_RETURN_NONE;
    PyObject *ans = PyList_New(0);
    if (!ans) return NULL;
    for (f = files; f != NULL; f = f->next) {
        entry = build_file_metadata(f, storage_id);
        if (entry == NULL) {
            Py_CLEAR(ans);
            break;
        }
        bool appended = PyList_Append(ans, entry) == 0;
        Py_DECREF(entry);
        if (!appended) {
            Py_CLEAR(ans);
            break;
        }
    }
    f = files;
    while (f != NULL) {
        files = f;
        f = f->next;
        LIBMTP_destroy_file_t(files);
    }
    return ans;
} // }}}

// Device.put_file {{{
static PyObject *
Device_put_file(Device *self, PyObject *args) {
    PyObject *stream, *callback = NULL, *errs, *fo = NULL;
    ProgressCallback cb;
    unsigned long parent_id, storage_id;
    unsigned long long filesize;
    int ret;
    char *name;
    LIBMTP_file_t f;

    ENSURE_DEV(NULL);
    ENSURE_STORAGE(NULL);

    if (!PyArg_ParseTuple(args, "kksOK|O", &storage_id, &parent_id, &name, &stream, &filesize, &callback)) return NULL;
    errs = PyList_New(0);
    if (errs == NULL) {
        PyErr_NoMemory();
        return NULL;
    }
    if (callback == NULL || !PyCallable_Check(callback)) callback = NULL;

    cb.obj = callback;
    cb.extra = stream;
    f.parent_id = (uint32_t)parent_id;
    f.storage_id = (uint32_t)storage_id;
    f.item_id = 0;
    f.filename = name;
    f.filetype = LIBMTP_FILETYPE_UNKNOWN;
    f.filesize = (uint64_t)filesize;
    Py_XINCREF(callback);
    Py_INCREF(stream);
    cb.state = PyEval_SaveThread();
    ret = LIBMTP_Send_File_From_Handler(self->device, data_from_python, &cb, &f, report_progress, &cb);
    PyEval_RestoreThread(cb.state);
    Py_XDECREF(callback);
    Py_DECREF(stream);

    if (ret != 0) dump_errorstack(self->device, errs);
    else fo = file_metadata(self->device, errs, f.item_id, storage_id);
    if (fo == NULL) {
        fo = Py_None;
        Py_INCREF(fo);
    }

    return Py_BuildValue("NN", fo, errs);

} // }}}

// Device.delete_object {{{
static PyObject *
Device_delete_object(Device *self, PyObject *args) {
    PyObject *errs;
    unsigned long id;
    int res;

    ENSURE_DEV(NULL);
    ENSURE_STORAGE(NULL);

    if (!PyArg_ParseTuple(args, "k", &id)) return NULL;
    errs = PyList_New(0);
    if (errs == NULL) {
        PyErr_NoMemory();
        return NULL;
    }

    Py_BEGIN_ALLOW_THREADS;
    res = LIBMTP_Delete_Object(self->device, (uint32_t)id);
    Py_END_ALLOW_THREADS;
    if (res != 0) dump_errorstack(self->device, errs);

    return Py_BuildValue("ON", (res == 0) ? Py_True : Py_False, errs);
} // }}}

// Device.create_folder {{{
static PyObject *
Device_create_folder(Device *self, PyObject *args) {
    PyObject *errs, *fo = NULL;
    unsigned long storage_id, parent_id;
    uint32_t folder_id;
    char *name;

    ENSURE_DEV(NULL);
    ENSURE_STORAGE(NULL);

    if (!PyArg_ParseTuple(args, "kks", &storage_id, &parent_id, &name)) return NULL;
    errs = PyList_New(0);
    if (errs == NULL) {
        PyErr_NoMemory();
        return NULL;
    }

    Py_BEGIN_ALLOW_THREADS;
    folder_id = LIBMTP_Create_Folder(self->device, name, (uint32_t)parent_id, (uint32_t)storage_id);
    Py_END_ALLOW_THREADS;

    if (folder_id == 0) dump_errorstack(self->device, errs);
    else fo = file_metadata(self->device, errs, folder_id, storage_id);
    if (fo == NULL) {
        fo = Py_None;
        Py_INCREF(fo);
    }

    return Py_BuildValue("NN", fo, errs);
} // }}}

static PyMethodDef Device_methods[] = {
    {"update_storage_info",
     (PyCFunction)Device_update_storage_info,
     METH_VARARGS,
     "update_storage_info() -> Reread the storage info from the device (total, space, free space, storage locations, etc.)"},

    {"get_filesystem",
     (PyCFunction)Device_get_filesystem,
     METH_VARARGS,
     "get_filesystem(storage_id, callback) -> Get the list of files and folders on the device in storage_id. Returns files, errors. callback must be a "
     "callable that is called as with (entry, level). It is called with every found object. If callback returns False and the object is a folder, it is not "
     "recursed into."},

    {"get_file",
     (PyCFunction)Device_get_file,
     METH_VARARGS,
     "get_file(fileid, stream, callback=None) -> Get the file specified by fileid from the device. stream must be a file-like object. The file will be written "
     "to it. callback works the same as in get_filelist(). Returns ok, errs, where errs is a list of errors (if any)."},

    {"get_file_by_name",
     (PyCFunction)Device_get_file_by_name,
     METH_VARARGS,
     "get_file_by_name(storage_id, parent_id, names, stream, callback=None) -> Get the file specified by names (a tuple of name components) relative to "
     "parent_id from the device. stream must be a file-like object. The file will be written to it. callback works the same as in get_filelist(). Returns None "
     "or (ok, errs), where errs is a list of errors (if any)."},

    {"list_folder_by_name",
     (PyCFunction)Device_list_folder_by_name,
     METH_VARARGS,
     "list_folder_by_name(storage_id, parent_id, names) -> List the folder specified by names (a tuple of name components) relative to parent_id from the "
     "device. Return None or a list of entries."},

    {"get_metadata_by_name",
     (PyCFunction)Device_get_metadata_by_name,
     METH_VARARGS,
     "get_metadata_by_name(storage_id, parent_id, names) -> Return metadata for specified name (a tuple of name components) relative to parent from the "
     "device. Return (metadata, errs)."},

    {"put_file",
     (PyCFunction)Device_put_file,
     METH_VARARGS,
     "put_file(storage_id, parent_id, filename, stream, size, callback=None) -> Put a file on the device. The file is read from stream. It is put inside the "
     "folder identified by parent_id on the storage identified by storage_id. Use parent_id=0 to put it in the root. stream must be a file-like object. size "
     "is the size in bytes of the data in stream. callback works the same as in get_filelist(). Returns fileinfo, errs, where errs is a list of errors (if "
     "any), and fileinfo is a file information dictionary, as returned by get_filelist(). fileinfo will be None if case or errors."},

    {"create_folder",
     (PyCFunction)Device_create_folder,
     METH_VARARGS,
     "create_folder(storage_id, parent_id, name) -> Create a folder named name under parent parent_id (use 0 for root) in the storage identified by "
     "storage_id. Returns folderinfo, errors, where folderinfo is the same dict as returned by get_folderlist(), it will be None if there are errors."},

    {"delete_object",
     (PyCFunction)Device_delete_object,
     METH_VARARGS,
     "delete_object(id) -> Delete the object identified by id from the device. Can be used to delete files, folders, etc. Returns ok, errs."},


    {NULL} /* Sentinel */
};

static PyGetSetDef Device_getsetters[] = {
    {(char *)"friendly_name", (getter)Device_friendly_name, NULL, (char *)"The friendly name of this device, can be None.", NULL},

    {(char *)"manufacturer_name", (getter)Device_manufacturer_name, NULL, (char *)"The manufacturer name of this device, can be None.", NULL},

    {(char *)"model_name", (getter)Device_model_name, NULL, (char *)"The model name of this device, can be None.", NULL},

    {(char *)"serial_number", (getter)Device_serial_number, NULL, (char *)"The serial number of this device, can be None.", NULL},

    {(char *)"device_version", (getter)Device_device_version, NULL, (char *)"The device version of this device, can be None.", NULL},

    {(char *)"ids", (getter)Device_ids, NULL, (char *)"The ids of the device (busnum, devnum, vendor_id, product_id, usb_serialnum)", NULL},

    {(char *)"storage_info",
     (getter)Device_storage_info,
     NULL,
     (char *)"Information about the storage locations on the device. Returns a list of dictionaries where each dictionary corresponds to the "
             "LIBMTP_devicestorage_struct.",
     NULL},

    {NULL} /* Sentinel */
};

static PyTypeObject DeviceType = {
    // {{{
    PyVarObject_HEAD_INIT(NULL, 0)
    /* tp_name             */ "libmtp.Device",
    /* tp_basicsize        */ sizeof(Device),
    /* tp_itemsize         */ 0,
    /* tp_dealloc          */ (destructor)Device_dealloc,
    /* tp_print            */ 0,
    /* tp_getattr          */ 0,
    /* tp_setattr          */ 0,
    /* tp_compare          */ 0,
    /* tp_repr             */ 0,
    /* tp_as_number        */ 0,
    /* tp_as_sequence      */ 0,
    /* tp_as_mapping       */ 0,
    /* tp_hash             */ 0,
    /* tp_call             */ 0,
    /* tp_str              */ 0,
    /* tp_getattro         */ 0,
    /* tp_setattro         */ 0,
    /* tp_as_buffer        */ 0,
    /* tp_flags            */ Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
    /* tp_doc              */ "Device",
    /* tp_traverse         */ 0,
    /* tp_clear            */ 0,
    /* tp_richcompare      */ 0,
    /* tp_weaklistoffset   */ 0,
    /* tp_iter             */ 0,
    /* tp_iternext         */ 0,
    /* tp_methods          */ Device_methods,
    /* tp_members          */ 0,
    /* tp_getset           */ Device_getsetters,
    /* tp_base             */ 0,
    /* tp_dict             */ 0,
    /* tp_descr_get        */ 0,
    /* tp_descr_set        */ 0,
    /* tp_dictoffset       */ 0,
    /* tp_init             */ (initproc)Device_init,
    /* tp_alloc            */ 0,
    /* tp_new              */ 0,
}; // }}}

// }}} End Device object definition

static PyObject *
set_debug_level(PyObject *self, PyObject *args) {
    int level;
    if (!PyArg_ParseTuple(args, "i", &level)) return NULL;
    LIBMTP_Set_Debug(level);
    Py_RETURN_NONE;
}


static PyObject *
is_mtp_device(PyObject *self, PyObject *args) {
    int busnum, devnum, ans = 0;

    if (!PyArg_ParseTuple(args, "ii", &busnum, &devnum)) return NULL;

    Py_BEGIN_ALLOW_THREADS;
    ans = LIBMTP_Check_Specific_Device(busnum, devnum);
    Py_END_ALLOW_THREADS;

    if (ans) Py_RETURN_TRUE;

    Py_RETURN_FALSE;
}

static PyObject *
known_devices(PyObject *self, PyObject *args) {
    PyObject *ans, *d;
    size_t i;

    ans = PyList_New(0);
    if (ans == NULL) return PyErr_NoMemory();

    for (i = 0;; i++) {
        if (calibre_mtp_device_table[i].vendor == NULL && calibre_mtp_device_table[i].product == NULL && calibre_mtp_device_table[i].vendor_id == 0xffff) break;
        d = Py_BuildValue("(HH)", (unsigned short)calibre_mtp_device_table[i].vendor_id, (unsigned short)calibre_mtp_device_table[i].product_id);
        if (d == NULL) {
            Py_DECREF(ans);
            ans = NULL;
            break;
        }
        if (PyList_Append(ans, d) != 0) {
            Py_DECREF(d);
            Py_DECREF(ans);
            ans = NULL;
            PyErr_NoMemory();
            break;
        }
        Py_DECREF(d);
    }

    return ans;
}

static char libmtp_doc[] = "Interface to libmtp.";

static PyMethodDef libmtp_methods[] = {
    {"set_debug_level", set_debug_level, METH_VARARGS, "set_debug_level(level)\n\nSet the debug level bit mask, see LIBMTP_DEBUG_* constants."},

    {"is_mtp_device",
     is_mtp_device,
     METH_VARARGS,
     "is_mtp_device(busnum, devnum)\n\nA probe is done and True returned if the probe succeeds. Note that probing can cause some devices to malfunction, and "
     "it is not very reliable, which is why we prefer to use the device database."},

    {"known_devices", known_devices, METH_VARARGS, "known_devices() -> Return the list of known (vendor_id, product_id) combinations."},

    {NULL, NULL, 0, NULL}};
static int
exec_module(PyObject *m) {
    DeviceType.tp_new = PyType_GenericNew;
    if (PyType_Ready(&DeviceType) < 0) return -1;

    MTPError = PyErr_NewException("libmtp.MTPError", NULL, NULL);
    if (MTPError == NULL) return -1;
    PyModule_AddObject(m, "MTPError", MTPError);

    // Redirect stdout to get rid of the annoying message about mtpz. Really,
    // who designs a library without anyway to control/redirect the debugging
    // output, and hardcoded paths that cannot be changed? Compiling libmtp without the crypt use flag disables mtpz support in libmtp
    /* int bak, new; */
    /* fprintf(stdout, "\n"); // This is needed, without it, for some odd reason the code below causes stdout to buffer all output after it is restored, rather
     * than using line buffering, and setlinebuf does not work. */
    /* fflush(stdout); */
    /* bak = dup(STDOUT_FILENO); */
    /* new = open("/dev/null", O_WRONLY); */
    /* dup2(new, STDOUT_FILENO); */
    /* close(new); */
    LIBMTP_Init();
    /* fflush(stdout); */
    /* dup2(bak, STDOUT_FILENO); */
    /* close(bak); */

    LIBMTP_Set_Debug(LIBMTP_DEBUG_NONE);

    Py_INCREF(&DeviceType);
    PyModule_AddObject(m, "Device", (PyObject *)&DeviceType);

    PyModule_AddStringMacro(m, LIBMTP_VERSION_STRING);
    PyModule_AddIntMacro(m, LIBMTP_DEBUG_NONE);
    PyModule_AddIntMacro(m, LIBMTP_DEBUG_PTP);
    PyModule_AddIntMacro(m, LIBMTP_DEBUG_PLST);
    PyModule_AddIntMacro(m, LIBMTP_DEBUG_USB);
    PyModule_AddIntMacro(m, LIBMTP_DEBUG_DATA);
    PyModule_AddIntMacro(m, LIBMTP_DEBUG_ALL);
    PyModule_AddIntMacro(m, LIBMTP_FILES_AND_FOLDERS_ROOT);

    return 0;
}

static PyModuleDef_Slot slots[] = {{Py_mod_exec, exec_module}, {0, NULL}};

static struct PyModuleDef module_def = {
    .m_base = PyModuleDef_HEAD_INIT,
    .m_name = "libmtp",
    .m_doc = libmtp_doc,
    .m_methods = libmtp_methods,
    .m_slots = slots,
};

CALIBRE_MODINIT_FUNC
PyInit_libmtp(void) { return PyModuleDef_Init(&module_def); }
