/***************************************************************************
 *             __________               __   ___.
 *   Open      \______   \ ____   ____ |  | _\_ |__   _______  ___
 *   Source     |       _//  _ \_/ ___\|  |/ /| __ \ /  _ \  \/  /
 *   Jukebox    |    |   (  <_> )  \___|    < | \_\ (  <_> > <  <
 *   Firmware   |____|_  /\____/ \___  >__|_ \|___  /\____/__/\_ \
 *                     \/            \/     \/    \/            \/
 *
 * Copyright (C) 2006 by Greg White
 *
 * 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 "config.h"
#include "system.h"
#include <stdio.h>
#include <errno.h>
#include "../kernel-internal.h"
#include "gcc_extensions.h"
#include "string.h"
#include "adc.h"
#include "powermgmt.h"
#include "storage.h"
#include "file_internal.h"
#include "dir.h"
#include "file.h"
#include "disk.h"
#include "common.h"
#include "rb-loader.h"
#include "loader_strerror.h"
#include "power.h"
#include "backlight.h"
#include "usb.h"
#include "button.h"
#include "font.h"
#include "lcd.h"
#include "version.h"

#define TAR_CHUNK 512
#define TAR_HEADER_SIZE 157

/* Where files sent via MTP are stored */
static const char basedir[] = "/Content/0b00/00/";
/* Can use memory after vector table up to 0x01f00000 */
static char * const tarbuf = (char *)0x00000040;
static const size_t tarbuf_size = 0x01f00000 - 0x00000040;
/* Firmware data */
static void * const load_buf = 0x00000000;
static const size_t load_buf_size = 0x20000000 - 0x100000;
static const void * const start_addr = 0x00000000;

/* Show a message + "Shutting down...", then power off the device */
static void display_message_and_power_off(int timeout, const char *msg)
{
    verbose = true;
    printf(msg);
    printf("Shutting down...");
    sleep(timeout);
    power_off();
}

static void check_battery_safe(void)
{
    if (battery_level_safe())
        return;

    display_message_and_power_off(HZ, "Battery low");
}

/* TODO: Handle charging while connected */
static void handle_usb(int connect_timeout)
{
    long end_tick = 0;
    int button;

    /* We need full button and backlight handling now */
    backlight_init();
    button_init();
    backlight_on();

    /* Start the USB driver */
    usb_init();
    usb_start_monitoring();

    /* Wait for threads to connect or cable is pulled */
    printf("USB: Connecting");

    if (connect_timeout != TIMEOUT_BLOCK)
        end_tick = current_tick + connect_timeout;

    while (1)
    {
        button = button_get_w_tmo(HZ/2);

        if (button == SYS_USB_CONNECTED)
            break; /* Hit */

        if (connect_timeout != TIMEOUT_BLOCK &&
            TIME_AFTER(current_tick, end_tick))
        {
            /* Timed out waiting for the connect - will happen when connected
             * to a charger through the USB port */
            printf("USB: Timed out");
            break;
        }

        if (usb_plugged() == USB_EXTRACTED)
            break; /* Cable pulled */
    }

    if (button == SYS_USB_CONNECTED)
    {
        /* Switch to verbose mode if not in it so that the status updates
         * are shown */
        verbose = true;
        /* Got the message - wait for disconnect */
        printf("Bootloader USB mode");

        usb_acknowledge(SYS_USB_CONNECTED_ACK, button_get_data());

        while (1)
        {
            button = button_get_w_tmo(HZ/2);
            if (button == SYS_USB_DISCONNECTED)
                break;

            check_battery_safe();
        }

        backlight_on();
        /* Sleep a little to let the backlight ramp up */
        sleep(HZ*5/4);
    }

    /* Put drivers initialized for USB connection into a known state */
    usb_close();
    button_close();
    backlight_close();
}

