/**
 * @file OTPChecker.c
 * @author Ambroz Bizjak <ambrop7@gmail.com>
 * 
 * @section LICENSE
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * 3. Neither the name of the author nor the
 *    names of its contributors may be used to endorse or promote products
 *    derived from this software without specific prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

#include <string.h>

#include <misc/balloc.h>

#include <security/OTPChecker.h>

static void OTPChecker_Table_Empty (OTPChecker *mc, struct OTPChecker_table *t);
static void OTPChecker_Table_AddOTP (OTPChecker *mc, struct OTPChecker_table *t, otp_t otp);
static void OTPChecker_Table_Generate (OTPChecker *mc, struct OTPChecker_table *t, OTPCalculator *calc, uint8_t *key, uint8_t *iv);
static int OTPChecker_Table_CheckOTP (OTPChecker *mc, struct OTPChecker_table *t, otp_t otp);

void OTPChecker_Table_Empty (OTPChecker *mc, struct OTPChecker_table *t)
{
    for (int i = 0; i < mc->num_entries; i++) {
        t->entries[i].avail = -1;
    }
}

void OTPChecker_Table_AddOTP (OTPChecker *mc, struct OTPChecker_table *t, otp_t otp)
{
    // calculate starting index
    int start_index = otp % mc->num_entries;
    
    // try indexes starting with the base position
    for (int i = 0; i < mc->num_entries; i++) {
        int index = bmodadd_int(start_index, i, mc->num_entries);
        struct OTPChecker_entry *entry = &t->entries[index];
        
        // if we find a free index, use it
        if (entry->avail < 0) {
            entry->otp = otp;
            entry->avail = 1;
            return;
        }
        
        // if we find a used index with the same mac,
        // use it by incrementing its count
        if (entry->otp == otp) {
            entry->avail++;
            return;
        }
    }
    
    // will never add more macs than we can hold
    ASSERT(0)
}

void OTPChecker_Table_Generate (OTPChecker *mc, struct OTPChecker_table *t, OTPCalculator *calc, uint8_t *key, uint8_t *iv)
{
    // calculate values
    otp_t *otps = OTPCalculator_Generate(calc, key, iv, 0);
    
    // empty table
    OTPChecker_Table_Empty(mc ,t);
    
    // add calculated values to table
    for (int i = 0; i < mc->num_otps; i++) {
        OTPChecker_Table_AddOTP(mc, t, otps[i]);
    }
}

int OTPChecker_Table_CheckOTP (OTPChecker *mc, struct OTPChecker_table *t, otp_t otp)
{
    // calculate starting index
    int start_index = otp % mc->num_entries;
    
    // try indexes starting with the base position
    for (int i = 0; i < mc->num_entries; i++) {
        int index = bmodadd_int(start_index, i, mc->num_entries);
        struct OTPChecker_entry *entry = &t->entries[index];
        
        // if we find an empty entry, there is no such mac
        if (entry->avail < 0) {
            return 0;
        }
        
        // if we find a matching entry, check its count
        if (entry->otp == otp) {
            if (entry->avail > 0) {
                entry->avail--;
                return 1;
            }
            return 0;
        }
    }
    
    // there are always empty slots
    ASSERT(0)
    return 0;
}

static void work_func (OTPChecker *mc)
{
    struct OTPChecker_table *table = &mc->tables[mc->next_table];
    OTPChecker_Table_Generate(mc, table, &mc->calc, mc->tw_key, mc->tw_iv);
}

static void work_done_handler (OTPChecker *mc)
{
    ASSERT(mc->tw_have)
    DebugObject_Access(&mc->d_obj);
    
    // free work
    BThreadWork_Free(&mc->tw);
    mc->tw_have = 0;
    
    // update next table number
    mc->next_table = bmodadd_int(mc->next_table, 1, mc->num_tables);
    
    // update number of used tables if not all are used yet
    if (mc->tables_used < mc->num_tables) {
        mc->tables_used++;
    }
    
    // call handler
    if (mc->handler) {
        mc->handler(mc->user);
        return;
    }
}

int OTPChecker_Init (OTPChecker *mc, int num_otps, int cipher, int num_tables, BThreadWorkDispatcher *twd)
{
    ASSERT(num_otps > 0)
    ASSERT(BEncryption_cipher_valid(cipher))
    ASSERT(num_tables > 0)
    
    // init arguments
    mc->num_otps = num_otps;
    mc->cipher = cipher;
    mc->num_tables = num_tables;
    mc->twd = twd;
    
    // set no handlers
    mc->handler = NULL;
    
    // set number of entries
    if (mc->num_otps > INT_MAX / 2) {
        goto fail0;
    }
    mc->num_entries = 2 * mc->num_otps;
    
    // set no tables used
    mc->tables_used = 0;
    mc->next_table = 0;
    
    // initialize calculator
    if (!OTPCalculator_Init(&mc->calc, mc->num_otps, cipher)) {
        goto fail0;
    }
    
    // allocate tables
    if (!(mc->tables = (struct OTPChecker_table *)BAllocArray(mc->num_tables, sizeof(mc->tables[0])))) {
        goto fail1;
    }
    
    // allocate entries
    if (!(mc->entries = (struct OTPChecker_entry *)BAllocArray2(mc->num_tables, mc->num_entries, sizeof(mc->entries[0])))) {
        goto fail2;
    }
    
    // initialize tables
    for (int i = 0; i < mc->num_tables; i++) {
        struct OTPChecker_table *table = &mc->tables[i];
        table->entries = mc->entries + (size_t)i * mc->num_entries;
        OTPChecker_Table_Empty(mc, table);
    }
    
    // have no work
    mc->tw_have = 0;
    
    DebugObject_Init(&mc->d_obj);
    return 1;
    
fail2:
    BFree(mc->tables);
fail1:
    OTPCalculator_Free(&mc->calc);
fail0:
    return 0;
}

void OTPChecker_Free (OTPChecker *mc)
{
    DebugObject_Free(&mc->d_obj);
    
    // free work
    if (mc->tw_have) {
        BThreadWork_Free(&mc->tw);
    }
    
    // free entries
    BFree(mc->entries);
    
    // free tables
    BFree(mc->tables);
    
    // free calculator
    OTPCalculator_Free(&mc->calc);
}

void OTPChecker_AddSeed (OTPChecker *mc, uint16_t seed_id, uint8_t *key, uint8_t *iv)
{
    ASSERT(mc->next_table >= 0)
    ASSERT(mc->next_table < mc->num_tables)
    DebugObject_Access(&mc->d_obj);
    
    // free existing work
    if (mc->tw_have) {
        BThreadWork_Free(&mc->tw);
    }
    
    // set table's seed ID
    mc->tables[mc->next_table].id = seed_id;
    
    // copy key and IV
    memcpy(mc->tw_key, key, BEncryption_cipher_key_size(mc->cipher));
    memcpy(mc->tw_iv, iv, BEncryption_cipher_block_size(mc->cipher));
    
    // start work
    BThreadWork_Init(&mc->tw, mc->twd, (BThreadWork_handler_done)work_done_handler, mc, (BThreadWork_work_func)work_func, mc);
    
    // set have work
    mc->tw_have = 1;
}

void OTPChecker_RemoveSeeds (OTPChecker *mc)
{
    DebugObject_Access(&mc->d_obj);
    
    // free existing work
    if (mc->tw_have) {
        BThreadWork_Free(&mc->tw);
        mc->tw_have = 0;
    }
    
    mc->tables_used = 0;
    mc->next_table = 0;
}

int OTPChecker_CheckOTP (OTPChecker *mc, uint16_t seed_id, otp_t otp)
{
    DebugObject_Access(&mc->d_obj);
    
    // try tables in reverse order
    for (int i = 1; i <= mc->tables_used; i++) {
        int table_index = bmodadd_int(mc->next_table, mc->num_tables - i, mc->num_tables);
        if (table_index == mc->next_table && mc->tw_have) {
            // ignore table that is being generated
            continue;
        }
        
        struct OTPChecker_table *table = &mc->tables[table_index];
        if (table->id == seed_id) {
            return OTPChecker_Table_CheckOTP(mc, table, otp);
        }
    }
    
    return 0;
}

void OTPChecker_SetHandlers (OTPChecker *mc, OTPChecker_handler handler, void *user)
{
    DebugObject_Access(&mc->d_obj);
    
    mc->handler = handler;
    mc->user = user;
}
