/***************************************************************************
 *             __________               __   ___.
 *   Open      \______   \ ____   ____ |  | _\_ |__   _______  ___
 *   Source     |       _//  _ \_/ ___\|  |/ /| __ \ /  _ \  \/  /
 *   Jukebox    |    |   (  <_> )  \___|    < | \_\ (  <_> > <  <
 *   Firmware   |____|_  /\____/ \___  >__|_ \|___  /\____/__/\_ \
 *                     \/            \/     \/    \/            \/
 * $Id$
 *
 * Copyright (C) 2007 Jens Arnold
 *
 * 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.
 *
 ****************************************************************************/

#include "plugin.h"
#include "lib/helper.h"

PLUGIN_HEADER

#define TESTBASEDIR "/__TEST__"
#define TEST_FILE   TESTBASEDIR "/test_disk.tmp"
#define FRND_SEED   0x78C3     /* arbirary */

#if (CONFIG_STORAGE & STORAGE_MMC)
#define TEST_SIZE (20*1024*1024)
#else
#define TEST_SIZE (300*1024*1024)
#endif
#define TEST_TIME 10 /* in seconds */

static unsigned char* audiobuf;
static ssize_t audiobuflen;

static unsigned short frnd_buffer;
static int line = 0;
static int max_line = 0;
static int log_fd;
static char logfilename[MAX_PATH];
static const char testbasedir[] = TESTBASEDIR;

static void mem_fill_frnd(unsigned char *addr, int len)
{
    unsigned char *end = addr + len;
    unsigned random = frnd_buffer;

    while (addr < end)
    {
        random = 75 * random + 74;
        *addr++ = random >> 8;
    }
    frnd_buffer = random;
}

static bool mem_cmp_frnd(unsigned char *addr, int len)
{
    unsigned char *end = addr + len;
    unsigned random = frnd_buffer;

    while (addr < end)
    {
        random = 75 * random + 74;
        if (*addr++ != ((random >> 8) & 0xff))
            return false;
    }
    frnd_buffer = random;
    return true;
}

static bool log_init(void)
{
    int h;

    rb->lcd_getstringsize("A", NULL, &h);
    max_line = LCD_HEIGHT / h;
    line = 0;
    rb->lcd_clear_display();
    rb->lcd_update();
    
    rb->create_numbered_filename(logfilename, "/", "test_disk_log_", ".txt",
                                 2 IF_CNFN_NUM_(, NULL));
    log_fd = rb->open(logfilename, O_RDWR|O_CREAT|O_TRUNC, 0666);
    return log_fd >= 0;
}

static void log_text(char *text, bool advance)
{
    rb->lcd_puts(0, line, text);
    rb->lcd_update();
    if (advance)
    {
        if (++line >= max_line)
            line = 0;
        rb->fdprintf(log_fd, "%s\n", text);
    }
}

static void log_close(void)
{
    rb->close(log_fd);
}

static bool test_fs(void)
{
    unsigned char text_buf[32];
    int total, current, align;
    int fd, ret;

    log_init();
    log_text("test_disk WRITE&VERIFY", true);
#if (CONFIG_PLATFORM & PLATFORM_NATIVE)
    rb->snprintf(text_buf, sizeof(text_buf), "CPU clock: %ld Hz",
                 *rb->cpu_frequency);
    log_text(text_buf, true);
#endif
    log_text("----------------------", true);
    rb->snprintf(text_buf, sizeof text_buf, "Data size: %dKB", (TEST_SIZE>>10));
    log_text(text_buf, true);

    fd = rb->creat(TEST_FILE, 0666);
    if (fd < 0)
    {
        rb->splashf(HZ, "creat() failed: %d", fd);
        goto error;
    }

    frnd_buffer = FRND_SEED;
    total = TEST_SIZE;
    while (total > 0)
    {
        align = rb->rand() & 0xf;
        current = rb->rand() % (audiobuflen - align);
        current = MIN(current, total);
        rb->snprintf(text_buf, sizeof text_buf, "Wrt %dKB, %dKB left",
                     current >> 10, total >> 10);
        log_text(text_buf, false);

        mem_fill_frnd(audiobuf + align, current);
        ret = rb->write(fd, audiobuf + align, current);
        if (current != ret)
        {
            rb->splashf(0, "write() failed: %d/%d", ret, current);
            rb->close(fd);
            goto error;
        }
        total -= current;
    }
    rb->close(fd);

    fd = rb->open(TEST_FILE, O_RDONLY);
    if (fd < 0)
    {
        rb->splashf(0, "open() failed: %d", ret);
        goto error;
    }

    frnd_buffer = FRND_SEED;
    total = TEST_SIZE;
    while (total > 0)
    {
        align = rb->rand() & 0xf;
        current = rb->rand() % (audiobuflen - align);
        current = MIN(current, total);
        rb->snprintf(text_buf, sizeof text_buf, "Cmp %dKB, %dKB left",
                     current >> 10, total >> 10);
        log_text(text_buf, false);

        ret = rb->read(fd, audiobuf + align, current);
        if (current != ret)
        {
            rb->splashf(0, "read() failed: %d/%d", ret, current);
            rb->close(fd);
            goto error;
        }
        if (!mem_cmp_frnd(audiobuf + align, current))
        {
            log_text(text_buf, true);
            log_text("Compare error.", true);
            rb->close(fd);
            goto error;
        }
        total -= current;
    }
    rb->close(fd);
    log_text(text_buf, true);
    log_text("Test passed.", true);

error:
    log_close();
    rb->remove(TEST_FILE);
    rb->button_clear_queue();
    rb->button_get(true);

    return false;
}

