#include "gbinder_hidl.h"

#include <euicc/euicc.h>
#include <euicc/hexutil.h>
#include <euicc/interface.h>
#include <lpac/utils.h>

#include <gbinder.h>
#include <stdio.h>
#include <stdlib.h>

#define ENV_DEBUG APDU_ENV_NAME(GBINDER, DEBUG)

#define HIDL_SERVICE_DEVICE "/dev/hwbinder"
#define HIDL_SERVICE_IFACE "android.hardware.radio@1.0::IRadio"
#define HIDL_SERVICE_IFACE_CALLBACK "android.hardware.radio@1.0::IRadioResponse"

// ref: IRadio
#define HIDL_SERVICE_SET_RESPONSE_FUNCTIONS GBINDER_FIRST_CALL_TRANSACTION
#define HIDL_SERVICE_ICC_OPEN_LOGICAL_CHANNEL (GBINDER_FIRST_CALL_TRANSACTION + 105)
#define HIDL_SERVICE_ICC_CLOSE_LOGICAL_CHANNEL (GBINDER_FIRST_CALL_TRANSACTION + 106)
#define HIDL_SERVICE_ICC_TRANSMIT_APDU_LOGICAL_CHANNEL (GBINDER_FIRST_CALL_TRANSACTION + 107)

// ref: IRadioResponse
#define HIDL_SERVICE_ICC_OPEN_LOGICAL_CHANNEL_CALLBACK (GBINDER_FIRST_CALL_TRANSACTION + 104)
#define HIDL_SERVICE_ICC_CLOSE_LOGICAL_CHANNEL_CALLBACK (GBINDER_FIRST_CALL_TRANSACTION + 105)
#define HIDL_SERVICE_ICC_TRANSMIT_APDU_LOGICAL_CHANNEL_CALLBACK (GBINDER_FIRST_CALL_TRANSACTION + 106)

static int lastChannelId = -1;

struct radio_response_info {
    int32_t type;
    int32_t serial;
    int32_t error;
};

struct icc_io_result {
    int32_t sw1;
    int32_t sw2;
    GBinderHidlString simResponse;
};

struct sim_apdu {
    int32_t sessionId;
    int32_t cla;
    int32_t instruction;
    int32_t p1;
    int32_t p2;
    int32_t p3;
    GBinderHidlString data;
};

static const GBinderWriterField sim_apdu_f[] = {GBINDER_WRITER_FIELD_HIDL_STRING(struct sim_apdu, data),
                                                GBINDER_WRITER_FIELD_END()};

static const GBinderWriterType sim_apdu_t = {GBINDER_WRITER_STRUCT_NAME_AND_SIZE(struct sim_apdu), sim_apdu_f};

static GBinderServiceManager *sm;
// IRadioResponse
static GBinderLocalObject *response_callback;
// IRadio
static GBinderRemoteObject *remote;
static GBinderClient *client;

static GMainLoop *binder_loop;

static int lastIntResp = -1;
static int lastRadioErr = 0;
static struct icc_io_result lastIccIoResult = {0};

static GBinderLocalReply *radio_response_transact(GBinderLocalObject *obj, GBinderRemoteRequest *req, guint code,
                                                  guint flags, int *status, void *user_data) {
    GBinderReader reader;

    gbinder_remote_request_init_reader(req, &reader);
    const struct radio_response_info *resp = gbinder_reader_read_hidl_struct(&reader, struct radio_response_info);
    lastRadioErr = resp->error;

    if (lastRadioErr != 0)
        goto out;

    switch (code) {
    case HIDL_SERVICE_ICC_OPEN_LOGICAL_CHANNEL_CALLBACK:
        gbinder_reader_read_int32(&reader, &lastIntResp);
        break;
    case HIDL_SERVICE_ICC_TRANSMIT_APDU_LOGICAL_CHANNEL_CALLBACK: {
        const struct icc_io_result *icc_io_res = gbinder_reader_read_hidl_struct(&reader, struct icc_io_result);
        // We cannot rely on the *req pointer being valid after we return
        lastIccIoResult.sw1 = icc_io_res->sw1;
        lastIccIoResult.sw2 = icc_io_res->sw2;
        lastIccIoResult.simResponse.data.str = strndup(icc_io_res->simResponse.data.str, icc_io_res->simResponse.len);
        lastIccIoResult.simResponse.len = icc_io_res->simResponse.len;
        lastIccIoResult.simResponse.owns_buffer = TRUE;
        break;
    }
    }

out:
    g_main_loop_quit(binder_loop);
    return NULL;
}

