// Parser for NDEF format data
// Supports multiple NDEF messages and records in same tag
// Parsed types: URI (+ Phone, Mail), Text, BT MAC, Contact, WiFi, Empty
// Documentation and sources indicated where relevant
// Made by @Willy-JL

#include "nfc_supported_card_plugin.h"

#include <nfc/helpers/nfc_util.h>
#include <flipper_application/flipper_application.h>
#include <nfc/protocols/mf_ultralight/mf_ultralight.h>

#define TAG "NDEF"

static bool is_text(const uint8_t* buf, size_t len) {
    for(size_t i = 0; i < len; i++) {
        const char c = buf[i];
        if((c < ' ' || c > '~') && c != '\r' && c != '\n') {
            return false;
        }
    }
    return true;
}

static void
    print_data(FuriString* str, const char* prefix, const uint8_t* buf, size_t len, bool force_hex) {
    if(prefix) furi_string_cat_printf(str, "%s: ", prefix);
    if(!force_hex && is_text(buf, len)) {
        char* tmp = malloc(len + 1);
        memcpy(tmp, buf, len);
        tmp[len] = '\0';
        furi_string_cat_printf(str, "%s", tmp);
        free(tmp);
    } else {
        for(uint8_t i = 0; i < len; i++) {
            furi_string_cat_printf(str, "%02X ", buf[i]);
        }
    }
    furi_string_cat(str, "\n");
}

static void parse_ndef_uri(FuriString* str, const uint8_t* payload, uint32_t payload_len) {
    // https://learn.adafruit.com/adafruit-pn532-rfid-nfc/ndef#uri-records-0x55-slash-u-607763
    const char* prepends[] = {
        [0x00] = "",
        [0x01] = "http://www.",
        [0x02] = "https://www.",
        [0x03] = "http://",
        [0x04] = "https://",
        [0x05] = "tel:",
        [0x06] = "mailto:",
        [0x07] = "ftp://anonymous:anonymous@",
        [0x08] = "ftp://ftp.",
        [0x09] = "ftps://",
        [0x0A] = "sftp://",
        [0x0B] = "smb://",
        [0x0C] = "nfs://",
        [0x0D] = "ftp://",
        [0x0E] = "dav://",
        [0x0F] = "news:",
        [0x10] = "telnet://",
        [0x11] = "imap:",
        [0x12] = "rtsp://",
        [0x13] = "urn:",
        [0x14] = "pop:",
        [0x15] = "sip:",
        [0x16] = "sips:",
        [0x17] = "tftp:",
        [0x18] = "btspp://",
        [0x19] = "btl2cap://",
        [0x1A] = "btgoep://",
        [0x1B] = "tcpobex://",
        [0x1C] = "irdaobex://",
        [0x1D] = "file://",
        [0x1E] = "urn:epc:id:",
        [0x1F] = "urn:epc:tag:",
        [0x20] = "urn:epc:pat:",
        [0x21] = "urn:epc:raw:",
        [0x22] = "urn:epc:",
        [0x23] = "urn:nfc:",
    };
    const char* prepend = "";
    uint8_t prepend_type = payload[0];
    if(prepend_type < COUNT_OF(prepends)) {
        prepend = prepends[prepend_type];
    }
    size_t prepend_len = strlen(prepend);

    size_t uri_len = prepend_len + (payload_len - 1);
    char* const uri_buf = malloc(uri_len);
    memcpy(uri_buf, prepend, prepend_len);
    memcpy(uri_buf + prepend_len, payload + 1, payload_len - 1);
    char* uri = uri_buf;

    const char* type = "URI";
    if(strncmp(uri, "http", strlen("http")) == 0) {
        type = "URL";
    } else if(strncmp(uri, "tel:", strlen("tel:")) == 0) {
        type = "Phone";
        uri += strlen("tel:");
        uri_len -= strlen("tel:");
    } else if(strncmp(uri, "mailto:", strlen("mailto:")) == 0) {
        type = "Mail";
        uri += strlen("mailto:");
        uri_len -= strlen("mailto:");
    }

    furi_string_cat_printf(str, "%s\n", type);
    print_data(str, NULL, (uint8_t*)uri, uri_len, false);
    free(uri_buf);
}

static void parse_ndef_text(FuriString* str, const uint8_t* payload, uint32_t payload_len) {
    furi_string_cat(str, "Text\n");
    print_data(str, NULL, payload + 3, payload_len - 3, false);
}

