/***************************************************************************
 *             __________               __   ___.
 *   Open      \______   \ ____   ____ |  | _\_ |__   _______  ___
 *   Source     |       _//  _ \_/ ___\|  |/ /| __ \ /  _ \  \/  /
 *   Jukebox    |    |   (  <_> )  \___|    < | \_\ (  <_> > <  <
 *   Firmware   |____|_  /\____/ \___  >__|_ \|___  /\____/__/\_ \
 *                     \/            \/     \/    \/            \/
 * $Id$
 *
 * Copyright (C) 2005 David Dent
 *
 * 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 "errno.h"

PLUGIN_HEADER

/* function return values */
enum tidy_return
{
    TIDY_RETURN_OK = 0,
    TIDY_RETURN_ERROR = 1,
    TIDY_RETURN_USB = 2,
    TIDY_RETURN_ABORT = 3,
};

#define MAX_TYPES 64
struct tidy_type {
    char filestring[64];
    bool directory;
    bool remove;
} tidy_types[MAX_TYPES];
int tidy_type_count;
bool tidy_loaded_and_changed = false;

#define DEFAULT_FILES PLUGIN_APPS_DIR "/disktidy.config"
#define CUSTOM_FILES  PLUGIN_APPS_DIR "/disktidy_custom.config"
void add_item(const char* name, int index)
{
    rb->strcpy(tidy_types[index].filestring, name);
    if (name[rb->strlen(name)-1] == '/')
    {
        tidy_types[index].directory = true;
        tidy_types[index].filestring[rb->strlen(name)-1] = '\0';
    }
    else
        tidy_types[index].directory = false;
}
static int find_file_string(const char *file, char *last_group)
{
    char temp[MAX_PATH];
    int i = 0, idx_last_group = -1;
    bool folder = false;
    rb->strcpy(temp, file);
    if (temp[rb->strlen(temp)-1] == '/')
    {
        folder = true;
        temp[rb->strlen(temp)-1] = '\0';
    }
    while (i<tidy_type_count)
    {
        if (!rb->strcmp(tidy_types[i].filestring, temp) &&
            folder == tidy_types[i].directory)
            return i;
        else if (!rb->strcmp(tidy_types[i].filestring, last_group))
            idx_last_group = i;
        i++;
    }
    /* not found, so insert it into its group */
    if (file[0] != '<' && idx_last_group != -1)
    {
        for (i=idx_last_group; i<tidy_type_count; i++)
        {
            if (tidy_types[i].filestring[0] == '<')
            {
                idx_last_group = i;
                break;
            }
        }
        /* shift items up one */
        for (i=tidy_type_count;i>idx_last_group;i--)
        {
            rb->strcpy(tidy_types[i].filestring, tidy_types[i-1].filestring);
            tidy_types[i].directory = tidy_types[i-1].directory;
            tidy_types[i].remove = tidy_types[i-1].remove;
        }
        tidy_type_count++;
        add_item(file, idx_last_group+1);
        return idx_last_group+1;
    }
    return i;
}

bool tidy_load_file(const char* file)
{
    int fd = rb->open(file, O_RDONLY), i;
    char buf[MAX_PATH], *str, *remove;
    char last_group[MAX_PATH] = "";
    bool new;
    if (fd < 0)
        return false;
    while ((tidy_type_count < MAX_TYPES) && 
            rb->read_line(fd, buf, MAX_PATH))
    {
        if (rb->settings_parseline(buf, &str, &remove))
        {
            i = find_file_string(str, last_group);
            new = (i >= tidy_type_count);
            if (!rb->strcmp(remove, "yes"))
                tidy_types[i].remove = true;
            else tidy_types[i].remove = false;
            if (new)
            {
                i = tidy_type_count;
                add_item(str, i);
                tidy_type_count++;
            }
            if (str[0] == '<')
                rb->strcpy(last_group, str);
        }
    }
    rb->close(fd);
    return true;
}

