/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
/*
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 2 and
 * only version 2 as published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 * 02110-1301, USA.
 *
 * Copyright (C) 2011 - 2012 Red Hat, Inc.
 * Copyright (C) 2010 - 2012 Google, Inc.
 */

#include <glib.h>
#include <string.h>
#include <errno.h>
#include <sys/ioctl.h>
#include <fcntl.h>
#include <termios.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>

typedef u_int16_t u16;
typedef u_int8_t u8;
typedef u_int32_t u32;
typedef u_int64_t u64;

typedef unsigned int qbool;
#ifndef TRUE
#define TRUE 1
#endif
#ifndef FALSE
#define FALSE 0
#endif

/***********************************************************/

static void
print_buf (const char *detail, const char *buf, size_t len)
{
    int i = 0, z;
    qbool newline = FALSE, indent = FALSE;
    char *f;
    guint flen;

    f = g_strdup_printf ("%s (%zu)  ", detail, len);
    flen = strlen (f);
    g_print ("%s", f);
    for (i = 0; i < len; i++) {
        if (indent) {
            z = flen;
            while (z--)
                g_print (" ");
            indent = FALSE;
        }
        g_print ("%02x ", buf[i] & 0xFF);
        if (((i + 1) % 16) == 0) {
            g_print ("\n");
            newline = TRUE;
            indent = TRUE;
        } else
            newline = FALSE;
    }

    if (!newline)
        g_print ("\n");
}

/**************************************************************/

typedef enum {
    QMI_SVC_CTL = 0x00,
    QMI_SVC_WDS = 0x01,
    QMI_SVC_DMS = 0x02,
    QMI_SVC_NAS = 0x03,
    QMI_SVC_QOS = 0x04,
    QMI_SVC_WMS = 0x05,
    QMI_SVC_PDS = 0x06,
    QMI_SVC_AUTH = 0x07,
    QMI_SVC_AT = 0x08,
    QMI_SVC_VOICE = 0x09,
    QMI_SVC_CAT2 = 0x0A,
    QMI_SVC_UIM = 0x0B,
    QMI_SVC_PBM = 0x0C,
    QMI_SVC_LOC = 0x10,
    QMI_SVC_SAR = 0x11,
    QMI_SVC_RMTFS = 0x14,
    QMI_SVC_CAT = 0xE0,
    QMI_SVC_RMS = 0xE1,
    QMI_SVC_OMA = 0xE2
} qmi_service_type;

struct qmux {
    u8 tf;    /* always 1 */
    u16 len;
    u8 ctrl;
    u8 service;
    u8 qmicid;
} __attribute__((__packed__));

struct qmi_tlv_status {
    u16 status;
    u16 error;
} __attribute__((__packed__));

struct getcid_req {
    struct qmux header;
    u8 req;
    u8 tid;
    u16 msgid;
    u16 tlvsize;
    u8 service;
    u16 size;
    u8 qmisvc;
} __attribute__((__packed__));

struct releasecid_req {
    struct qmux header;
    u8 req;
    u8 tid;
    u16 msgid;
    u16 tlvsize;
    u8 rlscid;
    u16 size;
    u16 cid;
} __attribute__((__packed__));

struct version_info_req {
    struct qmux header;
    u8 req;
    u8 tid;
    u16 msgid;
    u16 tlvsize;
} __attribute__((__packed__));

struct qmi_ctl_version_info_list_service {
    u8 service_type;   /* QMI_SVC_xxx */
    u16 major_version;
    u16 minor_version;
} __attribute__((__packed__));

struct qmi_tlv_ctl_version_info_list {
    u8 count;
    struct qmi_ctl_version_info_list_service services[0];
}__attribute__((__packed__));

struct seteventreport_req {
    struct qmux header;
    u8 req;
    u16 tid;
    u16 msgid;
    u16 tlvsize;
    u8 reportchanrate;
    u16 size;
    u8 period;
    u32 mask;
} __attribute__((__packed__));

struct getpkgsrvcstatus_req {
    struct qmux header;
    u8 req;
    u16 tid;
    u16 msgid;
    u16 tlvsize;
} __attribute__((__packed__));

struct getmeid_req {
    struct qmux header;
    u8 req;
    u16 tid;
    u16 msgid;
    u16 tlvsize;
} __attribute__((__packed__));