static bool file_speed(int chunksize, bool align)
{
    unsigned char text_buf[64];
    int fd, ret;
    long filesize = 0;
    long size, time;
    
    if (chunksize >= audiobuflen)
        return false;

    log_text("--------------------", true);

    /* File creation write speed */
    fd = rb->creat(TEST_FILE, 0666);
    if (fd < 0)
    {
        rb->splashf(HZ, "creat() failed: %d", fd);
        goto error;
    }
    time = *rb->current_tick;
    while (TIME_BEFORE(*rb->current_tick, time + TEST_TIME*HZ))
    {
        ret = rb->write(fd, audiobuf + (align ? 0 : 1), chunksize);
        if (chunksize != ret)
        {
            rb->splashf(HZ, "write() failed: %d/%d", ret, chunksize);
            rb->close(fd);
            goto error;
        }
        filesize += chunksize;
    }
    time = *rb->current_tick - time;
    rb->close(fd);
    rb->snprintf(text_buf, sizeof text_buf, "Create (%d,%c): %ld KB/s",
                 chunksize, align ? 'A' : 'U', (25 * (filesize>>8) / time) );
    log_text(text_buf, true);

    /* Existing file write speed */
    fd = rb->open(TEST_FILE, O_WRONLY);
    if (fd < 0)
    {
        rb->splashf(0, "open() failed: %d", fd);
        goto error;
    }
    time = *rb->current_tick;
    for (size = filesize; size > 0; size -= chunksize)
    {
        ret = rb->write(fd, audiobuf + (align ? 0 : 1), chunksize);
        if (chunksize != ret)
        {
            rb->splashf(0, "write() failed: %d/%d", ret, chunksize);
            rb->close(fd);
            goto error;
        }
    }
    time = *rb->current_tick - time;
    rb->close(fd);
    rb->snprintf(text_buf, sizeof text_buf, "Write  (%d,%c): %ld KB/s",
                 chunksize, align ? 'A' : 'U', (25 * (filesize>>8) / time) );
    log_text(text_buf, true);
    
    /* File read speed */
    fd = rb->open(TEST_FILE, O_RDONLY);
    if (fd < 0)
    {
        rb->splashf(0, "open() failed: %d", fd);
        goto error;
    }
    time = *rb->current_tick;
    for (size = filesize; size > 0; size -= chunksize)
    {
        ret = rb->read(fd, audiobuf + (align ? 0 : 1), chunksize);
        if (chunksize != ret)
        {
            rb->splashf(0, "read() failed: %d/%d", ret, chunksize);
            rb->close(fd);
            goto error;
        }
    }
    time = *rb->current_tick - time;
    rb->close(fd);
    rb->snprintf(text_buf, sizeof text_buf, "Read   (%d,%c): %ld KB/s",
                 chunksize, align ? 'A' : 'U', (25 * (filesize>>8) / time) );
    log_text(text_buf, true);
    rb->remove(TEST_FILE);
    return true;

  error:
    rb->remove(TEST_FILE);
    return false;
}