static void parse_ndef_bt(FuriString* str, const uint8_t* payload, uint32_t payload_len) {
    furi_string_cat(str, "BT MAC\n");
    print_data(str, NULL, payload + 2, payload_len - 2, true);
}

static void parse_ndef_vcard(FuriString* str, const uint8_t* payload, uint32_t payload_len) {
    char* tmp = malloc(payload_len + 1);
    memcpy(tmp, payload, payload_len);
    tmp[payload_len] = '\0';
    FuriString* fmt = furi_string_alloc_set(tmp);
    free(tmp);

    furi_string_trim(fmt);
    if(furi_string_start_with(fmt, "BEGIN:VCARD")) {
        furi_string_right(fmt, furi_string_search_char(fmt, '\n'));
        if(furi_string_end_with(fmt, "END:VCARD")) {
            furi_string_left(fmt, furi_string_search_rchar(fmt, '\n'));
        }
        furi_string_trim(fmt);
        if(furi_string_start_with(fmt, "VERSION:")) {
            furi_string_right(fmt, furi_string_search_char(fmt, '\n'));
            furi_string_trim(fmt);
        }
    }

    furi_string_cat(str, "Contact\n");
    print_data(str, NULL, (uint8_t*)furi_string_get_cstr(fmt), furi_string_size(fmt), false);
    furi_string_free(fmt);
}

static void parse_ndef_wifi(FuriString* str, const uint8_t* payload, uint32_t payload_len) {
// https://android.googlesource.com/platform/packages/apps/Nfc/+/refs/heads/main/src/com/android/nfc/NfcWifiProtectedSetup.java
#define CREDENTIAL_FIELD_ID (0x100E)
#define SSID_FIELD_ID (0x1045)
#define NETWORK_KEY_FIELD_ID (0x1027)
#define AUTH_TYPE_FIELD_ID (0x1003)
#define AUTH_TYPE_EXPECTED_SIZE (2)
#define AUTH_TYPE_OPEN (0x0001)
#define AUTH_TYPE_WPA_PSK (0x0002)
#define AUTH_TYPE_WPA_EAP (0x0008)
#define AUTH_TYPE_WPA2_EAP (0x0010)
#define AUTH_TYPE_WPA2_PSK (0x0020)
#define AUTH_TYPE_WPA_AND_WPA2_PSK (0x0022)
#define MAX_NETWORK_KEY_SIZE_BYTES (64)

    size_t i = 0;
    while(i < payload_len) {
        uint16_t field_id = nfc_util_bytes2num(payload + i, 2);
        i += 2;
        uint16_t field_len = nfc_util_bytes2num(payload + i, 2);
        i += 2;

        if(field_id == CREDENTIAL_FIELD_ID) {
            furi_string_cat(str, "WiFi\n");
            size_t start_position = i;
            while(i < start_position + field_len) {
                uint16_t cfg_id = nfc_util_bytes2num(payload + i, 2);
                i += 2;
                uint16_t cfg_len = nfc_util_bytes2num(payload + i, 2);
                i += 2;

                if(i + cfg_len > start_position + field_len) {
                    return;
                }

                switch(cfg_id) {
                case SSID_FIELD_ID:
                    print_data(str, "SSID", payload + i, cfg_len, false);
                    i += cfg_len;
                    break;
                case NETWORK_KEY_FIELD_ID:
                    if(cfg_len > MAX_NETWORK_KEY_SIZE_BYTES) {
                        return;
                    }
                    print_data(str, "PWD", payload + i, cfg_len, false);
                    i += cfg_len;
                    break;
                case AUTH_TYPE_FIELD_ID:
                    if(cfg_len != AUTH_TYPE_EXPECTED_SIZE) {
                        return;
                    }
                    short auth_type = nfc_util_bytes2num(payload + i, 2);
                    i += 2;
                    const char* auth;
                    switch(auth_type) {
                    case AUTH_TYPE_OPEN:
                        auth = "Open";
                        break;
                    case AUTH_TYPE_WPA_PSK:
                        auth = "WPA Personal";
                        break;
                    case AUTH_TYPE_WPA_EAP:
                        auth = "WPA Enterprise";
                        break;
                    case AUTH_TYPE_WPA2_EAP:
                        auth = "WPA2 Enterprise";
                        break;
                    case AUTH_TYPE_WPA2_PSK:
                        auth = "WPA2 Personal";
                        break;
                    case AUTH_TYPE_WPA_AND_WPA2_PSK:
                        auth = "WPA/WPA2 Personal";
                        break;
                    default:
                        auth = "Unknown";
                        break;
                    }
                    print_data(str, "AUTH", (uint8_t*)auth, strlen(auth), false);
                    break;
                default:
                    i += cfg_len;
                    break;
                }
            }
            return;
        }
        i += field_len;
    }
}