static void cleanup_channel(int id) {
    GBinderLocalRequest *req = gbinder_client_new_request(client);
    GBinderWriter writer;
    gbinder_local_request_init_writer(req, &writer);
    gbinder_writer_append_int32(&writer, 1000);
    gbinder_writer_append_int32(&writer, id);
    gbinder_client_transact_sync_oneway(client, HIDL_SERVICE_ICC_CLOSE_LOGICAL_CHANNEL, req);
    gbinder_local_request_unref(req);
    g_main_loop_run(binder_loop);
}

static void cleanup(void) {
    if (lastChannelId != -1) {
        fprintf(stderr, "Cleaning up leaked APDU channel %d\n", lastChannelId);
        cleanup_channel(lastChannelId);
        lastChannelId = -1;
    }
}

static void sighandler(int sig) {
    // This would trigger atexit() hooks
    exit(0);
}

static int try_open_slot(int slotId, const uint8_t *aid, uint32_t aid_len) {
    // First, try to connect to the HIDL service for this slot
    char fqname[255];
    snprintf(fqname, 255, "%s/slot%d", HIDL_SERVICE_IFACE, slotId);
    fprintf(stderr, "Attempting to connect to %s\n", fqname);

    int status = 0;
    sm = gbinder_servicemanager_new(HIDL_SERVICE_DEVICE);
    remote = gbinder_remote_object_ref(gbinder_servicemanager_get_service_sync(sm, fqname, &status));
    client = gbinder_client_new(remote, HIDL_SERVICE_IFACE);

    if (!client) {
        fprintf(stderr, "Failed to connect to IRadio\n");
        gbinder_client_unref(client);
        gbinder_remote_object_unref(remote);
        gbinder_servicemanager_unref(sm);
        return -1;
    }

    response_callback =
        gbinder_servicemanager_new_local_object(sm, HIDL_SERVICE_IFACE_CALLBACK, radio_response_transact, NULL);

    GBinderLocalRequest *req = gbinder_client_new_request(client);
    GBinderWriter writer;
    gbinder_local_request_init_writer(req, &writer);
    gbinder_writer_append_local_object(&writer, response_callback);
    gbinder_writer_append_local_object(&writer, NULL);
    gbinder_client_transact_sync_reply(client, HIDL_SERVICE_SET_RESPONSE_FUNCTIONS, req, &status);
    gbinder_local_request_unref(req);

    if (status < 0) {
        fprintf(stderr, "Failed to call IRadio::setResponseFunctions");
        return -1;
    }

    // Now, try to open the AID
    uint8_t aid_hex[255];
    euicc_hexutil_bin2hex(aid_hex, 255, aid, aid_len);

    req = gbinder_client_new_request(client);
    gbinder_local_request_init_writer(req, &writer);
    gbinder_writer_append_int32(&writer, 1000);
    gbinder_writer_append_hidl_string_copy(&writer, aid_hex);
    gbinder_writer_append_int32(&writer, 0);
    status = gbinder_client_transact_sync_oneway(client, HIDL_SERVICE_ICC_OPEN_LOGICAL_CHANNEL, req);
    gbinder_local_request_unref(req);

    if (status < 0) {
        fprintf(stderr, "Failed to call IRadio::iccOpenLogicalChannel: %d\n", status);
        return status;
    }

    g_main_loop_run(binder_loop);

    if (lastRadioErr != 0) {
        fprintf(stderr, "Failed to open APDU logical channel: %d\n", lastRadioErr);
        return -lastRadioErr;
    }
    fprintf(stderr, "opened logical channel id: %d\n", lastIntResp);

    return lastIntResp;
}

static int apdu_interface_connect(struct euicc_ctx *ctx) { return 0; }

static void apdu_interface_disconnect(struct euicc_ctx *ctx) { cleanup(); }

static int apdu_interface_logic_channel_open(struct euicc_ctx *ctx, const uint8_t *aid, uint8_t aid_len) {
    // We only start to use gbinder connection here, because only now can we detect whether
    // a given slot is a valid eSIM slot. This way we can automatically fall back in the case
    // where a device has only one eSIM -- we don't want to force the user to choose in this case.
    int res = try_open_slot(1, aid, aid_len);
    if (res < 0)
        res = try_open_slot(2, aid, aid_len);
    if (res >= 0)
        lastChannelId = res;
    return res;
}

