/***************************************************************************
 *             __________               __   ___.
 *   Open      \______   \ ____   ____ |  | _\_ |__   _______  ___
 *   Source     |       _//  _ \_/ ___\|  |/ /| __ \ /  _ \  \/  /
 *   Jukebox    |    |   (  <_> )  \___|    < | \_\ (  <_> > <  <
 *   Firmware   |____|_  /\____/ \___  >__|_ \|___  /\____/__/\_ \
 *                     \/            \/     \/    \/            \/
 *
 * Copyright (C) 2016 Franklin Wei
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
 * KIND, either express or implied.
 *
 ****************************************************************************/

/* simple OTP plugin */

/* see RFCs 4226, 6238 for more information about the algorithms used */

#include "plugin.h"

#include "lib/display_text.h"
#include "lib/pluginlib_actions.h"
#include "lib/pluginlib_exit.h"
#include "lib/sha1.h"

#define MAX_NAME   50
#define SECRET_MAX 256
#define URI_MAX    256
#define ACCT_FILE  PLUGIN_APPS_DATA_DIR "/otp.dat"

#define MAX(a, b) (((a)>(b))?(a):(b))

struct account_t {
    char name[MAX_NAME];

    bool is_totp; // hotp otherwise

    union {
        uint64_t hotp_counter;
        unsigned long totp_period;
    };

    int digits;

    unsigned char secret[SECRET_MAX];
    int sec_len;
};

static int max_accts = 0;

/* in plugin buffer */
static struct account_t *accounts = NULL;

static int next_slot = 0;

/* in SECONDS, asked for on first run */
static int time_offs = 0;

static int HOTP(unsigned char *secret, size_t sec_len, uint64_t ctr, int digits)
{
    ctr = htobe64(ctr);
    unsigned char hash[20];
    if(hmac_sha1(secret, sec_len, &ctr, 8, hash))
    {
        return -1;
    }

    int offs = hash[19] & 0xF;
    uint32_t code = (hash[offs] & 0x7F) << 24 |
        hash[offs + 1] << 16 |
        hash[offs + 2] << 8  |
        hash[offs + 3];

    int mod = 1;
    for(int i = 0; i < digits; ++i)
        mod *= 10;

    // debug
    // rb->splashf(HZ * 5, "HOTP %*s, %llu, %d: %d", sec_len, secret, htobe64(ctr), digits, code % mod);

    return code % mod;
}

#if CONFIG_RTC
static time_t get_utc(void)
{
    return rb->mktime(rb->get_time()) - time_offs;
}

static int TOTP(unsigned char *secret, size_t sec_len, uint64_t step, int digits)
{
    uint64_t tm = get_utc() / step;
    return HOTP(secret, sec_len, tm, digits);
}
#endif

/* search the accounts for a duplicate */
static bool acct_exists(const char *name)
{
    for(int i = 0; i < next_slot; ++i)
        if(!rb->strcmp(accounts[i].name, name))
            return true;
    return false;
}

// Base32 implementation
//
// Copyright 2010 Google Inc.
// Author: Markus Gutschke
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

