#include <stdlib.h>
#include <string.h>
#include <sqlite3.h>

#include <cJSON.h>
#include <logger.h>
#include <database.h>
#include <models/schedule.h>
#include <models/junction_tag.h>
#include <models/tag.h>

static int
db_update_insert(schedule_t *schedule, sqlite3_stmt *stmt)
{
    int rc;
    uint16_t *periods_blob = schedule_periods_to_blob(schedule);
    int blob_size = (int)sizeof(uint16_t) * ((periods_blob[0] * 2) + 1);

    sqlite3_bind_int(stmt, 1, schedule->id);
    sqlite3_bind_blob(stmt, 2, schedule->uid, sizeof(uuid_t), SQLITE_STATIC);
    sqlite3_bind_text(stmt, 3, schedule->name, -1, SQLITE_STATIC);
    sqlite3_bind_blob(stmt, 4, periods_blob, blob_size, SQLITE_STATIC);

    rc = sqlite3_step(stmt);

    sqlite3_finalize(stmt);

    free(periods_blob);

    return rc != SQLITE_DONE;
}
static schedule_t*
schedule_db_select_mapper(sqlite3_stmt *stmt)
{
    const uint16_t *periods_blob;
    schedule_t *new_schedule = malloc(sizeof(schedule_t));
    for(int i = 0; i < sqlite3_column_count(stmt); i++)
    {
        const char *name = sqlite3_column_name(stmt, i);
        switch(name[0])
        {
            case 'i': // id
                new_schedule->id = sqlite3_column_int(stmt, i);
                break;
            case 'n': // name
                strncpy(new_schedule->name, (const char*)sqlite3_column_text(stmt, i), 127);
                new_schedule->name[127] = '\0';
                break;
            case 'p': // periods
                periods_blob = sqlite3_column_blob(stmt, i);
                new_schedule->periods_count = periods_blob[0];
                new_schedule->periods = malloc(sizeof(period_t) * periods_blob[0]);

                for(int i = 0; i < periods_blob[0]; ++i)
                {
                    new_schedule->periods[i].start = periods_blob[(i * 2) + 1];
                    new_schedule->periods[i].end   = periods_blob[(i * 2) + 2];
                }

                break;
            case 'u': // uid
                uuid_copy(new_schedule->uid, (const unsigned char*)sqlite3_column_blob(stmt, i));
                break;
            default: // ignore columns not implemented
                break;
        }
    }
    return new_schedule;
}

static schedule_t**
schedule_db_select(sqlite3_stmt *stmt)
{
    schedule_t **all_schedules = malloc(sizeof(schedule_t*));

    int row = 0;

    while(true)
    {
        int s;

        s = sqlite3_step(stmt);
        if (s == SQLITE_ROW)
        {
            schedule_t *new_schedule = schedule_db_select_mapper(stmt);
            row++;

            all_schedules = (schedule_t**)realloc(all_schedules, sizeof(schedule_t*) * (row + 1));
            all_schedules[row - 1] = new_schedule;
        }
        else
        {
            if(s == SQLITE_DONE)
            {
                break;
            }
            else
            {
                LOG_ERROR("error selecting schedules from database: %s\n", sqlite3_errstr(s));
                break;
            }
        }
    }
    sqlite3_finalize(stmt);
    all_schedules[row] = NULL;
    return all_schedules;
}

int
schedule_save(schedule_t *schedule)
{
    sqlite3_stmt *stmt;
    if(schedule->id)
    {
        sqlite3_prepare_v2(global_database, "UPDATE schedules SET uid = ?2, name = ?3, periods = ?4 WHERE id=?1;", -1, &stmt, NULL);
    }
    else
    {
        sqlite3_prepare_v2(global_database, "INSERT INTO schedules(uid, name, periods) values (?2, ?3, ?4);", -1, &stmt, NULL);
    }

    int result = db_update_insert(schedule, stmt);

    if(result)
    {
        if(schedule->id)
        {
            LOG_ERROR("error inserting data: %s\n", sqlite3_errmsg(global_database));
        }
        else
        {
            LOG_ERROR("error updating data: %s\n", sqlite3_errmsg(global_database));
        }
    }
    else
    {
        schedule->id = sqlite3_last_insert_rowid(global_database);
    }
    return result;
}