static void untar(int tar_fd)
{
    char header[TAR_HEADER_SIZE];
    char *ptr;
    char path[102];
    int fd, i;
    int ret;
    size_t size = filesize(tar_fd);

    if (size > tarbuf_size)
    {
        printf("tar file too large"); /* Paranoid but proper */
        return;
    }

    ret = read(tar_fd, tarbuf, filesize(tar_fd));
    if (ret < 0)
    {
        printf("couldn't read tar file (%d)", ret);
        return;
    }
    ptr = tarbuf;

    while (1)
    {
        memcpy(header, ptr, TAR_HEADER_SIZE);

        if (*header == '\0')  /* Check for EOF */
            break;

        /* Parse the size field */
        size = 0;
        for (i = 124 ; i < 124 + 11 ; i++) {
            size = (8 * size) + header[i] - '0';
        }

        /* Skip rest of header */
        ptr += TAR_CHUNK;

        /* Make the path absolute */
        strcpy(path, "/");
        strcat(path, header);

        if (header[156] == '0')  /* file */
        {
            int wc;

            fd = creat(path, 0666);
            if (fd < 0)
            {
                printf("failed to create file (%d)", fd);
            }
            else
            {
                wc = write(fd, ptr, size);
                if (wc < 0)
                {
                    printf("write failed (%d)", wc);
                    break;
                }
                close(fd);
            }
            ptr += (size + TAR_CHUNK-1) & (~(TAR_CHUNK-1));
        }
        else if (header[156] == '5')  /* directory */
        {
            int ret;

            /* Remove the trailing slash */
            if (path[strlen(path) - 1] == '/')
                path[strlen(path) - 1] = '\0';

            /* Create the dir */
            ret = mkdir(path);
            if (ret < 0 && errno != EEXIST)
            {
                printf("failed to create dir (%d)", ret);
            }
        }
    }
}

/* Look for a tar file or rockbox binary in the MTP directory */
static void handle_untar(void)
{
    char buf[MAX_PATH];
    char tarstring[6];
    char model[5];
    struct dirent* entry;
    DIR* dir;
    int fd;
    int rc;

    dir = opendir(basedir);

    while ((entry = readdir(dir)))
    {
        if (*entry->d_name == '.')
            continue;

        snprintf(buf, sizeof(buf), "%s%s", basedir, entry->d_name);
        fd = open(buf, O_RDONLY);

        if (fd < 0)
            continue;

        /* Check whether the file is a rockbox binary. */
        lseek(fd, 4, SEEK_SET);
        rc = read(fd, model, 4);
        if (rc == 4)
        {
            model[4] = 0;
            if (strcmp(model, "gigs") == 0)
            {
                verbose = true;
                printf("Found rockbox binary. Moving...");
                close(fd);
                int ret = rename(buf, BOOTDIR "/" BOOTFILE);
                printf("returned %d", ret);
                sleep(HZ);
                break;
            }
        }

        /* Check whether the file is a tar file. */
        lseek(fd, 257, SEEK_SET);
        rc = read(fd, tarstring, 5);
        if (rc == 5)
        {
            tarstring[5] = 0;
            if (strcmp(tarstring, "ustar") == 0)
            {
                verbose = true;
                printf("Found tar file. Unarchiving...");
                lseek(fd, 0, SEEK_SET);
                untar(fd);
                close(fd);
                printf("Removing tar file");
                remove(buf);
                break;
            }
        }

        close(fd);
    }
}

/* Try to load the firmware and run it */
static void NORETURN_ATTR handle_firmware_load(void)
{
    int rc = load_firmware(load_buf, BOOTFILE, load_buf_size);

    if(rc <= EFILE_EMPTY)
        error(EBOOTFILE, rc, true);

    /* Pause to look at messages */
    while (1)
    {
        int button = button_read_device();

        /* Ignore settings reset */
        if (button == BUTTON_NONE || button == BUTTON_MENU)
            break;

        sleep(HZ/5);

        check_battery_safe();

        /* If the disk powers off, the firmware will lock at startup */
        storage_spin();
    }

    /* Put drivers into a known state */
    button_close_device();
    storage_close();
    system_prepare_fw_start();

    commit_discard_idcache();
    asm volatile ("bx %0": : "r"(start_addr));

    /* Halt */
    while (1)
        core_idle();
}

void main(void)
{
    int rc;
    int batt;

    system_init();
    kernel_init();

    /* Keep button_device_init early to delay calls to button_read_device */
    button_init_device();

    lcd_init();
    font_init();
    show_logo();
    lcd_clear_display();

    if (button_hold())
        display_message_and_power_off(HZ, "Hold switch on");

    if (button_read_device() != BUTTON_NONE)
        verbose = true;

    printf("Gigabeat S Rockbox Bootloader");
    printf("Version %s", rbversion);

    adc_init();
    batt = _battery_voltage();
    printf("Battery: %d.%03d V", batt / 1000, batt % 1000);
    check_battery_safe();

    rc = storage_init();
    if(rc)
        error(EATA, rc, true);

    filesystem_init();

    rc = disk_mount_all();
    if (rc <= 0)
        error(EDISK, rc, true);

    printf("Init complete");

    /* Do USB first since a tar or binary could be added to the MTP directory
     * at the time and we can untar or move after unplugging. */
    if (usb_plugged() == USB_INSERTED)
        handle_usb(HZ*2);

    handle_untar();
    handle_firmware_load(); /* No return */
}
