/***************************************************************************
 *             __________               __   ___.
 *   Open      \______   \ ____   ____ |  | _\_ |__   _______  ___
 *   Source     |       _//  _ \_/ ___\|  |/ /| __ \ /  _ \  \/  /
 *   Jukebox    |    |   (  <_> )  \___|    < | \_\ (  <_> > <  <
 *   Firmware   |____|_  /\____/ \___  >__|_ \|___  /\____/__/\_ \
 *                     \/            \/     \/    \/            \/
 *
 * Copyright (C) 2005 by Kevin Ferrare
 * Copyright (C) 2007 by Jonathan Gordon
 *
 * 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 <stdlib.h>
#include "string-extra.h"
#include "config.h"
#include "system.h"
#include "option_select.h"
#include "kernel.h"
#include "lang.h"
#include "talk.h"
#include "settings_list.h"
#include "sound.h"
#include "list.h"
#include "action.h"
#include "misc.h"
#include "splash.h"
#include "menu.h"
#include "quickscreen.h"

/* HASFLAG compares value to flags returns true if set, false otherwise */
#define HASFLAG(settings_list, flag) ((settings_list->flags & (flag)) == (flag))

static int selection_to_val(const struct settings_list *setting, int selection);
int option_value_as_int(const struct settings_list *setting)
{
    int type = (setting->flags & F_T_MASK);
    int temp = 0;
    if (type == F_T_BOOL)
        temp = *(bool*)setting->setting?1:0;
    else if (type == F_T_UINT || type == F_T_INT)
        temp = *(int*)setting->setting;
    return temp;
}

/* return an auto ranged time string, unit_idx specifies lowest or
   base index of the passed value -- flag F_TIME_SETTING calls this */
static const char *option_get_timestring(char *buf, int buf_len,
                                         int val, int unit_idx)
{
    return format_time_auto(buf, buf_len, val, unit_idx | UNIT_TRIM_ZERO, false);
}

/* these two vars are needed so arbitrary values can be added to the
   TABLE_SETTING settings if the F_ALLOW_ARBITRARY_VALS flag is set */
