//
// Copyright(C) 2005-2014 Simon Howard
//
// 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 program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//

#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>

#include "doomkeys.h"

#include "txt_spinctrl.h"
#include "txt_gui.h"
#include "txt_io.h"
#include "txt_main.h"
#include "txt_utf8.h"
#include "txt_window.h"

// Generate the format string to be used for displaying floats

static void FloatFormatString(float step, char *buf, size_t buf_len)
{
    int precision;

    precision = (int) ceil(-log(step) / log(10));

    if (precision > 0)
    {
        TXT_snprintf(buf, buf_len, "%%.%if", precision);
    }
    else
    {
        TXT_StringCopy(buf, "%.1f", buf_len);
    }
}

// Number of characters needed to represent a character 

static unsigned int IntWidth(int val)
{
    char buf[25];

    TXT_snprintf(buf, sizeof(buf), "%i", val);

    return strlen(buf);
}

static unsigned int FloatWidth(float val, float step)
{
    unsigned int precision;
    unsigned int result;

    // Calculate the width of the int value

    result = IntWidth((int) val);

    // Add a decimal part if the precision specifies it

    precision = (unsigned int) ceil(-log(step) / log(10));

    if (precision > 0)
    {
        result += precision + 1;
    }    

    return result;
}

// Returns the minimum width of the input box

static unsigned int SpinControlWidth(txt_spincontrol_t *spincontrol)
{
    unsigned int minw, maxw;

    switch (spincontrol->type)
    {
        case TXT_SPINCONTROL_FLOAT:
            minw = FloatWidth(spincontrol->min.f, spincontrol->step.f);
            maxw = FloatWidth(spincontrol->max.f, spincontrol->step.f);
            break;

        default:
        case TXT_SPINCONTROL_INT:
            minw = IntWidth(spincontrol->min.i);
            maxw = IntWidth(spincontrol->max.i);
            break;

    }
    
    // Choose the wider of the two values.  Add one so that there is always
    // space for the cursor when editing.

    if (minw > maxw)
    {
        return minw;
    }
    else
    {
        return maxw;
    }
}

static void TXT_SpinControlSizeCalc(TXT_UNCAST_ARG(spincontrol))
{
    TXT_CAST_ARG(txt_spincontrol_t, spincontrol);

    spincontrol->widget.w = SpinControlWidth(spincontrol) + 5;
    spincontrol->widget.h = 1;
}

static void SetBuffer(txt_spincontrol_t *spincontrol)
{
    char format[25];

    switch (spincontrol->type)
    {
        case TXT_SPINCONTROL_INT:
            TXT_snprintf(spincontrol->buffer, spincontrol->buffer_len,
                         "%i", spincontrol->value->i);
            break;

        case TXT_SPINCONTROL_FLOAT:
            FloatFormatString(spincontrol->step.f, format, sizeof(format));
            TXT_snprintf(spincontrol->buffer, spincontrol->buffer_len,
                         format, spincontrol->value->f);
            break;
    }
}

static void TXT_SpinControlDrawer(TXT_UNCAST_ARG(spincontrol))
{
    TXT_CAST_ARG(txt_spincontrol_t, spincontrol);
    unsigned int i;
    unsigned int padding;
    txt_saved_colors_t colors;
    int bw;
    int focused;

    focused = spincontrol->widget.focused;

    TXT_SaveColors(&colors);

    TXT_FGColor(TXT_COLOR_BRIGHT_CYAN);
    TXT_DrawCodePageString("\x1b ");

    TXT_RestoreColors(&colors);

    // Choose background color

    if (focused && spincontrol->editing)
    {
        TXT_BGColor(TXT_COLOR_BLACK, 0);
    }
    else
    {
        TXT_SetWidgetBG(spincontrol);
    }

    if (!spincontrol->editing)
    {
        SetBuffer(spincontrol);
    }

    i = 0;

    bw = TXT_UTF8_Strlen(spincontrol->buffer);
    padding = spincontrol->widget.w - bw - 4;

    while (i < padding)
    {
        TXT_DrawString(" ");
        ++i;
    }

    TXT_DrawString(spincontrol->buffer);
    i += bw;

    while (i < spincontrol->widget.w - 4)
    {
        TXT_DrawString(" ");
        ++i;
    }

    TXT_RestoreColors(&colors);
    TXT_FGColor(TXT_COLOR_BRIGHT_CYAN);
    TXT_DrawCodePageString(" \x1a");
}

static void TXT_SpinControlDestructor(TXT_UNCAST_ARG(spincontrol))
{
    TXT_CAST_ARG(txt_spincontrol_t, spincontrol);

    free(spincontrol->buffer);
}

static void AddCharacter(txt_spincontrol_t *spincontrol, int key)
{
    if (TXT_UTF8_Strlen(spincontrol->buffer) < SpinControlWidth(spincontrol)
     && strlen(spincontrol->buffer) < spincontrol->buffer_len - 2)
    {
        spincontrol->buffer[strlen(spincontrol->buffer) + 1] = '\0';
        spincontrol->buffer[strlen(spincontrol->buffer)] = key;
    }
}

static void Backspace(txt_spincontrol_t *spincontrol)
{
    if (TXT_UTF8_Strlen(spincontrol->buffer) > 0)
    {
        spincontrol->buffer[strlen(spincontrol->buffer) - 1] = '\0';
    }
}