bool tidy_remove_item(char *item, int attr)
{
    int i;
    char *file;
    bool ret = false, rem = false;
    for (i=0; ret == false && i < tidy_type_count; i++)
    {
        file = tidy_types[i].filestring;
        if (file[rb->strlen(file)-1] == '*')
        {
            if (!rb->strncmp(file, item, rb->strlen(file)-1))
                rem = true;
        }
        else if (!rb->strcmp(file, item))
            rem = true;
        if (rem)
        {
            if (!tidy_types[i].remove)
                return false;
            if (attr&ATTR_DIRECTORY)
                ret = tidy_types[i].directory;
            else ret = true;
        }
    }
    return ret;
}

void tidy_lcd_status(const char *name, int *removed)
{
    char text[24]; /* "Cleaned up nnnnn items" */

    /* display status text */
    rb->lcd_clear_display();
    rb->lcd_puts(0, 0, "Working ...");
    rb->lcd_puts(0, 1, name);
    rb->snprintf(text, 24, "Cleaned up %d items", *removed);
#ifdef HAVE_LCD_BITMAP
    rb->lcd_puts(0, 2, text);
#endif
    rb->lcd_update();
}

int tidy_path_append_entry(char *path, struct dirent *entry, int *path_length)
{
    int name_len = rb->strlen(entry->d_name);
    /* for the special case of path="/" this is one bigger but it's not a problem */
    int new_length = *path_length + name_len + 1;
    
    /* check overflow (keep space for trailing zero) */
    if(new_length >= MAX_PATH)
        return 0;
    
    /* special case for path <> "/" */
    if(rb->strcmp(path, "/") != 0)
    {
        rb->strcat(path + *path_length, "/");
        (*path_length)++;
    }
    /* strcat is unsafe but the previous check normally avoid any problem */
    /* use path_length to optimise */
    
    rb->strcat(path + *path_length, entry->d_name);
    *path_length += name_len;
    
    return 1;
}

void tidy_path_remove_entry(char *path, int old_path_length, int *path_length)
{
    path[old_path_length] = '\0';
    *path_length = old_path_length;
}

/* path is assumed to be array of size MAX_PATH */
enum tidy_return tidy_removedir(char *path, int *path_length, int *removed)
{
    /* delete directory */
    struct dirent *entry;
    enum tidy_return status = TIDY_RETURN_OK;
    int button;
    DIR *dir;
    int old_path_length = *path_length;

    /* display status text */
    tidy_lcd_status(path, removed);

    rb->yield();

    dir = rb->opendir(path);
    if (dir)
    {
        while((status == TIDY_RETURN_OK) && ((entry = rb->readdir(dir)) != 0))
        /* walk directory */
        {
            /* check for user input and usb connect */
            button = rb->get_action(CONTEXT_STD, TIMEOUT_NOBLOCK);
            if (button == ACTION_STD_CANCEL)
            {
                rb->closedir(dir);
                return TIDY_RETURN_ABORT;
            }
            if (rb->default_event_handler(button) == SYS_USB_CONNECTED)
            {
                rb->closedir(dir);
                return TIDY_RETURN_USB;
            }

            rb->yield();

            /* get absolute path */
            /* returns an error if path is too long */
            if(!tidy_path_append_entry(path, entry, path_length))
                /* silent error */
                continue;

            if (entry->attribute & ATTR_DIRECTORY)
            {
                /* dir ignore "." and ".." */
                if ((rb->strcmp(entry->d_name, ".") != 0) && \
                    (rb->strcmp(entry->d_name, "..") != 0))
                {
                    tidy_removedir(path, path_length, removed);
                }
            }
            else
            {
                /* file */
                *removed += 1;
                rb->remove(path);
            }
            
            /* restore path */
            tidy_path_remove_entry(path, old_path_length, path_length);
        }
        rb->closedir(dir);
        /* rmdir */
        *removed += 1;
        rb->rmdir(path);
    }
    else
    {
        status = TIDY_RETURN_ERROR;
    }
    return status;
}