static void parse_ndef_payload(
    FuriString* str,
    uint8_t tnf,
    const char* type,
    uint8_t type_len,
    const uint8_t* payload,
    uint32_t payload_len) {
    if(!payload_len) {
        furi_string_cat(str, "Empty\n");
        return;
    }
    switch(tnf) {
    case 0x01: // NFC Forum well-known type [NFC RTD]
        if(strncmp("U", type, type_len) == 0) {
            parse_ndef_uri(str, payload, payload_len);
        } else if(strncmp("T", type, type_len) == 0) {
            parse_ndef_text(str, payload, payload_len);
        } else {
            print_data(str, "Well-known type", (uint8_t*)type, type_len, false);
            print_data(str, "Payload", payload, payload_len, false);
        }
        break;
    case 0x02: // Media-type [RFC 2046]
        if(strncmp("application/vnd.bluetooth.ep.oob", type, type_len) == 0) {
            parse_ndef_bt(str, payload, payload_len);
        } else if(strncmp("text/vcard", type, type_len) == 0) {
            parse_ndef_vcard(str, payload, payload_len);
        } else if(strncmp("application/vnd.wfa.wsc", type, type_len) == 0) {
            parse_ndef_wifi(str, payload, payload_len);
        } else {
            print_data(str, "Media Type", (uint8_t*)type, type_len, false);
            print_data(str, "Payload", payload, payload_len, false);
        }
        break;
    case 0x00: // Empty
    case 0x03: // Absolute URI [RFC 3986]
    case 0x04: // NFC Forum external type [NFC RTD]
    case 0x05: // Unknown
    case 0x06: // Unchanged
    case 0x07: // Reserved
    default: // Unknown
        // Dump data without parsing
        print_data(str, "Type name format", &tnf, 1, true);
        print_data(str, "Type", (uint8_t*)type, type_len, false);
        print_data(str, "Payload", payload, payload_len, false);
        break;
    }
}

static const uint8_t* parse_ndef_message(
    FuriString* str,
    size_t message_num,
    const uint8_t* cur,
    const uint8_t* message_end) {
    // NDEF message and record documentation:
    // https://developer.nordicsemi.com/nRF_Connect_SDK/doc/latest/nrf/protocols/nfc/index.html#ndef-message-and-record-format
    size_t record_num = 0;
    bool last_record = false;
    while(cur < message_end) {
        // Flags and TNF
        uint8_t flags_tnf = *cur++;
        // Message Begin should only be set on first record
        if(record_num++ && flags_tnf & (1 << 7)) break;
        // Message End should only be set on last record
        if(last_record) break;
        if(flags_tnf & (1 << 6)) last_record = true;
        // Chunked Flag not supported
        if(flags_tnf & (1 << 5)) break;
        // Payload Length field of 1 vs 4 bytes
        bool short_record = flags_tnf & (1 << 4);
        // Is payload ID length and value present
        bool id_present = flags_tnf & (1 << 3);
        // Type Name Format 3 bit value
        uint8_t tnf = flags_tnf & 0b00000111;

        // Type Length
        uint8_t type_len = *cur++;

        // Payload Length
        uint32_t payload_len;
        if(short_record) {
            payload_len = *cur++;
        } else {
            payload_len = nfc_util_bytes2num(cur, 4);
            cur += 4;
        }

        // ID Length
        uint8_t id_len = 0;
        if(id_present) {
            id_len = *cur++;
        }

        // Payload Type
        char* type = NULL;
        if(type_len) {
            type = malloc(type_len);
            memcpy(type, cur, type_len);
            cur += type_len;
        }

        // Payload ID
        cur += id_len;

        furi_string_cat_printf(str, "\e*> M:%d R:%d - ", message_num, record_num);
        parse_ndef_payload(str, tnf, type, type_len, cur, payload_len);
        cur += payload_len;

        free(type);
        furi_string_trim(str, "\n");
        furi_string_cat(str, "\n\n");
    }
    return cur;
}