static int table_setting_oldval = 0, table_setting_array_position = 0;
const char *option_get_valuestring(const struct settings_list *setting,
                                   char *buffer, int buf_len,
                                   intptr_t temp_var)
{
    const char* str = buffer;
    if (HASFLAG(setting, F_BOOL_SETTING))
    {
        bool val = (bool)temp_var;
        strmemccpy(buffer, str(val? setting->bool_setting->lang_yes :
                                    setting->bool_setting->lang_no), buf_len);
    }
#if 0 /* probably dont need this one */
    else if (HASFLAG(setting, F_FILENAME))
    {
        struct filename_setting *info = setting->filename_setting;
        snprintf(buffer, buf_len, "%s%s%s", info->prefix,
                 (char*)temp_var, info->suffix);
    }
#endif
    else if ((HASFLAG(setting, F_INT_SETTING)) ||
              HASFLAG(setting, F_TABLE_SETTING))
    {
        const struct int_setting *int_info = setting->int_setting;
        const struct table_setting *tbl_info = setting->table_setting;
        int info_unit;
        const char *str_unit;
        const char* (*formatter)(char*, size_t, int, const char*);
        if (HASFLAG(setting, F_INT_SETTING))
        {
            formatter = int_info->formatter;
            info_unit = int_info->unit;
        }
        else
        {
            formatter = tbl_info->formatter;
            info_unit = tbl_info->unit;
        }

        bool is_time_setting = HASFLAG(setting, F_TIME_SETTING);
        if (is_time_setting)
            str = option_get_timestring(buffer, buf_len, (long)temp_var, info_unit);

        str_unit = unit_strings_core[info_unit];

        if (formatter)
            str = formatter(buffer, buf_len, (int)temp_var, str_unit);
        else if (!is_time_setting)
            snprintf(buffer, buf_len, "%d %s", (int)temp_var, str_unit?str_unit:"");
    }
    else if (HASFLAG(setting, F_T_SOUND))
    {
        format_sound_value(buffer, buf_len,
                           setting->sound_setting->setting,
                           temp_var);
    }
    else if (HASFLAG(setting, F_CHOICE_SETTING))
    {
        if (HASFLAG(setting, F_CHOICETALKS))
        {
            const struct choice_setting *info = setting->choice_setting;
            if (info->talks[(int)temp_var] < LANG_LAST_INDEX_IN_ARRAY)
            {
                strmemccpy(buffer, str(info->talks[(int)temp_var]), buf_len);
            }
            else
            {
                cfg_int_to_string(setting, (int)temp_var, buffer, buf_len);
            }
        }
        else
        {
            int value = (int)temp_var;
            char *val = P2STR(setting->choice_setting->desc[value]);
            strmemccpy(buffer, val, buf_len);
        }
    }
    return str;
}
void option_talk_value(const struct settings_list *setting, int value, bool enqueue)
{

    if (HASFLAG(setting, F_BOOL_SETTING))
    {
        bool val = (value==1);
        talk_id(val? setting->bool_setting->lang_yes :
                setting->bool_setting->lang_no, enqueue);
    }
#if 0 /* probably dont need this one */
    else if (HASFLAG(setting, F_FILENAME))
    {
}
#endif
    else if ((HASFLAG(setting, F_INT_SETTING)) ||
              HASFLAG(setting, F_TABLE_SETTING))
    {
        const struct int_setting *int_info = setting->int_setting;
        const struct table_setting *tbl_info = setting->table_setting;
        int unit;
        int32_t (*get_talk_id)(int, int);
        if (HASFLAG(setting, F_INT_SETTING))
        {
            unit = int_info->unit;
            get_talk_id = int_info->get_talk_id;
        }
        else
        {
            unit = tbl_info->unit;
            get_talk_id = tbl_info->get_talk_id;
        }
        if (get_talk_id)
            talk_id(get_talk_id(value, unit), enqueue);
        else if (HASFLAG(setting, F_TIME_SETTING))
            talk_time_intervals(value, unit, enqueue);
        else
            talk_value(value, unit, enqueue);
    }
    else if (HASFLAG(setting, F_T_SOUND))
    {
        int talkunit = UNIT_INT;
        int sound_setting = setting->sound_setting->setting;
        const char *unit = sound_unit(sound_setting);
        int decimals = sound_numdecimals(sound_setting);
        int phys = sound_val2phys(sound_setting, value);
        if (!strcmp(unit, "dB"))
            talkunit = UNIT_DB;
        else if (!strcmp(unit, "%"))
            talkunit = UNIT_PERCENT;
        else if (!strcmp(unit, "Hz"))
            talkunit = UNIT_HERTZ;
        talk_value_decimal(phys, talkunit, decimals, enqueue);
    }
    else if (HASFLAG(setting, F_CHOICE_SETTING))
    {
        if (HASFLAG(setting, F_CHOICETALKS))
        {
            talk_id(setting->choice_setting->talks[value], enqueue);
        }
        else
        {
            talk_id(P2ID(setting->choice_setting->desc[value]), enqueue);
        }
    }
}

static int option_talk(int selected_item, void * data)
{
    struct settings_list *setting = (struct settings_list *)data;
    int temp_var = selection_to_val(setting, selected_item);
    option_talk_value(setting, temp_var, false);
    return 0;
}

#if defined(HAVE_QUICKSCREEN) || defined(HAVE_RECORDING) || defined(HAVE_TOUCHSCREEN)
   /* only the quickscreen and recording trigger needs this */