struct qmiwds_stats {
    u32 txok;
    u32 rxok;
    u32 txerr;
    u32 rxerr;
    u32 txofl;
    u32 rxofl;
    u64 txbytesok;
    u64 rxbytesok;
    qbool linkstate;
    qbool reconfigure;
};

struct nas_signal_req {
    struct qmux header;
    u8 req;
    u16 tid;
    u16 msgid;
    u16 tlvsize;
} __attribute__((__packed__));

const size_t qmux_size = sizeof(struct qmux);

static void
qmux_fill(struct qmux *qmux, u8 service, u16 cid, u16 size)
{
    qmux->tf = 1;
    qmux->len = size - 1;
    qmux->ctrl = 0;
    qmux->service = cid & 0xff;
    qmux->qmicid = cid >> 8;
}

static void *
qmictl_new_getcid(u8 tid, u8 svctype, size_t *size)
{
    struct getcid_req *req;

    req = g_malloc0 (sizeof (*req));
    req->req = 0x00;
    req->tid = tid;
    req->msgid = 0x0022;
    req->tlvsize = 0x0004;
    req->service = 0x01;
    req->size = 0x0001;
    req->qmisvc = svctype;
    *size = sizeof(*req);
    qmux_fill (&req->header, QMI_SVC_CTL, 0, *size);
    return req;
}

static void *
qmictl_new_releasecid(u8 tid, u16 cid, size_t *size)
{
    struct releasecid_req *req;

    req = g_malloc0 (sizeof (*req));
    req->req = 0x00;
    req->tid = tid;
    req->msgid = 0x0023;
    req->tlvsize = 0x05;
    req->rlscid = 0x01;
    req->size = 0x0002;
    req->cid = cid;
    *size = sizeof(*req);
    qmux_fill (&req->header, QMI_SVC_CTL, 0, *size);
    return req;
}

static void *
qmictl_new_version_info(u8 tid, size_t *size)
{
    struct version_info_req *req;

    req = g_malloc0 (sizeof (*req));
    req->req = 0x00;
    req->tid = tid;
    req->msgid = 0x21;
    req->tlvsize = 0;
    *size = sizeof(*req);
    qmux_fill (&req->header, QMI_SVC_CTL, 0, *size);
    return req;
}

static void *
qmiwds_new_seteventreport(u8 tid, size_t *size)
{
    struct seteventreport_req *req;

    req = g_malloc0 (sizeof (*req));
    req->req = 0x00;
    req->tid = tid;
    req->msgid = 0x0001;
    req->tlvsize = 0x0008;
    req->reportchanrate = 0x11;
    req->size = 0x0005;
    req->period = 0x01;
    req->mask = 0x000000ff;
    *size = sizeof(*req);
    return req;
}

static void *
qmiwds_new_getpkgsrvcstatus(u8 tid, size_t *size)
{
    struct getpkgsrvcstatus_req *req;

    req = g_malloc0 (sizeof (*req));
    req->req = 0x00;
    req->tid = tid;
    req->msgid = 0x22;
    req->tlvsize = 0x0000;
    *size = sizeof(*req);
    return req;
}

static void *
qmidms_new_getmeid(u16 cid, u8 tid, size_t *size)
{
    struct getmeid_req *req;

    req = g_malloc0 (sizeof (*req));
    req->req = 0x00;
    req->tid = tid;
    req->msgid = 0x25;
    req->tlvsize = 0x0000;
    *size = sizeof(*req);
    qmux_fill (&req->header, QMI_SVC_WDS, cid, *size);
    return req;
}

static int
qmux_parse(u16 *cid, void *buf, size_t size)
{
    struct qmux *qmux = buf;

    if (!buf || size < 12)
        return -ENOMEM;

    if (qmux->tf != 1 || qmux->len != size - 1 || qmux->ctrl != 0x80)
        return -EINVAL;

    *cid = (qmux->qmicid << 8) + qmux->service;
    return sizeof(*qmux);
}