static bool ndef_parse(const NfcDevice* device, FuriString* parsed_data) {
    furi_assert(device);
    furi_assert(parsed_data);

    const MfUltralightData* data = nfc_device_get_data(device, NfcProtocolMfUltralight);

    bool parsed = false;

    do {
        // Memory layout documentation:
        // https://developer.nordicsemi.com/nRF_Connect_SDK/doc/latest/nrfxlib/nfc/doc/type_2_tag.html#id2

        // Double check static values layout
        // First 4 static reserved pages for UID, internal and lock bytes
        // (Not sure if NDEF cata can be found in cards with different layout)
        if(data->page[0].data[0] != 0x04) break;
        if(data->page[2].data[1] != 0x48) break; // Internal
        if(data->page[2].data[2] != 0x00) break; // Lock bytes
        if(data->page[2].data[3] != 0x00) break; // ...
        if(data->page[3].data[0] != 0xE1) break; // Capability container
        if(data->page[3].data[1] != 0x10) break; // ...

        // Data content starts here at 5th page
        const uint8_t* cur = &data->page[4].data[0];
        const uint8_t* end = &data->page[0].data[0] +
                             (mf_ultralight_get_pages_total(data->type) * MF_ULTRALIGHT_PAGE_SIZE);
        size_t message_num = 0;

        // Parse as TLV (see docs above)
        while(cur < end) {
            switch(*cur++) {
            case 0x03: { // NDEF message
                if(cur >= end) break;
                uint16_t len;
                if(*cur < 0xFF) { // 1 byte length
                    len = *cur++;
                } else { // 3 byte length (0xFF marker + 2 byte integer)
                    if(cur + 2 >= end) {
                        cur = end;
                        break;
                    }
                    len = nfc_util_bytes2num(++cur, 2);
                    cur += 2;
                }
                if(cur + len >= end) {
                    cur = end;
                    break;
                }

                if(message_num++ == 0) {
                    furi_string_printf(
                        parsed_data,
                        "\e#NDEF Format Data\nCard type: %s\n",
                        mf_ultralight_get_device_name(data, NfcDeviceNameTypeFull));
                }

                const uint8_t* message_end = cur + len;
                cur = parse_ndef_message(parsed_data, message_num, cur, message_end);
                if(cur != message_end) cur = end;

                break;
            }

            case 0xFE: // TLV end
                cur = end;
                if(message_num != 0) parsed = true;
                break;

            case 0x00: // Padding, has no length, skip
                break;

            case 0x01: // Lock control
            case 0x02: // Memory control
            case 0xFD: // Proprietary
                // We don't care, skip this TLV block
                if(cur >= end) break;
                if(*cur < 0xFF) { // 1 byte length
                    cur += *cur + 1; // Shift by TLV length
                } else { // 3 byte length (0xFF marker + 2 byte integer)
                    if(cur + 2 >= end) {
                        cur = end;
                        break;
                    }
                    cur += nfc_util_bytes2num(cur + 1, 2) + 3; // Shift by TLV length
                }
                break;

            default: // Unknown, bail to avoid problems
                cur = end;
                break;
            }
        }

        if(parsed) {
            furi_string_trim(parsed_data, "\n");
            furi_string_cat(parsed_data, "\n");
        } else {
            furi_string_reset(parsed_data);
        }
    } while(false);

    return parsed;
}

/* Actual implementation of app<>plugin interface */
static const NfcSupportedCardsPlugin ndef_plugin = {
    .protocol = NfcProtocolMfUltralight,
    .verify = NULL,
    .read = NULL,
    .parse = ndef_parse,
};

/* Plugin descriptor to comply with basic plugin specification */
static const FlipperAppPluginDescriptor ndef_plugin_descriptor = {
    .appid = NFC_SUPPORTED_CARD_PLUGIN_APP_ID,
    .ep_api_version = NFC_SUPPORTED_CARD_PLUGIN_API_VERSION,
    .entry_point = &ndef_plugin,
};

/* Plugin entry point - must return a pointer to const descriptor  */
const FlipperAppPluginDescriptor* ndef_plugin_ep() {
    return &ndef_plugin_descriptor;
}