void option_select_next_val(const struct settings_list *setting,
                            bool previous, bool apply)
{
    bool repeated = get_action_statuscode(NULL) & ACTION_REPEAT;

    int val = 0;
    int *value = setting->setting;
    if (HASFLAG(setting, F_BOOL_SETTING))
    {
        if (repeated)
            return;

        *(bool*)value = !*(bool*)value;
        if (apply && setting->bool_setting->option_callback)
            setting->bool_setting->option_callback(*(bool*)value);
        return;
    }
    else if (HASFLAG(setting, F_INT_SETTING))
    {
        struct int_setting *info = (struct int_setting *)setting->int_setting;
        bool neg_step = (info->step < 0);
        if (!previous)
        {
            val = *value + info->step;
            if (neg_step ? (val < info->max) : (val > info->max))
                val = repeated ? *value : info->min;
        }
        else
        {
            val = *value - info->step;
            if (neg_step ? (val > info->min) : (val < info->min))
                val = repeated ? *value : info->max;
        }
        *value = val;
        if (apply && info->option_callback)
            info->option_callback(val);
    }
    else if (HASFLAG(setting, F_T_SOUND))
    {
        int setting_id = setting->sound_setting->setting;
        int steps = sound_steps(setting_id);
        int min = sound_min(setting_id);
        int max = sound_max(setting_id);
        if (!previous)
        {
            val = *value + steps;
            if (val >= max)
                val = repeated ? *value :  min;
        }
        else
        {
            val = *value - steps;
            if (val < min)
                val = repeated ? *value : max;
        }
        *value = val;
        if (apply)
            sound_set(setting_id, val);
    }
    else if (HASFLAG(setting, F_CHOICE_SETTING))
    {
        struct choice_setting *info = (struct choice_setting *)setting->choice_setting;
        if (!previous)
        {
            val = *value + 1;
            if (val >= info->count)
                val = repeated ? *value :  0;
        }
        else
        {
            val = *value - 1;
            if (val < 0)
                val = repeated ? *value : info->count-1;
        }
        *value = val;
        if (apply && info->option_callback)
            info->option_callback(val);
    }
    else if (HASFLAG(setting, F_TABLE_SETTING))
    {
        const struct table_setting *tbl_info = setting->table_setting;
        int i, add;
        add = previous?tbl_info->count-1:1;
        for (i=0; i<tbl_info->count;i++)
        {
            if ((*value == tbl_info->values[i]) ||
                  (settings->flags&F_ALLOW_ARBITRARY_VALS &&
                    *value < tbl_info->values[i]))
            {
                int index = (i+add)%tbl_info->count;
                if (repeated && ((i == 0 && previous) || (!previous && i == tbl_info->count -1)))
                    val = *value;
                else
                    val = tbl_info->values[index];
                break;
            }
        }
        *value = val;
        if (apply && tbl_info->option_callback)
            tbl_info->option_callback(val);
    }
}
#endif

static int selection_to_val(const struct settings_list *setting, int selection)
{
    /* rockbox: comment 'set but unused' variables
    int min = 0;
    */
    int max = 0, step = 1;
    if ((HASFLAG(setting, F_BOOL_SETTING)) ||
         HASFLAG(setting, F_CHOICE_SETTING))
    {
        return selection;
    }
    else if (HASFLAG(setting, F_TABLE_SETTING))
    {
        const struct table_setting *info = setting->table_setting;
        if (HASFLAG(setting, F_ALLOW_ARBITRARY_VALS) &&
            table_setting_array_position != -1    &&
            (selection >= table_setting_array_position))
        {
            if (selection == table_setting_array_position)
                return table_setting_oldval;
            return info->values[selection-1];
        }
        else
            return info->values[selection];
    }
    else if (HASFLAG(setting, F_T_SOUND))
    {
        int setting_id = setting->sound_setting->setting;
        if(global_settings.list_order == LIST_ORDER_DESCENDING)
        {
            step = sound_steps(setting_id);
            max = (setting_id == SOUND_VOLUME) ?
                global_settings.volume_limit : sound_max(setting_id);
            /* min = sound_min(setting_id); */
        }
        else
        {
            step = -sound_steps(setting_id);
            /* min = sound_max(setting_id); */
            max = sound_min(setting_id);
        }
    }
    else if (HASFLAG(setting, F_INT_SETTING))
    {
        const struct int_setting *info = setting->int_setting;
        if(global_settings.list_order == LIST_ORDER_DESCENDING)
        {
            /* min = info->min; */
            max = info->max;
            step = info->step;
        }
        else
        {
            max = info->min;
            /* min = info->max; */
            step = -info->step;
        }
    }
    return max- (selection * step);
}

static const char * value_setting_get_name_cb(int selected_item,
                                              void * data,
                                              char *buffer,
                                              size_t buffer_len)
{
    selected_item = selection_to_val(data, selected_item);
    return option_get_valuestring(data, buffer, buffer_len, selected_item);
}