static u16
tlv_get(void *msg, u16 msgsize, u8 type, void *buf, u16 bufsize)
{
    u16 pos;
    u16 msize = 0;

    if (!msg || !buf)
        return -ENOMEM;

    for (pos = 4;  pos + 3 < msgsize; pos += msize + 3) {
        msize = *(u16 *)(msg + pos + 1);
        if (*(u8 *)(msg + pos) == type) {
            if (bufsize < msize)
                return -ENOMEM;

            memcpy(buf, msg + pos + 3, msize);
            return msize;
        }
    }

    return -ENOMSG;
}

static int
qmi_msgisvalid(void *msg, u16 size)
{
    struct qmi_tlv_status status;

    if (tlv_get(msg, size, 2, &status, sizeof (status)) == sizeof (status)) {
        if (le16toh (status.status != 0))
            return le16toh (status.error);
        else
            return 0;
    }
    return -ENOMSG;
}

static int
qmi_msgid(void *msg, u16 size)
{
    return size < 2 ? -ENODATA : *(u16 *)msg;
}

static int
qmictl_version_info_resp(void *buf, u16 size)
{
    int result, i;
    u8 offset = sizeof(struct qmux) + 2;
    u8 svcbuf[100];
    struct qmi_tlv_ctl_version_info_list *service_list;
    struct qmi_ctl_version_info_list_service *svc;

    if (!buf || size < offset)
        return -ENOMEM;

    buf = buf + offset;
    size -= offset;

    result = qmi_msgid(buf, size);
    if (result != 0x21)
        return -EFAULT;

    result = qmi_msgisvalid(buf, size);
    if (result != 0)
        return -EFAULT;

    /* Get the services TLV */
    result = tlv_get(buf, size, 0x01, svcbuf, sizeof (svcbuf));
    if (result < 0)
        return -EFAULT;

    service_list = (struct qmi_tlv_ctl_version_info_list *) svcbuf;
    if (result < (service_list->count * sizeof (struct qmi_ctl_version_info_list_service)))
        return -EFAULT;

    svc = &(service_list->services[0]);
    for (i = 0; i < service_list->count; i++, svc++) {
        g_message ("SVC: %d  v%d.%d",
                   svc->service_type,
                   le16toh (svc->major_version),
                   le16toh (svc->minor_version));
    }

    return 0;
}

static int
qmictl_getcid_resp(void *buf, u16 size, u16 *cid)
{
    int result;
    u8 offset = sizeof(struct qmux) + 2;

    if (!buf || size < offset)
        return -ENOMEM;

    buf = buf + offset;
    size -= offset;

    result = qmi_msgid(buf, size);
    if (result != 0x22)
        return -EFAULT;

    result = qmi_msgisvalid(buf, size);
    if (result != 0)
        return -EFAULT;

    result = tlv_get(buf, size, 0x01, cid, 2);
    if (result != 2)
        return -EFAULT;

    return 0;
}

static int
qmictl_releasecid_resp(void *buf, u16 size)
{
    int result;
    u8 offset = sizeof(struct qmux) + 2;

    if (!buf || size < offset)
        return -ENOMEM;

    buf = buf + offset;
    size -= offset;

    result = qmi_msgid(buf, size);
    if (result != 0x23)
        return -EFAULT;

    result = qmi_msgisvalid(buf, size);
    if (result != 0)
        return -EFAULT;

    return 0;
}

static int
qmiwds_event_resp(void *buf, u16 size, struct qmiwds_stats *stats)
{
    int result;
    u8 status[2];

    u8 offset = sizeof(struct qmux) + 3;

    if (!buf || size < offset || !stats)
        return -ENOMEM;

    buf = buf + offset;
    size -= offset;

    result = qmi_msgid(buf, size);
    if (result == 0x01) {
        tlv_get(buf, size, 0x10, &stats->txok, 4);
        tlv_get(buf, size, 0x11, &stats->rxok, 4);
        tlv_get(buf, size, 0x12, &stats->txerr, 4);
        tlv_get(buf, size, 0x13, &stats->rxerr, 4);
        tlv_get(buf, size, 0x14, &stats->txofl, 4);
        tlv_get(buf, size, 0x15, &stats->rxofl, 4);
        tlv_get(buf, size, 0x19, &stats->txbytesok, 8);
        tlv_get(buf, size, 0x1A, &stats->rxbytesok, 8);
    } else if (result == 0x22) {
        result = tlv_get(buf, size, 0x01, &status[0], 2);
        if (result >= 1)
            stats->linkstate = status[0] == 0x02;
        if (result == 2)
            stats->reconfigure = status[1] == 0x01;

        if (result < 0)
            return result;
    } else {
        return -EFAULT;
    }

    return 0;
}