int
schedule_remove(schedule_t *schedule)
{
    sqlite3_stmt *stmt;
    if(!schedule->id)
    {
        return 0;
    }

    sqlite3_prepare_v2(global_database, "DELETE FROM schedules WHERE id=?1;", -1, &stmt, NULL);
    sqlite3_bind_int(stmt, 1, schedule->id);

    int rc = sqlite3_step(stmt);

    sqlite3_finalize(stmt);

    return rc != SQLITE_DONE;
}

int
schedule_is_protected(schedule_t *schedule)
{
    uuid_t tmp_uuid;

    memset(tmp_uuid, 0, sizeof(uuid_t));
    memcpy(tmp_uuid, "off", 3);
    if(uuid_compare(schedule->uid, tmp_uuid) == 0)
    {
        return 1;
    }

    memset(tmp_uuid, 0, sizeof(uuid_t));
    memcpy(tmp_uuid, "on", 2);
    if(uuid_compare(schedule->uid, tmp_uuid) == 0)
    {
        return 1;
    }

    return 0;
}

void
schedule_free(schedule_t *schedule)
{
    free(schedule->periods);
    free(schedule);
}

void
schedule_free_list(schedule_t **schedules)
{
    for(int i = 0; schedules[i] != NULL; ++i)
    {
        schedule_free(schedules[i]);
    }
    free(schedules);
}

uint16_t*
schedule_periods_to_blob(schedule_t *schedule)
{
    uint16_t *blob = malloc(sizeof(uint16_t) * ((schedule->periods_count * 2) + 1));

    blob[0] = schedule->periods_count;

    for(int i = 0; i < schedule->periods_count; i++)
    {
        blob[(i * 2) + 1] = schedule->periods[i].start;
        blob[(i * 2) + 2] = schedule->periods[i].end;
    }
    return blob;
}

cJSON*
schedule_to_json(schedule_t *schedule)
{
    cJSON *json = cJSON_CreateObject();

    cJSON *json_name = cJSON_CreateString(schedule->name);
    if(json_name == NULL)
    {
        cJSON_Delete(json);
        return NULL;
    }
    cJSON_AddItemToObject(json, "name", json_name);

    char uuid_str[UUID_STR_LEN];
    schedule_uid_unparse(schedule->uid, uuid_str);
    cJSON *json_id = cJSON_CreateString(uuid_str);
    if(json_name == NULL)
    {
        cJSON_Delete(json);
        return NULL;
    }
    cJSON_AddItemToObject(json, "id", json_id);

    cJSON *json_periods = cJSON_CreateArray();
    if(json_periods == NULL)
    {
        cJSON_Delete(json);
        return NULL;
    }
    cJSON_AddItemToObject(json, "periods", json_periods);

    for(int i = 0; i < schedule->periods_count; ++i)
    {
        cJSON *json_period = cJSON_CreateObject();
        if (json_period == NULL)
        {
            continue;
        }

        char start_str[8], end_str[8];
        period_t *period = &schedule->periods[i];
        sprintf(start_str, "%02d:%02d", period->start / 60, period->start % 60);
        sprintf(end_str, "%02d:%02d", period->end / 60, period->end % 60);

        cJSON *json_period_start = cJSON_CreateString(start_str);
        if (json_period_start == NULL)
        {
            LOG_DEBUG("failed to add start period from string '%s'\n", start_str);
            cJSON_Delete(json_period);
            continue;
        }
        cJSON_AddItemToObject(json_period, "start", json_period_start);

        cJSON *json_period_end = cJSON_CreateString(end_str);
        if (json_period_end == NULL)
        {
            LOG_DEBUG("failed to add end period from string '%s'\n", end_str);
            cJSON_Delete(json_period);
            continue;
        }
        cJSON_AddItemToObject(json_period, "end", json_period_end);

        cJSON_AddItemToArray(json_periods, json_period);
    }

    cJSON *json_tags = cJSON_CreateArray();
    int *tags_ids = junction_tag_get_tags_for_schedule_id(schedule->id);
    if(tags_ids != NULL)
    {
        for(int i = 0; tags_ids[i] != 0; ++i)
        {
            char *tag = tag_get_tag(tags_ids[i]);
            if(tag == NULL)
            {
                continue;   
            }

            cJSON *json_tag = cJSON_CreateString(tag);
            if (json_tag == NULL)
            {
                LOG_DEBUG("failed to add tag from string '%s'\n", tag);
                free(tag);
                continue;
            }
            cJSON_AddItemToArray(json_tags, json_tag);
            free(tag);
        }
        free(tags_ids);
    }
    cJSON_AddItemToObject(json, "tags", json_tags);

    return json;
}