/* wrapper to convert from int param to bool param in option_screen */
static void (*boolfunction)(bool);
static void bool_funcwrapper(int value)
{
    if (value)
        boolfunction(true);
    else
        boolfunction(false);
}

static void val_to_selection(const struct settings_list *setting, int oldvalue,
                             int *nb_items, int *selected,
                             void (**function)(int))
{
    int var_type = setting->flags&F_T_MASK;
    /* set the number of items and current selection */
    if (var_type == F_T_INT || var_type == F_T_UINT)
    {
        if (HASFLAG(setting, F_CHOICE_SETTING))
        {
            *nb_items = setting->choice_setting->count;
            *selected = oldvalue;
            *function = setting->choice_setting->option_callback;
        }
        else if (HASFLAG(setting, F_TABLE_SETTING))
        {
            const struct table_setting *info = setting->table_setting;
            int i;
            *nb_items = info->count;
            *selected = -1;
            table_setting_array_position = -1;
            for (i=0;*selected==-1 && i<*nb_items;i++)
            {
                if (HASFLAG(setting, F_ALLOW_ARBITRARY_VALS) &&
                    (oldvalue < info->values[i]))
                {
                    table_setting_oldval = oldvalue;
                    table_setting_array_position = i;
                    *selected = i;
                    (*nb_items)++;
                }
                else if (oldvalue == info->values[i])
                    *selected = i;
            }
            *function = info->option_callback;
        }
        else if (HASFLAG(setting, F_T_SOUND))
        {
            int setting_id = setting->sound_setting->setting;
            int steps = sound_steps(setting_id);
            int min = sound_min(setting_id);
            int max = (setting_id == SOUND_VOLUME) ?
                global_settings.volume_limit : sound_max(setting_id);
            *nb_items = (max-min)/steps + 1;
            if (global_settings.list_order == LIST_ORDER_DESCENDING)
                *selected = (max - oldvalue) / steps;
            else
                *selected = (oldvalue - min) / steps;
            *function = sound_get_fn(setting_id);
        }
        else
        {
            const struct int_setting *info = setting->int_setting;
            int min, max, step;
            max = info->max;
            min = info->min;
            step = info->step;
            *nb_items = (max-min)/step + 1;
            if(global_settings.list_order == LIST_ORDER_DESCENDING)
                *selected = (max - oldvalue) / step;
            else
                *selected = (oldvalue - min) / step;
            *function = info->option_callback;
        }
    }
    else if (var_type == F_T_BOOL)
    {
        *selected = oldvalue;
        *nb_items = 2;
        boolfunction = setting->bool_setting->option_callback;
        if (boolfunction)
            *function = bool_funcwrapper;
    }
}