static void EnforceLimits(txt_spincontrol_t *spincontrol)
{
    switch (spincontrol->type)
    {
        case TXT_SPINCONTROL_INT:
            if (spincontrol->value->i > spincontrol->max.i)
                spincontrol->value->i = spincontrol->max.i;
            else if (spincontrol->value->i < spincontrol->min.i)
                spincontrol->value->i = spincontrol->min.i;
            break;

        case TXT_SPINCONTROL_FLOAT:
            if (spincontrol->value->f > spincontrol->max.f)
                spincontrol->value->f = spincontrol->max.f;
            else if (spincontrol->value->f < spincontrol->min.f)
                spincontrol->value->f = spincontrol->min.f;
            break;
    }
}

static void FinishEditing(txt_spincontrol_t *spincontrol)
{
    switch (spincontrol->type)
    {
        case TXT_SPINCONTROL_INT:
            spincontrol->value->i = atoi(spincontrol->buffer);
            break;

        case TXT_SPINCONTROL_FLOAT:
            spincontrol->value->f = (float) atof(spincontrol->buffer);
            break;
    }

    spincontrol->editing = 0;
    EnforceLimits(spincontrol);
}

static int TXT_SpinControlKeyPress(TXT_UNCAST_ARG(spincontrol), int key)
{
    TXT_CAST_ARG(txt_spincontrol_t, spincontrol);

    // Enter to enter edit mode

    if (spincontrol->editing)
    {
        if (key == KEY_ENTER)
        {
            FinishEditing(spincontrol);
            return 1;
        }

        if (key == KEY_ESCAPE)
        {
            // Abort without saving value
            spincontrol->editing = 0;
            return 1;
        }

        if (isdigit(key) || key == '-' || key == '.')
        {
            AddCharacter(spincontrol, key);
            return 1;
        }

        if (key == KEY_BACKSPACE)
        {
            Backspace(spincontrol);
            return 1;
        }
    }
    else
    {
        // Non-editing mode

        if (key == KEY_ENTER)
        {
            spincontrol->editing = 1;
            TXT_StringCopy(spincontrol->buffer, "", spincontrol->buffer_len);
            return 1;
        }
        if (key == KEY_LEFTARROW)
        {
            switch (spincontrol->type)
            {
                case TXT_SPINCONTROL_INT:
                    spincontrol->value->i -= spincontrol->step.i;
                    break;

                case TXT_SPINCONTROL_FLOAT:
                    spincontrol->value->f -= spincontrol->step.f;
                    break;
            }

            EnforceLimits(spincontrol);

            return 1;
        }
        
        if (key == KEY_RIGHTARROW)
        {
            switch (spincontrol->type)
            {
                case TXT_SPINCONTROL_INT:
                    spincontrol->value->i += spincontrol->step.i;
                    break;

                case TXT_SPINCONTROL_FLOAT:
                    spincontrol->value->f += spincontrol->step.f;
                    break;
            }

            EnforceLimits(spincontrol);

            return 1;
        }
    }

    return 0;
}

static void TXT_SpinControlMousePress(TXT_UNCAST_ARG(spincontrol),
                                   int x, int y, int b)
{
    TXT_CAST_ARG(txt_spincontrol_t, spincontrol);
    unsigned int rel_x;

    rel_x = x - spincontrol->widget.x;

    if (rel_x < 2)
    {
        TXT_SpinControlKeyPress(spincontrol, KEY_LEFTARROW);
    }
    else if (rel_x >= spincontrol->widget.w - 2)
    {
        TXT_SpinControlKeyPress(spincontrol, KEY_RIGHTARROW);
    }
}

static void TXT_SpinControlFocused(TXT_UNCAST_ARG(spincontrol), int focused)
{
    TXT_CAST_ARG(txt_spincontrol_t, spincontrol);

    FinishEditing(spincontrol);
}

txt_widget_class_t txt_spincontrol_class =
{
    TXT_AlwaysSelectable,
    TXT_SpinControlSizeCalc,
    TXT_SpinControlDrawer,
    TXT_SpinControlKeyPress,
    TXT_SpinControlDestructor,
    TXT_SpinControlMousePress,
    NULL,
    TXT_SpinControlFocused,
};

static txt_spincontrol_t *TXT_BaseSpinControl(void)
{
    txt_spincontrol_t *spincontrol;

    spincontrol = malloc(sizeof(txt_spincontrol_t));

    TXT_InitWidget(spincontrol, &txt_spincontrol_class);
    spincontrol->buffer_len = 25;
    spincontrol->buffer = malloc(spincontrol->buffer_len);
    TXT_StringCopy(spincontrol->buffer, "", spincontrol->buffer_len);
    spincontrol->editing = 0;

    return spincontrol;
}

txt_spincontrol_t *TXT_NewSpinControl(int *value, int min, int max)
{
    txt_spincontrol_t *spincontrol;

    spincontrol = TXT_BaseSpinControl();
    spincontrol->type = TXT_SPINCONTROL_INT;
    spincontrol->value = (void *) value;
    spincontrol->min.i = min;
    spincontrol->max.i = max;
    spincontrol->step.i = 1;
    SetBuffer(spincontrol);

    return spincontrol;
}

txt_spincontrol_t *TXT_NewFloatSpinControl(float *value, float min, float max)
{
    txt_spincontrol_t *spincontrol;

    spincontrol = TXT_BaseSpinControl();
    spincontrol->type = TXT_SPINCONTROL_FLOAT;
    spincontrol->value = (void *) value;
    spincontrol->min.f = min;
    spincontrol->max.f = max;
    spincontrol->step.f = 0.1f;
    SetBuffer(spincontrol);

    return spincontrol;
}