/* path is assumed to be array of size MAX_PATH */
enum tidy_return tidy_clean(char *path, int *path_length, int *removed)
{
    /* deletes junk files and dirs left by system */
    struct dirent *entry;
    enum tidy_return status = TIDY_RETURN_OK;
    int button;
    int del; /* has the item been deleted */
    DIR *dir;
    int old_path_length = *path_length;

    /* display status text */
    tidy_lcd_status(path, removed);

    rb->yield();

    dir = rb->opendir(path);
    if (dir)
    {
        while((status == TIDY_RETURN_OK) && ((entry = rb->readdir(dir)) != 0))
        /* walk directory */
        {
            /* check for user input and usb connect */
            button = rb->get_action(CONTEXT_STD, TIMEOUT_NOBLOCK);
            if (button == ACTION_STD_CANCEL)
            {
                rb->closedir(dir);
                return TIDY_RETURN_ABORT;
            }
            if (rb->default_event_handler(button) == SYS_USB_CONNECTED)
            {
                rb->closedir(dir);
                return TIDY_RETURN_USB;
            }

            rb->yield();

            if (entry->attribute & ATTR_DIRECTORY)
            {
                /* directory ignore "." and ".." */
                if ((rb->strcmp(entry->d_name, ".") != 0) && \
                    (rb->strcmp(entry->d_name, "..") != 0))
                {
                    del = 0;

                    /* get absolute path */
                    /* returns an error if path is too long */
                    if(!tidy_path_append_entry(path, entry, path_length))
                        /* silent error */
                        continue;

                    if (tidy_remove_item(entry->d_name, entry->attribute))
                    {
                        /* delete dir */
                        tidy_removedir(path, path_length, removed);
                        del = 1;
                    }

                    if (del == 0)
                    {
                        /* dir not deleted so clean it */
                        status = tidy_clean(path, path_length, removed);
                    }
                    
                    /* restore path */
                    tidy_path_remove_entry(path, old_path_length, path_length);
                }
            }
            else
            {
                /* file */
                del = 0;
                if (tidy_remove_item(entry->d_name, entry->attribute))
                {
                    /* get absolute path */
                    /* returns an error if path is too long */
                    if(!tidy_path_append_entry(path, entry, path_length))
                        /* silent error */
                        continue;

                    *removed += 1; /* increment removed files counter */
                    /* delete file */
                    rb->remove(path);
                    del = 1;
                    
                    /* restore path */
                    tidy_path_remove_entry(path, old_path_length, path_length);
                }
            }
        }
        rb->closedir(dir);
        return status;
    }
    else
    {
        return TIDY_RETURN_ERROR;
    }
}

enum tidy_return tidy_do(void)
{
    /* clean disk and display num of items removed */
    int removed = 0;
    enum tidy_return status;
    char text[24]; /* "Cleaned up nnnnn items" */
    char path[MAX_PATH];
    int path_length;

#ifdef HAVE_ADJUSTABLE_CPU_FREQ
    rb->cpu_boost(true);
#endif

    rb->strcpy(path, "/");
    path_length = rb->strlen(path);
    status = tidy_clean(path, &path_length, &removed);

#ifdef HAVE_ADJUSTABLE_CPU_FREQ
    rb->cpu_boost(false);
#endif

    if ((status == TIDY_RETURN_OK) || (status == TIDY_RETURN_ABORT))
    {
        rb->lcd_clear_display();
        rb->snprintf(text, 24, "Cleaned up %d items", removed);
        if (status == TIDY_RETURN_ABORT)
        {
            rb->splash(HZ, "User aborted");
            rb->lcd_clear_display();
        }
        rb->splash(HZ*2, text);
    }
    return status;
}

enum themable_icons get_icon(int item, void * data)
{
    (void)data;
    if (tidy_types[item].filestring[0] == '<') /* special type */
        return Icon_Folder;
    else if (tidy_types[item].remove)
        return Icon_Cursor;
    else
        return Icon_NOICON;
}

static const char* get_name(int selected_item, void * data,
                            char * buffer, size_t buffer_len)
{
    (void)data;
    if (tidy_types[selected_item].directory)
    {
        rb->snprintf(buffer, buffer_len, "%s/",
                     tidy_types[selected_item].filestring);
        return buffer;
    }
    return tidy_types[selected_item].filestring;
}

