#include <gio/gio.h>
#include <glib.h>
#include <gcrypt.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <fcntl.h>
#include <glob.h>
#include <errno.h>

#include "identifier.h"

static char* ofono_name = "org.ofono";
static char* cache_file = WHOOPSIE_ID_PATH;
static gint fail_next_get_mac = 0;
static gint fail_next_get_uuid = 0;
static gint fail_next_get_serial = 0;

void whoopsie_identifier_set_ofono_name (char *name) {
    ofono_name = name;
}

void whoopsie_identifier_set_cache_file (char *filename) {
    cache_file = filename;
}

void
whoopsie_hex_to_char (char* buf, const char *str, int len)
{
    char* p = NULL;
    int i = 0;

    g_return_if_fail (buf);
    g_return_if_fail (str);

    p = buf;
    for (i = 0; i < len; i++) {
        snprintf(p, 3, "%02x", (unsigned char) str[i]);
        p += 2;
    }
    buf[2*len] = 0;
}

static int
load_file (char *dir, char *file, char *datav, size_t datac) {
    char name[200] = {0};

    if (sprintf(name, "%s%s", dir, file) < 0) {
        return -1;
    }

    int fd = open(name, O_RDONLY);
    if (fd < 0) {
        return -1;
    }

    int n = read(fd, datav, datac);
    if (n < 1) {
        return -1;
    }

    if (close(fd) < 0) {
        return -1;
    }

    // prune out the ending \n if present
    datav[datav[n-1] == '\n' ? n-1 : n] = 0;

    return 0;
}

void
whoopsie_identifier_get_cached (char** res, GError** error)
{
    g_return_if_fail (res);

    gsize length = 0;

    if (!g_file_get_contents (cache_file, res, &length, NULL)) {
        g_set_error (error, g_quark_from_static_string ("whoopsie-quark"), 0,
                     "Could not load cached identifier file.");
    } else if (length != HASHLEN) {
        g_set_error (error, g_quark_from_static_string ("whoopsie-quark"), 0,
                     "Got an unexpected length reading cached id.");
        g_free (*res);
        *res = NULL;
    }
    return;
}

void
whoopsie_identifier_get_mac_address (char** res, GError** error)
{
    glob_t globbuf;
    char data[200] = {0};
    gboolean success = FALSE;

    g_return_if_fail (res);

    if (g_atomic_int_compare_and_exchange (&fail_next_get_mac, WHOOPSIE_FAIL_GENERIC, 0)) {
        g_set_error (error, g_quark_from_static_string ("whoopsie-quark"), WHOOPSIE_FAILED_BY_REQUEST,
                     "Failed by tester request");
        return;
    }

    if (glob ("/sys/class/net/*/", 0, NULL, &globbuf)) {
        g_set_error (error, g_quark_from_static_string ("whoopsie-quark"), 0,
                     "globbing interfaces: %s", strerror(errno));
        return;
    }

    if (!g_atomic_int_compare_and_exchange (&fail_next_get_mac, WHOOPSIE_FAIL_NO_IFACES, 0)) {
        for (int i=0; i < globbuf.gl_pathc; i++) {
            if (load_file(globbuf.gl_pathv[i], "type", data, 200) < 0) {
                g_set_error (error, g_quark_from_static_string ("whoopsie-quark"), 0,
                             "loading interface type: %s", strerror(errno));
                return;
            }
            if (!strcmp(data, "1")) {
                if (load_file(globbuf.gl_pathv[i], "address", data, 200) < 0) {
                    g_set_error (error, g_quark_from_static_string ("whoopsie-quark"), 0,
                                 "loading interface address: %s", strerror(errno));
                    return;
                }
                success = TRUE;
                break;
            }
        }
    }

    if (!success) {
        g_set_error (error, g_quark_from_static_string ("whoopsie-quark"), 0,
                     "Unable to find a hardware address");
        return;
    }

    *res = g_strdup (data);
}

static void
_get_serial (char** res, GError** error, const char* filename, int length, gint* fail_next_get)
{
    int fp;

    g_return_if_fail (res);

    if (g_atomic_int_compare_and_exchange (fail_next_get, WHOOPSIE_FAIL_GENERIC, 0)) {
        g_set_error (error, g_quark_from_static_string ("whoopsie-quark"), WHOOPSIE_FAILED_BY_REQUEST,
                     "Failed by tester request");
        return;
    }

    fp = open (filename, O_RDONLY);
    if (fp < 0) {
        g_set_error (error, g_quark_from_static_string ("whoopsie-quark"), 0,
                     "Could not open the product uuid/serial file.");
        return;
    }
    *res = g_malloc (length+1);
    if (read (fp, *res, length) == length)
        (*res)[length] = '\0';
    else
        g_set_error (error, g_quark_from_static_string ("whoopsie-quark"), 0,
                     "Got an unexpected length reading the product uuid/serial.");
    close (fp);
}

void
whoopsie_identifier_get_system_uuid (char** res, GError** error)
{
	_get_serial (res, error, "/sys/class/dmi/id/product_uuid", 36, &fail_next_get_uuid);
}

void
whoopsie_identifier_get_android_serialno (char** res, GError** error)
{
	_get_serial (res, error, "/sys/class/android_usb/android0/iSerial", 16, &fail_next_get_serial);
}