static int
qmidms_meid_resp(void *buf, u16 size, char *meid, int meidsize)
{
    int result;

    u8 offset = sizeof(struct qmux) + 3;

    if (!buf || size < offset || meidsize < 14)
        return -ENOMEM;

    buf = buf + offset;
    size -= offset;

    result = qmi_msgid(buf, size);
    if (result != 0x25)
        return -EFAULT;

    result = qmi_msgisvalid(buf, size);
    if (result)
        return -EFAULT;

    result = tlv_get(buf, size, 0x12, meid, 14);
    if (result != 14)
        return -EFAULT;

    return 0;
}

static void *
qminas_new_signal(u16 cid, u8 tid, size_t *size)
{
    struct nas_signal_req *req;

    req = g_malloc0 (sizeof (*req));
    req->req = 0x00;
    req->tid = tid;
    req->msgid = 0x20;
    req->tlvsize = 0x0000;
    *size = sizeof(*req);
    qmux_fill (&req->header, QMI_SVC_NAS, cid, *size);
    return req;
}

/* NAS/Get Signal Strength TLV 0x01 */
struct qminas_resp_signalstrength {
    u8 dbm;
    u8 act;
} __attribute__((__packed__));

static int
qminas_signal_resp(void *buf, u16 size, u8 *dbm, u8 *act)
{
    int result;
    struct qminas_resp_signalstrength signal;

    u8 offset = sizeof(struct qmux) + 3;

    if (!buf || size < offset)
        return -ENOMEM;

    buf = buf + offset;
    size -= offset;

    result = qmi_msgid(buf, size);
    if (result != 0x20)
        return -EFAULT;

    result = qmi_msgisvalid(buf, size);
    if (result)
        return -EFAULT;

    result = tlv_get(buf, size, 0x01, &signal, sizeof (signal));
    if (result != sizeof (signal))
        return -EFAULT;

    *dbm = signal.dbm;
    *act = signal.act;
    return 0;
}

/*****************************************************/

static size_t
send_and_wait_reply (int fd, void *b, size_t blen, char *reply, size_t rlen)
{
    ssize_t num;
    fd_set in;
    int result;
    struct timeval timeout = { 1, 0 };

    print_buf (">>>", b, blen);
    num = write (fd, b, blen);
    if (num != blen) {
        g_warning ("Failed to write: wrote %zd err %d", num, errno);
        return 0;
    }

    FD_ZERO (&in);
    FD_SET (fd, &in);
    result = select (fd + 1, &in, NULL, NULL, &timeout);
    if (result != 1 || !FD_ISSET (fd, &in)) {
        g_warning ("No data pending");
        return 0;
    }

    errno = 0;
    num = read (fd, reply, rlen);
    if (num < 0) {
        g_warning ("read error %d", errno);
        return 0;
    }
    print_buf ("<<<", reply, num);
    return num;
}

/* CTL service transaction ID */
static u8 ctl_tid = 1;

static int
get_meid (int fd)
{
    void *b;
    size_t blen;
    u8 dms_tid = 1;
    u16 dms_cid = 0;
    char reply[2048];
    size_t rlen;
    char meid[16];
    int err;

    /* Allocate a DMS client ID */
    b = qmictl_new_getcid (ctl_tid++, QMI_SVC_DMS, &blen);
    g_assert (b);

    rlen = send_and_wait_reply (fd, b, blen, reply, sizeof (reply));
    g_free (b);
    if (rlen <= 0)
        return 1;

    err = qmictl_getcid_resp (reply, rlen, &dms_cid);
    if (err < 0) {
        g_warning ("Failed to get DMS client ID: %d", err);
        return 1;
    }

    g_message ("DMS CID %d 0x%X", dms_cid, dms_cid);

    /* Get the MEID */
    b = qmidms_new_getmeid(dms_cid, dms_tid++, &blen);
    g_assert (b);

    rlen = send_and_wait_reply (fd, b, blen, reply, sizeof (reply));
    g_free (b);
    if (rlen <= 0)
        return 1;

    memset (meid, 0, sizeof (meid));
    err = qmidms_meid_resp (reply, rlen, meid, sizeof (meid) - 1);
    if (err < 0)
        g_warning ("Failed to get MEID: %d", err);
    else
        g_message ("MEID: %s", meid);

    /* Relese the DMS client ID */
    b = qmictl_new_releasecid (ctl_tid++, dms_cid, &blen);
    g_assert (b);

    rlen = send_and_wait_reply (fd, b, blen, reply, sizeof (reply));
    g_free (b);
    if (rlen <= 0)
        return 1;

    err = qmictl_releasecid_resp (reply, rlen);
    if (err < 0) {
        g_warning ("Failed to release DMS client ID: %d", err);
        return 1;
    }

    return 0;
}