int list_action_callback(int action, struct gui_synclist *lists)
{
    if (action == ACTION_STD_OK)
    {
        int selection = rb->gui_synclist_get_sel_pos(lists);
        if (tidy_types[selection].filestring[0] == '<')
        {
            int i;
            if (!rb->strcmp(tidy_types[selection].filestring, "< ALL >"))
            {
                for (i=0; i<tidy_type_count; i++)
                {
                    if (tidy_types[i].filestring[0] != '<')
                        tidy_types[i].remove = true;
                }
            }
            else if (!rb->strcmp(tidy_types[selection].filestring, "< NONE >"))
            {
                for (i=0; i<tidy_type_count; i++)
                {
                    if (tidy_types[i].filestring[0] != '<')
                        tidy_types[i].remove = false;
                }
            }
            else /* toggle all untill the next <> */
            {
                selection++;
                while (selection < tidy_type_count &&
                       tidy_types[selection].filestring[0] != '<')
                {
                    tidy_types[selection].remove = !tidy_types[selection].remove;
                    selection++;
                }
            }
        }
        else
            tidy_types[selection].remove = !tidy_types[selection].remove;
        tidy_loaded_and_changed = true;
        return ACTION_REDRAW;
    }
    return action;
}

enum tidy_return tidy_lcd_menu(void)
{
    int selection = 0;
    enum tidy_return status = TIDY_RETURN_OK;
    bool menu_quit = false;

    MENUITEM_STRINGLIST(menu, "Disktidy Menu", NULL,
                        "Start Cleaning", "Files to Clean",
                        "Quit");

    while (!menu_quit)
    {
        switch(rb->do_menu(&menu, &selection, NULL, false))
        {
            case 0:
                menu_quit = true;   /* start cleaning */
                break;

            case 1:
            {
                bool show_icons = rb->global_settings->show_icons;
                struct simplelist_info list;
                rb->simplelist_info_init(&list, "Files to Clean",
                                         tidy_type_count, NULL);
                list.get_icon = get_icon;
                list.get_name = get_name;
                list.action_callback = list_action_callback;
                rb->simplelist_show_list(&list);
                rb->global_settings->show_icons = show_icons;
            }
            break;

            default:
                status = TIDY_RETURN_ABORT; /* exit plugin */
                menu_quit = true;
                break;
        }
    }
    return status;
}

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

    tidy_type_count = 0;
    tidy_load_file(DEFAULT_FILES);
    tidy_load_file(CUSTOM_FILES);
    if (tidy_type_count == 0)
    {
        rb->splash(3*HZ, "Missing disktidy.config file");
        return PLUGIN_ERROR;
    }
    status = tidy_lcd_menu();
    if (tidy_loaded_and_changed)
    {
        int fd = rb->creat(CUSTOM_FILES, 0666);
        int i;
        if (fd >= 0)
        {
            for(i=0;i<tidy_type_count;i++)
            {
                rb->fdprintf(fd, "%s%c: %s\n", tidy_types[i].filestring,
                             tidy_types[i].directory?'/':'\0',
                             tidy_types[i].remove?"yes":"no");
            }
            rb->close(fd);
        }
    }
    if (status == TIDY_RETURN_ABORT)
        return PLUGIN_OK;
    while (true)
    {
            status = tidy_do();

            switch (status)
            {
                case TIDY_RETURN_OK:
                    return PLUGIN_OK;
                case TIDY_RETURN_ERROR:
                    return PLUGIN_ERROR;
                case TIDY_RETURN_USB:
                    return PLUGIN_USB_CONNECTED;
                case TIDY_RETURN_ABORT:
                    return PLUGIN_OK;
            }
    }

    if (rb->default_event_handler(rb->button_get(false)) == SYS_USB_CONNECTED)
        return PLUGIN_USB_CONNECTED;

    rb->yield();

    return PLUGIN_OK;
}