void
whoopsie_identifier_sha512 (char* source, char* res, GError** error)
{
    int md_len;
    gcry_md_hd_t sha512 = NULL;
    unsigned char* id = NULL;

    g_return_if_fail (source);
    g_return_if_fail (res);

    if (!gcry_check_version (GCRYPT_VERSION)) {
        g_set_error (error, g_quark_from_static_string ("whoopsie-quark"), 0,
                     "libcrypt version mismatch.");
        return;
    }
    gcry_control (GCRYCTL_INITIALIZATION_FINISHED, 0);
    md_len = gcry_md_get_algo_dlen(GCRY_MD_SHA512);
    if (md_len != HASHLEN / 2) {
        g_set_error (error, g_quark_from_static_string ("whoopsie-quark"), 0,
                 "Received an incorrect size for the SHA512 message digest");
        return;
    }
    if (gcry_md_open (&sha512, GCRY_MD_SHA512, 0) || sha512 == NULL) {
        g_set_error (error, g_quark_from_static_string ("whoopsie-quark"), 0,
             "Failed to create a SHA512 message digest for the product_uuid.");
        return;
    }
    gcry_md_write (sha512, source, strlen (source));
    gcry_md_final (sha512);
    id = gcry_md_read (sha512, GCRY_MD_SHA512);
    if (id == NULL) {
        g_set_error (error, g_quark_from_static_string ("whoopsie-quark"), 0,
             "Failed to read the SHA512 message digest for the product_uuid.");
        gcry_md_close (sha512);
        return;
    }
    whoopsie_hex_to_char (res, (const char*)id, md_len);
    gcry_md_close (sha512);
}

void whoopsie_identifier_append_imei (char** result, GError** error)
{
    GVariant* res;
    GError* err = NULL;

    GDBusConnection* con = g_bus_get_sync (G_BUS_TYPE_SYSTEM, NULL, &err);

    if (err != NULL) {
        g_set_error (error, g_quark_from_static_string ("whoopsie-quark"), 0,
                     "Unable to connect to the bus: %s", err->message);
        return;
    }

    res = g_dbus_connection_call_sync (con, ofono_name, "/",
                                       "org.ofono.Manager", "GetModems",
                                       NULL, G_VARIANT_TYPE ("(a(oa{sv}))"),
                                       G_DBUS_CALL_FLAGS_NONE,
                                       -1, NULL, &err);

    if (err != NULL) {
        g_set_error (error, g_quark_from_static_string ("whoopsie-quark"), 0,
                     "Unable to call ofono's GetModems: %s", err->message);
        return;
    }
    g_object_unref (con);

    /* res is an a(oa{sv}) */
    GVariant* var1 = g_variant_get_child_value (res, 0);
    g_variant_unref (res);
    /* but the first array can be empty */
    if (g_variant_n_children (var1) < 1) {
        g_set_error (error, g_quark_from_static_string ("whoopsie-quark"), 0,
                     "No modems in GetModems response");
        return;
    }
    GVariant* var2 = g_variant_get_child_value (var1, 0);
    g_variant_unref (var1);
    GVariant* var3 = g_variant_get_child_value (var2, 1);
    g_variant_unref (var2);
    GVariant* var4 = g_variant_lookup_value (var3, "Serial", G_VARIANT_TYPE ("s"));
    g_variant_unref (var3);
    if (var4 == NULL) {
        g_set_error (error, g_quark_from_static_string ("whoopsie-quark"), 0,
                     "No Serial key in GetModems response");
        return;
    }
    gsize imei_len;
    const gchar* imei = g_variant_get_string(var4, &imei_len);
    if (imei_len < 15 || imei_len > 16) {
        g_set_error (error, g_quark_from_static_string ("whoopsie-quark"), 0,
                     "GetModems' Serial key is not an IMEI (has length %zu)", imei_len);
        return;
    }

    size_t old_len = *result ? strlen(*result) : 0;

    *result = g_realloc (*result, old_len + imei_len + 1);
    strcpy (*result + old_len, imei);

    g_variant_unref(var4);
}

void
whoopsie_identifier_generate (char** res, GError** error)
{
    char* identifier = NULL;

    g_return_if_fail (res);

    whoopsie_identifier_get_cached (res, error);
    if ((!error || !(*error)) && *res)
        return;

    if (error && *error) {
        g_error_free (*error);
        *error = NULL;
    }

    whoopsie_identifier_get_system_uuid (&identifier, error);
    if ((!error || !(*error)) && identifier)
        goto out;

    if (error && *error) {
        g_error_free (*error);
        *error = NULL;
    }

    whoopsie_identifier_get_android_serialno (&identifier, error);
    if ((!error || !(*error)) && identifier)
        goto out;

    if (error && *error) {
        g_error_free (*error);
        *error = NULL;
    }

    whoopsie_identifier_get_mac_address (&identifier, error);
    if ((!error || !(*error)) && identifier) {
        whoopsie_identifier_append_imei (&identifier, error);
        if (error && *error) {
            g_error_free (*error);
            *error = NULL;
        }

        goto out;
    }

    return;

out:
    *res = g_malloc (HASHLEN + 1);
    whoopsie_identifier_sha512 (identifier, *res, error);
    g_free (identifier);
}

void whoopsie_identifier_fail_next_get_mac (gint how)
{
    g_atomic_int_set (&fail_next_get_mac, how);
}

void whoopsie_identifier_fail_next_get_uuid ()
{
    g_atomic_int_set (&fail_next_get_uuid, 1);
}

void whoopsie_identifier_fail_next_get_serial ()
{
    g_atomic_int_set (&fail_next_get_serial, 1);
}
