/*
 * MIT License
 *
 * Copyright (c) 2023 PrimmR
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

#include <stdlib.h>
#include <string.h>
#include "kitchen_conversions_face.h"

#include "watch_common_display.h"

typedef struct
{
    char name[6];          // Name to display on selection
    double conv_factor_uk; // Unit as represented in base units (UK)
    double conv_factor_us; // Unit as represented in base units (US)
    int16_t linear_factor; // Addition of constant (For temperatures)
} unit;

#define TICK_FREQ 4

#define MEASURES_COUNT 3 // Number of different measurement 'types'
#define WEIGHT 0
#define TEMP 1
#define VOL 2

// Names of measurements (classic & custom LCD)
static char measures[MEASURES_COUNT][7] = {"n&ass", " Temp", " VOL"};
static char measures_custom[MEASURES_COUNT][7] = {"n&ass", "  temp", "Volume"};

// Number of items in each category
#define WEIGHT_COUNT 4
#define TEMP_COUNT 3
#define VOL_COUNT 9
const uint8_t units_count[4] = {WEIGHT_COUNT, TEMP_COUNT, VOL_COUNT};

static const unit weights[WEIGHT_COUNT] = {
    {" g", 1., 1., 0}, // BASE
    {" kg", 1000., 1000, 0},
    {"Ounce", 28.34952, 28.34952, 0},
    {" Pound", 453.5924, 453.5924, 0},
};

static const unit temps[TEMP_COUNT] = {
    {" # C", 1.8, 1.8, 32},
    {" # F", 1., 1., 0}, // BASE
    {"Gas Mk", 25., 25., 250},
};

static const unit temps_custom[TEMP_COUNT] = {
    {" #C", 1.8, 1.8, 32},
    {" #F", 1., 1., 0}, // BASE
    {"Gas Mk", 25., 25., 250},
};

static const unit vols[VOL_COUNT] = {
    {"  n&L", 1., 1., 0}, // BASE (ml)
    {"   L", 1000., 1000., 0},
    {" Fl Oz", 28.41306, 29.57353, 0},
    {" Tbsp", 17.75816, 14.78677, 0},
    {" Tsp", 5.919388, 4.928922, 0},
    {"  Cup", 284.1306, 236.5882, 0},
    {" Pint", 568.2612, 473.1765, 0},
    {" Quart", 1136.522, 946.353, 0},
    {"Gallon", 4546.09, 3785.412, 0},
};

static int8_t calc_success_seq[5] = {BUZZER_NOTE_G6, 10, BUZZER_NOTE_C7, 10, 0};
static int8_t calc_fail_seq[5] = {BUZZER_NOTE_C7, 10, BUZZER_NOTE_G6, 10, 0};

// Resets all state variables to 0
static void reset_state(kitchen_conversions_state_t *state)
{
    state->pg = measurement;
    state->measurement_i = 0;
    state->from_i = 0;
    state->from_is_us = movement_use_imperial_units(); // If uses imperial, most likely to be US
    state->to_i = 0;
    state->to_is_us = movement_use_imperial_units();
    state->selection_value = 0;
    state->selection_index = 0;
    state->alarm_held = false;
}

void kitchen_conversions_face_setup(uint8_t watch_face_index, void **context_ptr)
{
    (void)watch_face_index;
    if (*context_ptr == NULL)
    {
        *context_ptr = malloc(sizeof(kitchen_conversions_state_t));
        memset(*context_ptr, 0, sizeof(kitchen_conversions_state_t));
        // Do any one-time tasks in here; the inside of this conditional happens only at boot.
    }
    // Do any pin or peripheral setup here; this will be called whenever the watch wakes from deep sleep.
}

void kitchen_conversions_face_activate(void *context)
{
    kitchen_conversions_state_t *state = (kitchen_conversions_state_t *)context;

    // Handle any tasks related to your watch face coming on screen.
    movement_request_tick_frequency(TICK_FREQ);

    reset_state(state);
}

// Increments index pointer by 1, wrapping
#define increment_wrapping(index, wrap) ({(index)++; index %= wrap; })

static uint32_t pow_10(uint8_t n)
{
    uint32_t result = 1;
    for (int i = 0; i < n; i++)
    {
        result *= 10;
    }
    return result;
}

// Returns correct list of units for the measurement index
static unit *get_unit_list(uint8_t measurement_i)
{
    switch (measurement_i)
    {
    case WEIGHT:
        return (unit *)weights;

    case TEMP:
        return (unit *)(watch_get_lcd_type() == WATCH_LCD_TYPE_CUSTOM ? temps_custom : temps);

    case VOL:
        return (unit *)vols;

    default:
        return (unit *)weights;
    }
}

// Increment digit by 1 in input (wraps)
static void increment_input(kitchen_conversions_state_t *state)
{
    uint8_t digit = state->selection_value / pow_10(DISPLAY_DIGITS - 1 - state->selection_index) % 10;
    if (digit != 9)
    {
        state->selection_value += pow_10(DISPLAY_DIGITS - 1 - state->selection_index);
    }
    else
    {
        state->selection_value -= 9 * pow_10(DISPLAY_DIGITS - 1 - state->selection_index);
    }
}

// Displays the list of units in the selected category
static void display_units(uint8_t measurement_i, uint8_t list_i)
{
    // Slighly hacky way to improve ml display on new LCD
    if (watch_get_lcd_type() == WATCH_LCD_TYPE_CUSTOM && measurement_i == VOL && list_i == 0)
        watch_display_text(WATCH_POSITION_BOTTOM, "    mL");
    else
        watch_display_text(WATCH_POSITION_BOTTOM, get_unit_list(measurement_i)[list_i].name);
}

static void display(kitchen_conversions_state_t *state, uint8_t subsec)
{
    watch_clear_display();

    switch (state->pg)
    {
    case measurement:
    {
        watch_display_text_with_fallback(WATCH_POSITION_TOP, "Unit", "Un");
        char* measurement_name = (watch_get_lcd_type() == WATCH_LCD_TYPE_CUSTOM ? measures_custom : measures)[state->measurement_i];
        watch_display_text(WATCH_POSITION_BOTTOM, measurement_name);
    }
    break;

    case from:
        display_units(state->measurement_i, state->from_i);

        // Display Fr if non-locale specific, else display locale and F
        if (state->measurement_i == VOL)
        {
            watch_display_text(WATCH_POSITION_TOP_RIGHT, " F");

            char *locale_custom = state->from_is_us ? "USA" : "IMP";
            char *locale = state->from_is_us ? "A " : "GB";
            watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, locale_custom, locale);
        }
        else
        {
            watch_display_text_with_fallback(WATCH_POSITION_TOP, "Frm", "Fr");
        }

        break;

    case to:
        display_units(state->measurement_i, state->to_i);

        // Display to if non-locale specific, else display locale and 't'
        if (state->measurement_i == VOL) {
            watch_display_text(WATCH_POSITION_TOP_RIGHT, " T");

            char *locale_custom = state->to_is_us ? "USA" : "IMP";
            char *locale = state->to_is_us ? "A " : "GB";
            watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, locale_custom, locale);
        }
        else
        {
            watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, " to", "to");
        }

        break;

    case input:
    {
        char buf[7];
        sprintf(buf, "%06lu", state->selection_value);
        watch_display_text(WATCH_POSITION_BOTTOM, buf);

        // Only allow ints for Gas Mk
        if (state->measurement_i == TEMP && state->from_i == 2)
        {
            watch_display_character(' ', 8);
            watch_display_character(' ', 9);
        }

        // Blink digit (on & off) twice a second
        if (subsec % 2)
        {
            watch_display_character(' ', 4 + state->selection_index);
        }

        watch_display_text_with_fallback(WATCH_POSITION_TOP, "Input", "In");
    }
    break;

    case result:
    {
        unit froms = get_unit_list(state->measurement_i)[state->from_i];
        unit tos = get_unit_list(state->measurement_i)[state->to_i];
        // Chooses correct factor for locale
        double f_conv_factor = state->from_is_us ? froms.conv_factor_us : froms.conv_factor_uk;
        double t_conv_factor = state->to_is_us ? tos.conv_factor_us : tos.conv_factor_uk;
        // Converts
        double to_base = (state->selection_value * f_conv_factor) + 100 * froms.linear_factor;
        double conversion = ((to_base - 100 * tos.linear_factor) / t_conv_factor);

        // If number too large or too small
        uint8_t lower_bound = (state->measurement_i == TEMP && state->to_i == 2) ? 100 : 0;
        if (conversion >= 1000000 || conversion < lower_bound)
        {
            watch_set_indicator(WATCH_INDICATOR_BELL);
            watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, " Error", " Err");

            if (movement_button_should_sound())
                watch_buzzer_play_sequence(calc_fail_seq, NULL);
        }
        else
        {
            uint32_t rounded = conversion + .5;
            char buf[7];
            sprintf(buf, "%6lu", rounded);
            watch_display_text(WATCH_POSITION_BOTTOM, buf);

            // Make sure LSDs always filled
            if (rounded < 10)
            {
                watch_display_character('0', 7);
                watch_display_character('0', 8);
            }
            else if (rounded < 100)
            {
                watch_display_character('0', 7);
            }

            if (movement_button_should_sound())
                watch_buzzer_play_sequence(calc_success_seq, NULL);
        }
        watch_display_text_with_fallback(WATCH_POSITION_TOP, "Res =", " =");
    }

    break;

    default:
        break;
    }
}

bool kitchen_conversions_face_loop(movement_event_t event, void *context)
{
    kitchen_conversions_state_t *state = (kitchen_conversions_state_t *)context;

    switch (event.event_type)
    {
    case EVENT_ACTIVATE:
        // Initial UI
        display(state, event.subsecond);
        break;
    case EVENT_TICK:
        // Update for blink animation on input
        if (state->pg == input)
        {
            display(state, event.subsecond);

            // Increments input twice a second when light button held
            if (state->alarm_held && event.subsecond % 2)
                increment_input(state);
        }
        break;
    case EVENT_ALARM_BUTTON_UP:
        // Cycles options
        switch (state->pg)
        {
        case measurement:
            increment_wrapping(state->measurement_i, MEASURES_COUNT);
            break;

        case from:
            increment_wrapping(state->from_i, units_count[state->measurement_i]);
            break;

        case to:
            increment_wrapping(state->to_i, units_count[state->measurement_i]);
            break;

        case input:
            increment_input(state);
            break;

        default:
            break;
        }

        // Light button does nothing on final screen
        if (state->pg != result)
            display(state, event.subsecond);

        state->alarm_held = false;

        break;

    case EVENT_LIGHT_BUTTON_UP:
        // Increments selected digit
        if (state->pg == input)
        {

            // Moves between digits in input
            // Wraps at 6 digits unless gas mark selected
            if (state->selection_index < (DISPLAY_DIGITS - 1) - 2 * (state->measurement_i == TEMP && state->from_i == 2))
            {
                state->selection_index++;
            }
            else
            {
                state->pg++;
                display(state, event.subsecond);
            }
        }
        // Moves forward 1 page
        else
        {
            if (state->pg == SCREEN_NUM - 1)
            {
                reset_state(state);
            }
            else
            {
                state->pg++;
            }

            // Play boop
            if (movement_button_should_sound())
                watch_buzzer_play_note(BUZZER_NOTE_C7, 50);
        }

        display(state, event.subsecond);

        state->alarm_held = false;

        break;

    case EVENT_LIGHT_LONG_PRESS:
        // Moves backwards through pages, resetting certain values
        if (state->pg != measurement)
        {
            switch (state->pg)
            {
            case measurement:
                state->measurement_i = 0;
                break;

            case from:
                state->from_i = 0;
                state->from_is_us = movement_use_imperial_units();
                break;

            case to:
                state->to_i = 0;
                state->to_is_us = movement_use_imperial_units();
                break;

            case input:
                state->selection_index = 0;
                state->selection_value = 0;
                break;

            case result:
                state->selection_index = 0;
                break;

            default:
                break;
            }

            state->pg--;
            display(state, event.subsecond);

            // Play beep
            if (movement_button_should_sound())
                watch_buzzer_play_note(BUZZER_NOTE_C8, 50);

            state->alarm_held = false;
        }
        break;

    case EVENT_ALARM_LONG_PRESS:
        // Switch between locales
        if (state->measurement_i == VOL)
        {
            if (state->pg == from)
            {
                state->from_is_us = !state->from_is_us;
            }
            else if (state->pg == to)
            {
                state->to_is_us = !state->to_is_us;
            }

            if (state->pg == from || state->pg == to)
            {
                display(state, event.subsecond);

                // Play bleep
                if (movement_button_should_sound())
                    watch_buzzer_play_note(BUZZER_NOTE_E7, 50);
            }
        }

        // Sets flag to increment input digit when alarm button held
        if (state->pg == input)
            state->alarm_held = true;

        break;

    case EVENT_ALARM_LONG_UP:
        state->alarm_held = false;
        break;

    case EVENT_TIMEOUT:
        movement_move_to_face(0);
        break;

    default:
        return movement_default_loop_handler(event);
    }

    return true;
}

void kitchen_conversions_face_resign(void *context)
{
    (void)context;

    // handle any cleanup before your watch face goes off-screen.
}