static int base32_decode(uint8_t *result, int bufSize, const uint8_t *encoded) {
  int buffer = 0;
  int bitsLeft = 0;
  int count = 0;
  for (const uint8_t *ptr = encoded; count < bufSize && *ptr; ++ptr) {
    uint8_t ch = *ptr;
    if (ch == ' ' || ch == '\t' || ch == '\r' || ch == '\n' || ch == '-') {
      continue;
    }
    buffer <<= 5;

    // Deal with commonly mistyped characters
    if (ch == '0') {
      ch = 'O';
    } else if (ch == '1') {
      ch = 'L';
    } else if (ch == '8') {
      ch = 'B';
    }

    // Look up one base32 digit
    if ((ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z')) {
      ch = (ch & 0x1F) - 1;
    } else if (ch >= '2' && ch <= '7') {
      ch -= '2' - 26;
    } else {
      return -1;
    }

    buffer |= ch;
    bitsLeft += 5;
    if (bitsLeft >= 8) {
      result[count++] = buffer >> (bitsLeft - 8);
      bitsLeft -= 8;
    }
  }
  if (count < bufSize) {
    result[count] = '\000';
  }
  return count;
}

static int base32_encode(const uint8_t *data, int length, uint8_t *result,
                         int bufSize) {
  if (length < 0 || length > (1 << 28)) {
    return -1;
  }
  int count = 0;
  if (length > 0) {
    int buffer = data[0];
    int next = 1;
    int bitsLeft = 8;
    while (count < bufSize && (bitsLeft > 0 || next < length)) {
      if (bitsLeft < 5) {
        if (next < length) {
          buffer <<= 8;
          buffer |= data[next++] & 0xFF;
          bitsLeft += 8;
        } else {
          int pad = 5 - bitsLeft;
          buffer <<= pad;
          bitsLeft += pad;
        }
      }
      int index = 0x1F & (buffer >> (bitsLeft - 5));
      bitsLeft -= 5;
      result[count++] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"[index];
    }
  }
  if (count < bufSize) {
    result[count] = '\000';
  }
  return count;
}

/***********************************************************************
 * File browser (from rockpaint)
 ***********************************************************************/

static bool browse( char *dst, int dst_size, const char *start )
{
    struct browse_context browse = {
        .dirfilter = SHOW_ALL,
        .flags = BROWSE_SELECTONLY | BROWSE_NO_CONTEXT_MENU,
        .icon = Icon_NOICON,
        .root = start,
        .buf = dst,
        .bufsize = dst_size,
    };

    rb->rockbox_browse(&browse);
    return (browse.flags & BROWSE_SELECTED);
}

static bool read_accts(void)
{
    int fd = rb->open(ACCT_FILE, O_RDONLY);
    if(fd < 0)
        return false;

    char buf[4];
    char magic[4] = { 'O', 'T', 'P', '1' };
    rb->read(fd, buf, 4);
    if(memcmp(magic, buf, 4))
    {
        rb->splash(HZ * 2, "Corrupt save data!");
        rb->close(fd);
        return false;
    }

    rb->read(fd, &time_offs, sizeof(time_offs));

    while(next_slot < max_accts)
    {
        if(rb->read(fd, accounts + next_slot, sizeof(struct account_t)) != sizeof(struct account_t))
            break;
        ++next_slot;
    }

    rb->close(fd);
    return true;
}

static void save_accts(void)
{
    int fd = rb->open(ACCT_FILE, O_WRONLY | O_CREAT | O_TRUNC, 0600);
    rb->fdprintf(fd, "OTP1");

    rb->write(fd, &time_offs, sizeof(time_offs));

    for(int i = 0; i < next_slot; ++i)
        rb->write(fd, accounts + i, sizeof(struct account_t));
    rb->close(fd);
}

static void add_acct_file(void)
{
    char fname[MAX_PATH];
    rb->splash(HZ * 2, "Please choose file containing URI(s).");
    int before = next_slot;
    if(browse(fname, sizeof(fname), "/"))
    {
        int fd = rb->open(fname, O_RDONLY);
        do {
            memset(accounts + next_slot, 0, sizeof(struct account_t));

            accounts[next_slot].digits = 6;

            char uri_buf[URI_MAX];
            if(!rb->read_line(fd, uri_buf, sizeof(uri_buf)))
                break;

            if(next_slot >= max_accts)
            {
                rb->splash(HZ * 2, "Account limit reached: some accounts not added.");
                break;
            }

            /* check for URI prefix */
            if(rb->strncmp(uri_buf, "otpauth://", 10))
                continue;

            char *save;
            char *tok = rb->strtok_r(uri_buf + 10, "/", &save);
            if(!rb->strcmp(tok, "totp"))
            {
                accounts[next_slot].is_totp = true;
                accounts[next_slot].totp_period = 30;
#if !CONFIG_RTC
                rb->splash(2 * HZ, "TOTP not supported!");
                continue;
#endif
            }
            else if(!rb->strcmp(tok, "hotp"))
            {
                accounts[next_slot].is_totp = false;
                accounts[next_slot].hotp_counter = 0;
            }

            tok = rb->strtok_r(NULL, "?", &save);
            if(!tok)
                continue;

            if(acct_exists(tok))
            {
                rb->splashf(HZ * 2, "Not adding account with duplicate name `%s'!", tok);
                continue;
            }

            if(!rb->strlen(tok))
            {
                rb->splashf(HZ * 2, "Skipping account with empty name.");
                continue;
            }

            rb->strlcpy(accounts[next_slot].name, tok, sizeof(accounts[next_slot].name));

            bool have_secret = false;

            do {
                tok = rb->strtok_r(NULL, "=", &save);
                if(!tok)
                    continue;

                char* tok_val = rb->strtok_r(NULL, "&", &save);
                if(!tok_val)
                    continue;

                if(!rb->strcmp(tok, "secret"))
                {
                    if(have_secret)
                    {
                        rb->splashf(HZ * 2, "URI with multiple `secret' parameters found, skipping!");
                        goto fail;
                    }
                    have_secret = true;
                    if((accounts[next_slot].sec_len = base32_decode(accounts[next_slot].secret, SECRET_MAX, tok_val)) <= 0)
                        goto fail;
                }
                else if(!rb->strcmp(tok, "counter"))
                {
                    if(accounts[next_slot].is_totp)
                    {
                        rb->splash(HZ * 2, "Counter parameter specified for TOTP!? Skipping...");
                        goto fail;
                    }
                    accounts[next_slot].hotp_counter = rb->atoi(tok_val);
                }
                else if(!rb->strcmp(tok, "period"))
                {
                    if(!accounts[next_slot].is_totp)
                    {
                        rb->splash(HZ * 2, "Period parameter specified for HOTP!? Skipping...");
                        goto fail;
                    }
                    accounts[next_slot].totp_period = rb->atoi(tok_val);
                }
                else if(!rb->strcmp(tok, "digits"))
                {
                    accounts[next_slot].digits = rb->atoi(tok_val);
                    if(accounts[next_slot].digits < 1 || accounts[next_slot].digits > 9)
                    {
                        rb->splashf(HZ * 2, "Digits parameter not in acceptable range, skipping.");
                        goto fail;
                    }
                }
                else
                    rb->splashf(HZ, "Unnown parameter `%s' ignored.", tok);
            } while(tok);

            if(!have_secret)
            {
                rb->splashf(HZ * 2, "URI with NO `secret' parameter found, skipping!");
                goto fail;
            }

            ++next_slot;

        fail:

            ;
        } while(1);
        rb->close(fd);
    }
    if(before == next_slot)
        rb->splash(HZ * 2, "No accounts added.");
    else
    {
        rb->splashf(HZ * 2, "Added %d account(s).", next_slot - before);
        save_accts();
    }
}

static void add_acct_manual(void)
{
    if(next_slot >= max_accts)
    {
        rb->splashf(HZ * 2, "Account limit reached!");
        return;
    }
    memset(accounts + next_slot, 0, sizeof(struct account_t));

    rb->splash(HZ * 1, "Enter account name.");
    char* buf = accounts[next_slot].name;
    if(rb->kbd_input(buf, sizeof(accounts[next_slot].name), NULL) < 0)
        return;

    if(acct_exists(buf))
    {
        rb->splash(HZ * 2, "Duplicate account name!");
        return;
    }

    rb->splash(HZ * 2, "Enter base32-encoded secret.");

    char temp_buf[SECRET_MAX * 2];
    memset(temp_buf, 0, sizeof(temp_buf));

    if(rb->kbd_input(temp_buf, sizeof(temp_buf), NULL) < 0)
        return;

    if((accounts[next_slot].sec_len = base32_decode(accounts[next_slot].secret, SECRET_MAX, temp_buf)) <= 0)
    {
        rb->splash(HZ * 2, "Invalid Base32 secret!");
        return;
    }

#if CONFIG_RTC
    const struct text_message prompt = { (const char*[]) {"Is this a TOTP account?", "The protocol can be determined from the URI."}, 2};
    enum yesno_res response = rb->gui_syncyesno_run(&prompt, NULL, NULL);
    if(response == YESNO_NO)
        accounts[next_slot].is_totp = false;
    else
        accounts[next_slot].is_totp = true;
#endif

    memset(temp_buf, 0, sizeof(temp_buf));

    if(!accounts[next_slot].is_totp)
    {
        rb->splash(HZ * 2, "Enter counter (0 is normal).");
        temp_buf[0] = '0';
    }
    else
    {
        rb->splash(HZ * 2, "Enter time step (30 is normal).");
        temp_buf[0] = '3';
        temp_buf[1] = '0';
    }

    if(rb->kbd_input(temp_buf, sizeof(temp_buf), NULL) < 0)
        return;

    if(!accounts[next_slot].is_totp)
        accounts[next_slot].hotp_counter = rb->atoi(temp_buf);
    else
        accounts[next_slot].totp_period = rb->atoi(temp_buf);

    rb->splash(HZ * 2, "Enter code length (6 is normal).");

    memset(temp_buf, 0, sizeof(temp_buf));
    temp_buf[0] = '6';

    if(rb->kbd_input(temp_buf, sizeof(temp_buf), NULL) < 0)
        return;

    accounts[next_slot].digits = rb->atoi(temp_buf);

    if(accounts[next_slot].digits < 1 || accounts[next_slot].digits > 9)
    {
        rb->splash(HZ, "Invalid length!");
        return;
    }

    ++next_slot;

    save_accts();

    rb->splashf(HZ * 2, "Success.");
}

static void add_acct(void)
{
    MENUITEM_STRINGLIST(menu, "Add Account", NULL,
                        "From URI on disk",
                        "Manual Entry",
                        "Back");
    int sel = 0;
    bool quit = false;
    while(!quit)
    {
        switch(rb->do_menu(&menu, &sel, NULL, false))
        {
        case 0:
            add_acct_file();
            break;
        case 1:
            add_acct_manual();
            break;
        case 2:
        default:
            quit = true;
            break;
        }
    }
}

static void show_code(int acct)
{
    if(!accounts[acct].is_totp)
    {
        rb->splashf(0, "%0*d", accounts[acct].digits,
                               HOTP(accounts[acct].secret,
                                    accounts[acct].sec_len,
                                    accounts[acct].hotp_counter,
                                    accounts[acct].digits));
        ++accounts[acct].hotp_counter;
    }
#if CONFIG_RTC
    else
    {
        rb->splashf(0, "%0*d (%lu seconds(s) left)", accounts[acct].digits,
                                                     TOTP(accounts[acct].secret,
                                                          accounts[acct].sec_len,
                                                          accounts[acct].totp_period,
                                                          accounts[acct].digits),
                    accounts[acct].totp_period - (unsigned long) get_utc() % accounts[acct].totp_period);
    }
#else
    else
    {
        rb->splash(0, "TOTP not supported on this device!");
    }
#endif
    while(1)
    {
        int button = rb->button_get(true);
        if(button && !(button & BUTTON_REL))
            break;
        rb->yield();
    }

    save_accts();
    rb->lcd_clear_display();
}

static void gen_codes(void)
{
    rb->lcd_clear_display();
    /* native menus don't seem to support dynamic names easily, so we
     * roll our own */
    static const struct button_mapping *plugin_contexts[] = { pla_main_ctx };
    int idx = 0;
    if(next_slot > 0)
    {
        rb->lcd_putsf(0, 0, "Generate Code");
        rb->lcd_putsf(0, 1, "%s", accounts[0].name);
        rb->lcd_update();
    }
    else
    {
        rb->splash(HZ * 2, "No accounts configured!");
        return;
    }
    while(1)
    {
        int button = pluginlib_getaction(-1, plugin_contexts, ARRAYLEN(plugin_contexts));
        switch(button)
        {
        case PLA_LEFT:
            --idx;
            if(idx < 0)
                idx = next_slot - 1;
            break;
        case PLA_RIGHT:
            ++idx;
            if(idx >= next_slot)
                idx = 0;
            break;
        case PLA_SELECT:
            show_code(idx);
            break;
        case PLA_CANCEL:
        case PLA_EXIT:
            exit_on_usb(button);
            return;
        default:
            break;
        }
        rb->lcd_clear_display();
        rb->lcd_putsf(0, 0, "Generate Code");
        rb->lcd_putsf(0, 1, "%s", accounts[idx].name);
        rb->lcd_update();
    }
}

static bool danger_confirm(void)
{
    int sel = 0;
    MENUITEM_STRINGLIST(menu, "Are you REALLY SURE?", NULL,
                        "No",
                        "No",
                        "No",
                        "No",
                        "No",
                        "No",
                        "No",
                        "Yes, DO IT", // 7
                        "No",
                        "No",
                        "No",
                        "No");

    switch(rb->do_menu(&menu, &sel, NULL, false))
    {
    case 7:
        return true;
    default:
        return false;
    }
}

char data_buf[MAX(MAX_NAME, SECRET_MAX * 2)];
char temp_sec[SECRET_MAX];
size_t old_len;

static void edit_menu(int acct)
{
    rb->splashf(HZ, "Editing account `%s'.", accounts[acct].name);

    /* HACK ALERT */
    /* two different menus, one handling logic */
    MENUITEM_STRINGLIST(menu_1, "Edit Account", NULL,
                        "Rename",
                        "Delete",
                        "Change HOTP Counter",
                        "Change Digit Count",
                        "Change Shared Secret",
                        "Back");

    MENUITEM_STRINGLIST(menu_2, "Edit Account", NULL,
                        "Rename", // 0
                        "Delete", // 1
                        "Change TOTP Period", // 2
                        "Change Digit Count", // 3
                        "Change Shared Secret", // 4
                        "Back"); // 5

    const struct menu_item_ex *menu = (accounts[acct].is_totp) ? &menu_2 : &menu_1;

    bool quit = false;
    int sel = 0;
    while(!quit)
    {
        switch(rb->do_menu(menu, &sel, NULL, false))
        {
        case 0: // rename
            rb->splash(HZ, "Enter new name.");
            rb->strlcpy(data_buf, accounts[acct].name, sizeof(data_buf));
            if(rb->kbd_input(data_buf, sizeof(data_buf), NULL) < 0)
                break;
            if(acct_exists(data_buf))
            {
                rb->splash(HZ * 2, "Duplicate account name!");
                break;
            }
            rb->strlcpy(accounts[acct].name, data_buf, sizeof(accounts[acct].name));
            save_accts();
            break;
        case 1: // delete
            if(danger_confirm())
            {
                rb->memmove(accounts + acct, accounts + acct + 1, (next_slot - acct - 1) * sizeof(struct account_t));
                --next_slot;
                save_accts();
                rb->splashf(HZ, "Deleted.");
                return;
            }
            else
                rb->splash(HZ, "Not confirmed.");
            break;
        case 2: // HOTP counter OR TOTP period
            if(accounts[acct].is_totp)
                rb->snprintf(data_buf, sizeof(data_buf), "%d", (int)accounts[acct].hotp_counter);
            else
                rb->snprintf(data_buf, sizeof(data_buf), "%lu", accounts[acct].totp_period);

            if(rb->kbd_input(data_buf, sizeof(data_buf), NULL) < 0)
                break;

            if(accounts[acct].is_totp)
                accounts[acct].totp_period = rb->atoi(data_buf);
            else
                accounts[acct].hotp_counter = rb->atoi(data_buf);

            save_accts();

            rb->splash(HZ, "Success.");
            break;
        case 3: // digits
            rb->snprintf(data_buf, sizeof(data_buf), "%d", accounts[acct].digits);
            if(rb->kbd_input(data_buf, sizeof(data_buf), NULL) < 0)
                break;

            accounts[acct].digits = rb->atoi(data_buf);
            save_accts();

            rb->splash(HZ, "Success.");
            break;
        case 4: // secret
            old_len = accounts[acct].sec_len;
            memcpy(temp_sec, accounts[acct].secret, accounts[acct].sec_len);
            base32_encode(accounts[acct].secret, accounts[acct].sec_len, data_buf, sizeof(data_buf));

            if(rb->kbd_input(data_buf, sizeof(data_buf), NULL) < 0)
                break;

            int ret = base32_decode(accounts[acct].secret, sizeof(accounts[acct].secret), data_buf);
            if(ret <= 0)
            {
                memcpy(accounts[acct].secret, temp_sec, SECRET_MAX);
                accounts[acct].sec_len = old_len;
                rb->splash(HZ * 2, "Invalid Base32 secret!");
                break;
            }
            accounts[acct].sec_len = ret;

            save_accts();
            rb->splash(HZ, "Success.");
            break;
        case 5:
            quit = true;
            break;
        default:
            break;
        }
    }
}

static void edit_accts(void)
{
    rb->lcd_clear_display();
    /* native menus don't seem to support dynamic names easily, so we
     * roll our own */
    static const struct button_mapping *plugin_contexts[] = { pla_main_ctx };
    int idx = 0;
    if(next_slot > 0)
    {
        rb->lcd_putsf(0, 0, "Edit Account");
        rb->lcd_putsf(0, 1, "%s", accounts[0].name);
        rb->lcd_update();
    }
    else
    {
        rb->splash(HZ * 2, "No accounts configured!");
        return;
    }
    while(1)
    {
        int button = pluginlib_getaction(-1, plugin_contexts, ARRAYLEN(plugin_contexts));
        switch(button)
        {
        case PLA_LEFT:
            --idx;
            if(idx < 0)
                idx = next_slot - 1;
            break;
        case PLA_RIGHT:
            ++idx;
            if(idx >= next_slot)
                idx = 0;
            break;
        case PLA_SELECT:
            edit_menu(idx);
            if(!next_slot)
                return;
            if(idx == next_slot)
                idx = 0;
            break;
        case PLA_CANCEL:
        case PLA_EXIT:
            return;
        default:
            exit_on_usb(button);
            break;
        }
        rb->lcd_clear_display();
        rb->lcd_putsf(0, 0, "Edit Account");
        rb->lcd_putsf(0, 1, "%s", accounts[idx].name);
        rb->lcd_update();
    }
}

#if CONFIG_RTC

/* label is like this: [+/-]HH:MM ... */
static int get_time_seconds(const char *label)
{
    if(!rb->strcmp(label, "UTC"))
        return 0;

    char buf[32];

    /* copy the part after "UTC" */
    rb->strlcpy(buf, label + 3, sizeof(buf));

    char *save, *tok;

    tok = rb->strtok_r(buf, ":", &save);
    /* positive or negative: sign left */
    int hr = rb->atoi(tok);

    tok = rb->strtok_r(NULL, ": ", &save);
    int min = rb->atoi(tok);

    return 3600 * hr + 60 * min;
}

/* returns the offset in seconds associated with a time zone */
static int get_time_offs(void)
{
    MENUITEM_STRINGLIST(menu, "Select Time Offset", NULL,
                        "UTC-12:00", // 0
                        "UTC-11:00", // 1
                        "UTC-10:00 (HAST)", // 2
                        "UTC-9:30",  // 3
                        "UTC-9:00 (AKST, HADT)", // 4
                        "UTC-8:00 (PST, AKDT)", // 5
                        "UTC-7:00 (MST, PDT)", // 6
                        "UTC-6:00 (CST, MDT)", // 7
                        "UTC-5:00 (EST, CDT)", // 8
                        "UTC-4:00 (AST, EDT)", // 9
                        "UTC-3:30 (NST)", // 10
                        "UTC-3:00 (ADT)", // 11
                        "UTC-2:30 (NDT)", // 12
                        "UTC-2:00", // 13
                        "UTC-1:00", // 14
                        "UTC",      // 15
                        "UTC+1:00", // 16
                        "UTC+2:00", // 17
                        "UTC+3:00", // 18
                        "UTC+3:30", // 19
                        "UTC+4:00", // 20
                        "UTC+4:30", // 21
                        "UTC+5:00", // 22
                        "UTC+5:30", // 23
                        "UTC+5:45", // 24
                        "UTC+6:00", // 25
                        "UTC+6:30", // 26
                        "UTC+7:00", // 27
                        "UTC+8:00", // 28
                        "UTC+8:30", // 29
                        "UTC+8:45", // 30
                        "UTC+9:00", // 31
                        "UTC+9:30", // 32
                        "UTC+10:00", // 33
                        "UTC+10:30", // 34
                        "UTC+11:00", // 35
                        "UTC+12:00", // 36
                        "UTC+12:45", // 37
                        "UTC+13:00", // 38
                        "UTC+14:00", // 39
        );

    int sel = 0;
    for(unsigned int i = 0; i < ARRAYLEN(menu_); ++i)
        if(time_offs == get_time_seconds(menu_[i]))
        {
            sel = i;
            break;
        }

    /* relies on menu internals */
    rb->do_menu(&menu, &sel, NULL, false);

    /* see apps/menu.h */
    const char *label = menu_[sel];

    return get_time_seconds(label);

#if 0
    /* provided in case menu internals change */
    switch(rb->do_menu(&menu, &sel, NULL, false))
    {
    case 0: case 1: case 2:
        return (sel - 12) * 3600;
    case 3:
        return -9 * 3600 - 30 * 60;
    case 4: case 5: case 6: case 7: case 8: case 9:
        return (sel - 13) * 3600;
    case 10:
        return -3 * 3600 - 30 * 60;
    case 11:
        return -3 * 3600;
    case 12:
        return -3 * 3600 - 30 * 60;
    case 13: case 14: case 15: case 16: case 17: case 18:
        return (sel - 15) * 3600;

    case 19:
        return 3 * 3600 + 30 * 60;
    case 20:
        return 4 * 3600;
    case 21:
        return 4 * 3600 + 30 * 60;
    case 22:
        return 5 * 3600;
    case 23:
        return 5 * 3600 + 30 * 60;
    case 24:
        return 5 * 3600 + 45 * 60;
    case 25:
        return 6 * 3600;
    case 26:
        return 6 * 3600 + 30 * 60;
    case 27: case 28:
        return (sel - 20) * 3600;
    case 29:
        return 8 * 3600 + 30 * 60;
    case 30:
        return 8 * 3600 + 45 * 60;
    case 31:
        return 9 * 3600;
    case 32:
        return 9 * 3600 + 30 * 60;
    case 33:
        return 10 * 3600;
    case 34:
        return 10 * 3600 + 30 * 60;
    case 35: case 36:
        return (sel - 24) * 3600;
    case 37:
        return 12 * 3600 + 45 * 60;
    case 38: case 39:
        return (sel - 25) * 3600;
    default:
        rb->splash(0, "BUG: time zone fall-through: REPORT ME!!!");
        break;
    }
    return 0;
#endif
}
#endif

static void adv_menu(void)
{
    MENUITEM_STRINGLIST(menu, "Advanced", NULL,
                        "Edit Account",
                        "Delete ALL accounts",
#if CONFIG_RTC
                        "Change Time Offset",
#endif
                        "Back");

    bool quit = false;
    int sel = 0;
    while(!quit)
    {
        switch(rb->do_menu(&menu, &sel, NULL, false))
        {
        case 0:
            edit_accts();
            break;
        case 1:
            if(danger_confirm())
            {
                next_slot = 0;
                save_accts();
                rb->splash(HZ, "It is done, my master.");
            }
            else
                rb->splash(HZ, "Not confirmed.");
            break;
#if CONFIG_RTC
        case 2:
            time_offs = get_time_offs();
            break;
        case 3:
#else
        case 2:
#endif
            quit = 1;
            break;
        default:
            break;
        }
    }
}

/* displays the help text */
static void show_help(void)
{

#ifdef HAVE_LCD_COLOR
    rb->lcd_set_foreground(LCD_WHITE);
    rb->lcd_set_background(LCD_BLACK);
#endif

    rb->lcd_setfont(FONT_UI);

    static char *help_text[] = { "One-Time Password Manager", "",
                                 "Introduction", "",
                                 "This", "plugin", "allows", "you", "to", "generate", "one-time", "passwords", "to", "provide", "a", "second", "factor", "of", "authentication", "for", "services", "that", "support", "it.",
                                 "It", "suppports", "both", "event-based", "(HOTP),", "and", "time-based", "(TOTP)", "password", "schemes.",
                                 "In", "order", "to", "ensure", "proper", "functioning", "of", "time-based", "passwords", "ensure", "that", "the", "clock", "is", "accurate", "to", "within", "30", "seconds", "of", "actual", "time.",
                                 "Note", "that", "some", "devices", "lack", "a", "real-time", "clock,", "so", "time-based", "passwords", "are", "not", "supported", "on", "those", "targets." };

    struct style_text style[] = {
        {0,  TEXT_CENTER | TEXT_UNDERLINE},
        {2,  C_RED},
        LAST_STYLE_ITEM
    };

    display_text(ARRAYLEN(help_text), help_text, style, NULL, true);
}

/* this is the plugin entry point */
enum plugin_status plugin_start(const void* parameter)
{
    (void)parameter;

    /* self-test with RFC 4226 values */
    if(HOTP("12345678901234567890", rb->strlen("12345678901234567890"), 1, 6) != 287082)
    {
        return PLUGIN_ERROR;
    }

    size_t bufsz;
    accounts = rb->plugin_get_buffer(&bufsz);
    max_accts = bufsz / sizeof(struct account_t);

    if(!read_accts())
#if CONFIG_RTC
    {
        time_offs = get_time_offs();
    }
#else
    {
        ;
    }
#endif

    MENUITEM_STRINGLIST(menu, "One-Time Password Manager", NULL,
                        "Generate Code",
                        "Add Account",
                        "Help",
                        "Advanced",
                        "Quit");

    bool quit = false;
    int sel = 0;
    while(!quit)
    {
        switch(rb->do_menu(&menu, &sel, NULL, false))
        {
        case 0:
            gen_codes();
            break;
        case 1:
            add_acct();
            break;
        case 2:
            show_help();
            break;
        case 3:
            adv_menu();
            break;
        case 4:
            quit = 1;
            break;
        default:
            break;
        }
    }

    /* save to disk */
    save_accts();

    /* tell Rockbox that we have completed successfully */
    return PLUGIN_OK;
}