bool option_screen(const struct settings_list *setting,
                   struct viewport parent[NB_SCREENS],
                   bool use_temp_var, const unsigned char* option_title)
{
    int action;
    bool done = false;
    struct gui_synclist lists;
    int oldvalue, nb_items = 0, selected = 0, temp_var;
    int *variable;
    bool allow_wrap = (!HASFLAG(setting, F_NO_WRAP));
    bool cb_on_select_only = HASFLAG(setting, F_CB_ON_SELECT_ONLY);
    bool cb_on_changed = HASFLAG(setting, F_CB_ONLY_IF_CHANGED);

    int var_type = setting->flags&F_T_MASK;
    void (*function)(int) = NULL;
    const char *title = NULL;
    if (var_type == F_T_INT || var_type == F_T_UINT)
    {
        variable = use_temp_var ? &temp_var: (int*)setting->setting;
        temp_var = oldvalue = *(int*)setting->setting;
    }
    else if (var_type == F_T_BOOL)
    {
        /* bools always use the temp variable...
        if use_temp_var is false it will be copied to setting->setting every change */
        variable = &temp_var;
        temp_var = oldvalue = *(bool*)setting->setting?1:0;
    }
    else return false; /* only int/bools can go here */
    push_current_activity(ACTIVITY_OPTIONSELECT);
    gui_synclist_init(&lists, value_setting_get_name_cb,
                      (void*)setting, false, 1, parent);
    if (setting->lang_id == -1)
    {
        title = setting_get_cfgvals(setting);
    }

    if (!title)
        title = P2STR(option_title);

    gui_synclist_set_title(&lists, title, Icon_Questionmark);
    if(global_settings.talk_menu)
        gui_synclist_set_voice_callback(&lists, option_talk);

    val_to_selection(setting, oldvalue, &nb_items, &selected, &function);
    gui_synclist_set_nb_items(&lists, nb_items);
    gui_synclist_select_item(&lists, selected);

    gui_synclist_draw(&lists);
    /* talk the item */
    gui_synclist_speak_item(&lists);
    while (!done)
    {
        /* override user wraparound setting; used mainly by EQ settings.
         * Not sure this is justified? */
        if (!allow_wrap)
            lists.wraparound = false;

        if (list_do_action(CONTEXT_LIST, HZ, /* HZ so the status bar redraws */
                           &lists, &action))
        {
            /* setting changed */
            selected = gui_synclist_get_sel_pos(&lists);
            *variable = selection_to_val(setting, selected);
            if (var_type == F_T_BOOL && !use_temp_var)
                *(bool*)setting->setting = (*variable==1);
        }
        else if (action == ACTION_NONE)
            continue;
        else if (action == ACTION_STD_CANCEL || action == ACTION_STD_MENU)
        {
            /* setting canceled, restore old value if changed */
            if (*variable != oldvalue)
            {
                *variable = oldvalue;
                if (var_type == F_T_BOOL && !use_temp_var)
                    *(bool*)setting->setting = (oldvalue==1);
                splash(HZ/2, ID2P(LANG_CANCEL));
            }
            done = true;
        }
        else if (action == ACTION_STD_CONTEXT)
        {
            /* reset setting to default */
            reset_setting(setting, variable);
            if (var_type == F_T_BOOL && !use_temp_var)
                *(bool*)setting->setting = (*variable==1);
            val_to_selection(setting, *variable, &nb_items,
                                &selected, &function);
            gui_synclist_select_item(&lists, selected);
            gui_synclist_draw(&lists);
            gui_synclist_speak_item(&lists);
        }
        else if (action == ACTION_STD_OK)
        {
            /* setting accepted, store now if it used a temp var */
            if (use_temp_var)
            {
                if (var_type == F_T_INT || var_type == F_T_UINT)
                    *(int*)setting->setting = *variable;
                else
                    *(bool*)setting->setting = (*variable==1);
            }
            settings_save();
            done = true;
            cb_on_select_only = false; /* unset the flag so callback can be called */
        }
        else if(default_event_handler(action) == SYS_USB_CONNECTED)
        {
            pop_current_activity();
            return true;
        }

        /* callback */
        if (!cb_on_select_only && function)
        {
            if (!cb_on_changed || (*variable != oldvalue))
            {
                function(*variable);
                /* if the volume is changing we need to let the skins know */
                if (function == sound_get_fn(SOUND_VOLUME))
                    global_status.last_volume_change = current_tick;
            }
        }

    }
    pop_current_activity();
    return false;
}

int get_setting_info_for_bar(const struct settings_list *setting, int offset, int *count, int *val)
{
    int var_type = setting->flags&F_T_MASK;
    void (*function)(int) = NULL;
    int oldvalue;

    if (var_type == F_T_INT || var_type == F_T_UINT)
    {
        if (!(setting->flags&F_EQSETTING) || offset > 2)
            offset = 0;
        oldvalue = ((int*)setting->setting)[offset];
    }
    else if (var_type == F_T_BOOL)
    {
        oldvalue = *(bool*)setting->setting?1:0;
    }
    else
    {
        *val = 0;
        *count = 1;
        return false; /* only int/bools can go here */
    }

    val_to_selection(setting, oldvalue, count, val, &function);
    return true;
}

#ifdef HAVE_TOUCHSCREEN
void update_setting_value_from_touch(const struct settings_list *setting, int offset, int selection)
{
    int new_val = selection_to_val(setting, selection);
    int var_type = setting->flags&F_T_MASK;

    if (var_type == F_T_INT || var_type == F_T_UINT)
    {
        if (!(setting->flags&F_EQSETTING) || offset > 2)
            offset = 0;
        ((int*)setting->setting)[offset] = new_val;
    }
    else if (var_type == F_T_BOOL)
    {
        *(bool*)setting->setting = new_val ? true : false;
    }
}
#endif