static bool test_speed(void)
{
    unsigned char text_buf[64];
    DIR *dir = NULL;
    struct dirent *entry = NULL;
    int fd, last_file;
    int i, n;
    long time;

    rb->memset(audiobuf, 'T', audiobuflen);
    log_init();
    log_text("test_disk SPEED TEST", true);
#if (CONFIG_PLATFORM & PLATFORM_NATIVE)
    rb->snprintf(text_buf, sizeof(text_buf), "CPU clock: %ld Hz",
                 *rb->cpu_frequency);
    log_text(text_buf, true);
#endif
    log_text("--------------------", true);

    /* File creation speed */
    time = *rb->current_tick + TEST_TIME*HZ;
    for (i = 0; TIME_BEFORE(*rb->current_tick, time); i++)
    {
        rb->snprintf(text_buf, sizeof(text_buf), TESTBASEDIR "/%08x.tmp", i);
        fd = rb->creat(text_buf, 0666);
        if (fd < 0)
        {
            last_file = i;
            rb->splashf(HZ, "creat() failed: %d", fd);
            goto error;
        }
        rb->close(fd);
    }
    last_file = i;
    rb->snprintf(text_buf, sizeof(text_buf), "Create:  %d files/s",
                 last_file / TEST_TIME);
    log_text(text_buf, true);
    
    /* File open speed */
    time = *rb->current_tick + TEST_TIME*HZ;
    for (n = 0, i = 0; TIME_BEFORE(*rb->current_tick, time); n++, i++)
    {
        if (i >= last_file)
            i = 0;
        rb->snprintf(text_buf, sizeof(text_buf), TESTBASEDIR "/%08x.tmp", i);
        fd = rb->open(text_buf, O_RDONLY);
        if (fd < 0)
        {
            rb->splashf(HZ, "open() failed: %d", fd);
            goto error;
        }
        rb->close(fd);
    }
    rb->snprintf(text_buf, sizeof(text_buf), "Open:    %d files/s", n / TEST_TIME);
    log_text(text_buf, true);

    /* Directory scan speed */
    time = *rb->current_tick + TEST_TIME*HZ;
    for (n = 0; TIME_BEFORE(*rb->current_tick, time); n++)
    {
        if (entry == NULL)
        {
            if (dir != NULL)
                rb->closedir(dir);
            dir = rb->opendir(testbasedir);
            if (dir == NULL)
            {
                rb->splash(HZ, "opendir() failed.");
                goto error;
            }
        }
        entry = rb->readdir(dir);
    }
    rb->closedir(dir);
    rb->snprintf(text_buf, sizeof(text_buf), "Dirscan: %d files/s", n / TEST_TIME);
    log_text(text_buf, true);

    /* File delete speed */
    time = *rb->current_tick;
    for (i = 0; i < last_file; i++)
    {
        rb->snprintf(text_buf, sizeof(text_buf), TESTBASEDIR "/%08x.tmp", i);
        rb->remove(text_buf);
    }
    rb->snprintf(text_buf, sizeof(text_buf), "Delete:  %ld files/s",
                 last_file * HZ / (*rb->current_tick - time));
    log_text(text_buf, true);
    
    if (file_speed(512, true)
        && file_speed(512, false)
        && file_speed(4096, true)
        && file_speed(4096, false)
        && file_speed(1048576, true))
        file_speed(1048576, false);

    log_text("DONE", false);
    log_close();
    rb->button_clear_queue();
    rb->button_get(true);
    return false;

  error:
    for (i = 0; i < last_file; i++)
    {
        rb->snprintf(text_buf, sizeof(text_buf), TESTBASEDIR "/%08x.tmp", i);
        rb->remove(text_buf);
    }
    log_text("DONE", false);
    log_close();
    rb->button_clear_queue();
    rb->button_get(true);
    return false;
}


/* this is the plugin entry point */
enum plugin_status plugin_start(const void* parameter)
{
    MENUITEM_STRINGLIST(menu, "Test Disk Menu", NULL,
                        "Disk speed", "Write & verify");
    int selected=0;
    bool quit = false;
    int align;
    DIR *dir;

    (void)parameter;

    if ((dir = rb->opendir(testbasedir)) == NULL)
    {
        if (rb->mkdir(testbasedir) < 0)
        {
            rb->splash(HZ*2, "Can't create test directory.");
            return PLUGIN_ERROR;
        }
    }
    else
    {
        rb->closedir(dir);
    }

    audiobuf = rb->plugin_get_audio_buffer((size_t *)&audiobuflen);
    /* align start and length to 32 bit */
    align = (-(int)audiobuf) & 3;
    audiobuf += align;
    audiobuflen = (audiobuflen - align) & ~3;

    rb->srand(*rb->current_tick);

    /* Turn off backlight timeout */
    backlight_force_on(); /* backlight control in lib/helper.c */

    while(!quit)
    {
        switch(rb->do_menu(&menu, &selected, NULL, false))
        {
            case 0:
                test_speed();
                break;
            case 1:
                test_fs();
                break;
            default:
                quit = true;
                break;
        }
    }

    /* Turn on backlight timeout (revert to settings) */
    backlight_use_settings(); /* backlight control in lib/helper.c */
    
    rb->rmdir(testbasedir);

    return PLUGIN_OK;
}