schedule_t*
schedule_get_by_id_or_off(int id)
{
    sqlite3_stmt *stmt;

    sqlite3_prepare_v2(global_database, "SELECT * FROM schedules WHERE id = ?1;", -1, &stmt, NULL);
    sqlite3_bind_int(stmt, 1, id);

    schedule_t **sql_result = schedule_db_select(stmt);

    schedule_t *result = sql_result[0];
    free(sql_result);

    if(result)
    {
        return result;
    }
    
    uuid_t tmp_uuid;
    memset(tmp_uuid, 0, sizeof(uuid_t));
    memcpy(tmp_uuid, "off", 3);

    return schedule_get_by_uid(tmp_uuid);
}

schedule_t*
schedule_get_by_id(int id)
{
    sqlite3_stmt *stmt;

    sqlite3_prepare_v2(global_database, "SELECT * FROM schedules WHERE id = ?1;", -1, &stmt, NULL);
    sqlite3_bind_int(stmt, 1, id);

    schedule_t **sql_result = schedule_db_select(stmt);

    schedule_t *result = sql_result[0];
    free(sql_result);

    return result;
}

schedule_t*
schedule_get_by_uid_or_off(uuid_t uid)
{
    sqlite3_stmt *stmt;

    sqlite3_prepare_v2(global_database, "SELECT * FROM schedules WHERE uid = ?1;", -1, &stmt, NULL);
    sqlite3_bind_blob(stmt, 1, uid, sizeof(uuid_t), SQLITE_STATIC);

    schedule_t **sql_result = schedule_db_select(stmt);

    schedule_t *result = sql_result[0];
    free(sql_result);

    if(result)
    {
        return result;
    }
    
    uuid_t tmp_uuid;
    memset(tmp_uuid, 0, sizeof(uuid_t));
    memcpy(tmp_uuid, "off", 3);

    return schedule_get_by_uid(tmp_uuid);
}

schedule_t*
schedule_get_by_uid(uuid_t uid)
{
    sqlite3_stmt *stmt;

    sqlite3_prepare_v2(global_database, "SELECT * FROM schedules WHERE uid = ?1;", -1, &stmt, NULL);
    sqlite3_bind_blob(stmt, 1, uid, sizeof(uuid_t), SQLITE_STATIC);

    schedule_t **sql_result = schedule_db_select(stmt);

    schedule_t *result = sql_result[0];
    free(sql_result);

    return result;
}

schedule_t**
schedule_get_all()
{
    sqlite3_stmt *stmt;

    sqlite3_prepare_v2(global_database, "SELECT * FROM schedules;", -1, &stmt, NULL);

    return schedule_db_select(stmt);

}

int
schedule_uid_parse(const char *uid_str, uuid_t result)
{
    if(strcmp("off", uid_str) == 0)
    {
        memset(result, 0, sizeof(uuid_t));
        memcpy(result, "off", 3);
        return 0;
    }
    if(strcmp("on", uid_str) == 0)
    {
        memset(result, 0, sizeof(uuid_t));
        memcpy(result, "on", 2);
        return 0;
    }

    if(uuid_parse(uid_str, result))
    {
        return 1;
    }
    return 0;
}

void
schedule_uid_unparse(const uuid_t uid, char *result)
{
    uuid_t tmp_uuid;

    memset(tmp_uuid, 0, sizeof(uuid_t));
    memcpy(tmp_uuid, "off", 3);
    if(uuid_compare(uid, tmp_uuid) == 0)
    {
        strcpy(result, "off");
        return;
    }

    memset(tmp_uuid, 0, sizeof(uuid_t));
    memcpy(tmp_uuid, "on", 2);
    if(uuid_compare(uid, tmp_uuid) == 0)
    {
        strcpy(result, "on");
        return;
    }

    uuid_unparse(uid, result);
}