static void apdu_interface_logic_channel_close(struct euicc_ctx *ctx, uint8_t channel) {
    cleanup_channel(channel);
    if (lastChannelId == channel)
        lastChannelId = -1;

    // Only do this cleanup here, because on exit these objects will be destroyed anyway
    gbinder_client_unref(client);
    gbinder_remote_object_unref(remote);
    gbinder_servicemanager_unref(sm);
}

static int apdu_interface_transmit(struct euicc_ctx *ctx, uint8_t **rx, uint32_t *rx_len, const uint8_t *tx,
                                   uint32_t tx_len) {
    GBinderLocalRequest *req = gbinder_client_new_request(client);
    GBinderWriter writer;
    gbinder_local_request_init_writer(req, &writer);
    gbinder_writer_append_int32(&writer, 1000);

    uint8_t tx_hex[4096] = {0};
    euicc_hexutil_bin2hex(tx_hex, 4096, &tx[5], tx_len - 5);

    if (getenv_or_default(ENV_DEBUG, false))
        fprintf(stderr, "APDU req: %s\n", tx_hex);

    struct sim_apdu apdu = {
        .sessionId = lastChannelId,
        .cla = tx[0],
        .instruction = tx[1],
        .p1 = tx[2],
        .p2 = tx[3],
        .p3 = tx[4],
        .data =
            {
                .data = {.str = (const char *)tx_hex},
                .len = strlen(tx_hex) + 1,
                .owns_buffer = FALSE,
            },
    };
    gbinder_writer_append_struct(&writer, &apdu, &sim_apdu_t, NULL);
    int status = gbinder_client_transact_sync_oneway(client, HIDL_SERVICE_ICC_TRANSMIT_APDU_LOGICAL_CHANNEL, req);
    gbinder_local_request_unref(req);

    if (status < 0) {
        fprintf(stderr, "Failed to call IRadio::iccTransmitApduLogicalChannel: %d\n", status);
        return status;
    }

    g_main_loop_run(binder_loop);

    if (lastRadioErr != 0) {
        return -lastRadioErr;
    }

    if (getenv_or_default(ENV_DEBUG, false))
        fprintf(stderr, "APDU resp: %d%d %d %s\n", lastIccIoResult.sw1, lastIccIoResult.sw2,
                lastIccIoResult.simResponse.len, lastIccIoResult.simResponse.data.str);

    *rx_len = lastIccIoResult.simResponse.len / 2 + 2;
    *rx = calloc(*rx_len, sizeof(uint8_t));
    euicc_hexutil_hex2bin_r(*rx, *rx_len, lastIccIoResult.simResponse.data.str, lastIccIoResult.simResponse.len);
    (*rx)[*rx_len - 2] = lastIccIoResult.sw1;
    (*rx)[*rx_len - 1] = lastIccIoResult.sw2;

    // see radio_response_transact -- this is our buffer.
    free((void *)lastIccIoResult.simResponse.data.str);

    return 0;
}

static int libapduinterface_init(struct euicc_apdu_interface *ifstruct) {
    set_deprecated_env_name(ENV_DEBUG, "GBINDER_APDU_DEBUG");

    ifstruct->connect = apdu_interface_connect;
    ifstruct->disconnect = apdu_interface_disconnect;
    ifstruct->logic_channel_open = apdu_interface_logic_channel_open;
    ifstruct->logic_channel_close = apdu_interface_logic_channel_close;
    ifstruct->transmit = apdu_interface_transmit;

    // Install cleanup routine
    atexit(cleanup);
    signal(SIGINT, sighandler);

    // The glib loop is detached from any client object, so create it here.
    binder_loop = g_main_loop_new(NULL, FALSE);

    return 0;
}

static int libapduinterface_main(int argc, char **argv) { return 0; }

static void libapduinterface_fini(struct euicc_apdu_interface *ifstruct) {}

const struct euicc_driver driver_apdu_gbinder_hidl = {
    .type = DRIVER_APDU,
    .name = "gbinder_hidl",
    .init = (int (*)(void *))libapduinterface_init,
    .main = libapduinterface_main,
    .fini = (void (*)(void *))libapduinterface_fini,
};