static const char *
act_to_string (u8 act)
{
    switch (act) {
    case 0:
        return "no service";
    case 1:
        return "CDMA2000 1x";
    case 2:
        return "CDMA2000 HRPD/EVDO";
    case 3:
        return "AMPS";
    case 4:
        return "GSM";
    case 5:
        return "UMTS";
    case 8:
        return "LTE";
    default:
        break;
    }
    return "unknown";
}

static int
get_signal (int fd)
{
    void *b;
    size_t blen;
    u8 nas_tid = 1;
    u16 nas_cid = 0;
    char reply[2048];
    size_t rlen;
    int err;
    u8 dbm = 0, act = 0;

    /* Allocate a NAS client ID */
    b = qmictl_new_getcid (ctl_tid++, QMI_SVC_NAS, &blen);
    g_assert (b);

    rlen = send_and_wait_reply (fd, b, blen, reply, sizeof (reply));
    g_free (b);
    if (rlen <= 0)
        return 1;

    err = qmictl_getcid_resp (reply, rlen, &nas_cid);
    if (err < 0) {
        g_warning ("Failed to get NAS client ID: %d", err);
        return 1;
    }

    g_message ("NAS CID %d 0x%X", nas_cid, nas_cid);

    /* Get the signal strength */
    b = qminas_new_signal(nas_cid, nas_tid++, &blen);
    g_assert (b);

    rlen = send_and_wait_reply (fd, b, blen, reply, sizeof (reply));
    g_free (b);
    if (rlen <= 0)
        return 1;

    err = qminas_signal_resp (reply, rlen, &dbm, &act);
    if (err < 0)
        g_warning ("Failed to get signal: %d", err);
    else {
        g_message ("dBm: -%d", 0x100 - (dbm & 0xFF));
        g_message ("AcT: %s", act_to_string (act));
    }

    /* Relese the NAS client ID */
    b = qmictl_new_releasecid (ctl_tid++, nas_cid, &blen);
    g_assert (b);

    rlen = send_and_wait_reply (fd, b, blen, reply, sizeof (reply));
    g_free (b);
    if (rlen <= 0)
        return 1;

    err = qmictl_releasecid_resp (reply, rlen);
    if (err < 0) {
        g_warning ("Failed to release NAS client ID: %d", err);
        return 1;
    }

    return 0;
}

int main(int argc, char *argv[])
{
    int fd;
    void *b;
    size_t blen;
    u8 ctl_tid = 1;
    char reply[2048];
    size_t rlen;
    int err;

    if (argc != 2) {
        g_warning ("usage: %s <port>", argv[0]);
        return 1;
    }

    errno = 0;
    fd = open (argv[1], O_RDWR | O_EXCL | O_NONBLOCK | O_NOCTTY);
    if (fd < 0) {
        g_warning ("%s: open failed: %d", argv[1], errno);
        return 1;
    }

    /* Send the ready request */
    b = qmictl_new_version_info (ctl_tid++, &blen);
    g_assert (b);

    rlen = send_and_wait_reply (fd, b, blen, reply, sizeof (reply));
    g_free (b);
    if (rlen <= 0)
        goto out;

    err = qmictl_version_info_resp (reply, rlen);
    if (err < 0) {
        g_warning ("Failed to get version info: %d", err);
        goto out;
    }

    get_meid (fd);
    get_signal (fd);

out:
    close (fd);
    return 0;
}

