#include "channel.h"

#include <ctype.h>
#include <dirent.h>
#include <math.h>
#include <libavformat/avformat.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <sys/stat.h>

static int has_supported_extension(const char *name) {
    const char *dot = strrchr(name, '.');
    if (!dot) {
        return 0;
    }

    dot++;
    return strcasecmp(dot, "mp4") == 0 ||
           strcasecmp(dot, "mkv") == 0 ||
           strcasecmp(dot, "mov") == 0 ||
           strcasecmp(dot, "avi") == 0 ||
           strcasecmp(dot, "m4v") == 0;
}

static int path_is_directory(const char *path) {
    struct stat st;
    return path && stat(path, &st) == 0 && S_ISDIR(st.st_mode);
}

static void trim_extension(char *text) {
    char *dot = strrchr(text, '.');
    if (dot) {
        *dot = '\0';
    }
}

static void make_title_case(char *text) {
    int new_word = 1;
    for (size_t i = 0; text[i] != '\0'; ++i) {
        if (text[i] == '-' || text[i] == '_' || text[i] == '.') {
            text[i] = ' ';
            new_word = 1;
            continue;
        }
        if (isspace((unsigned char) text[i])) {
            new_word = 1;
            continue;
        }
        text[i] = new_word ? (char) toupper((unsigned char) text[i]) : (char) tolower((unsigned char) text[i]);
        new_word = 0;
    }
}

static void safe_copy(char *out, size_t out_size, const char *value) {
    if (!out || out_size == 0) {
        return;
    }
    snprintf(out, out_size, "%s", value ? value : "");
}

static void derive_display_name(const char *source, char *out, size_t out_size) {
    char temp[256];
    safe_copy(temp, sizeof(temp), source);
    trim_extension(temp);
    make_title_case(temp);
    safe_copy(out, out_size, temp);
}

static void derive_program_title(const char *file_name, char *out, size_t out_size) {
    derive_display_name(file_name, out, out_size);
}

static char *trim_whitespace(char *text) {
    char *end;
    while (*text && isspace((unsigned char) *text)) {
        text++;
    }
    end = text + strlen(text);
    while (end > text && isspace((unsigned char) end[-1])) {
        end--;
    }
    *end = '\0';
    return text;
}

static double probe_duration_seconds(const char *file_path) {
    AVFormatContext *format_context = NULL;
    double duration = 0.0;

    if (avformat_open_input(&format_context, file_path, NULL, NULL) < 0) {
        return 0.0;
    }

    if (avformat_find_stream_info(format_context, NULL) >= 0 && format_context->duration > 0) {
        duration = (double) format_context->duration / (double) AV_TIME_BASE;
    }

    avformat_close_input(&format_context);
    return duration;
}

static int compare_programs(const void *left, const void *right) {
    const ProgramEntry *a = (const ProgramEntry *) left;
    const ProgramEntry *b = (const ProgramEntry *) right;
    return strcmp(a->file_name, b->file_name);
}

static int compare_channels(const void *left, const void *right) {
    const Channel *a = (const Channel *) left;
    const Channel *b = (const Channel *) right;
    int a_has_number = a->number > 0;
    int b_has_number = b->number > 0;

    if (a_has_number && b_has_number && a->number != b->number) {
        return a->number - b->number;
    }
    if (a_has_number != b_has_number) {
        return a_has_number ? -1 : 1;
    }
    return strcmp(a->name, b->name);
}

static int parse_channel_metadata(Channel *channel, const char *channel_dir) {
    char metadata_path[PATH_MAX];
    FILE *file;
    char line[512];

    snprintf(metadata_path, sizeof(metadata_path), "%s/description.txt", channel_dir);
    file = fopen(metadata_path, "r");
    if (!file) {
        return 0;
    }

    while (fgets(line, sizeof(line), file) != NULL) {
        char *equals = strchr(line, '=');
        char *key;
        char *value;
        if (!equals) {
            continue;
        }
        *equals = '\0';
        key = trim_whitespace(line);
        value = trim_whitespace(equals + 1);
        if (strcasecmp(key, "name") == 0) {
            safe_copy(channel->name, sizeof(channel->name), value);
        } else if (strcasecmp(key, "number") == 0) {
            channel->number = atoi(value);
        } else if (strcasecmp(key, "description") == 0) {
            safe_copy(channel->description, sizeof(channel->description), value);
        }
    }

    fclose(file);
    return 0;
}

static int load_channel_programs(Channel *channel, const char *channel_dir) {
    DIR *directory;
    struct dirent *entry;
    int capacity = 8;

    directory = opendir(channel_dir);
    if (!directory) {
        return -1;
    }

    channel->programs = calloc((size_t) capacity, sizeof(ProgramEntry));
    if (!channel->programs) {
        closedir(directory);
        return -1;
    }

    while ((entry = readdir(directory)) != NULL) {
        ProgramEntry *program;
        char file_path[PATH_MAX];

        if (entry->d_name[0] == '.' || !has_supported_extension(entry->d_name)) {
            continue;
        }

        snprintf(file_path, sizeof(file_path), "%s/%s", channel_dir, entry->d_name);
        if (path_is_directory(file_path)) {
            continue;
        }

        if (channel->program_count == capacity) {
            ProgramEntry *resized;
            capacity *= 2;
            resized = realloc(channel->programs, (size_t) capacity * sizeof(ProgramEntry));
            if (!resized) {
                closedir(directory);
                return -1;
            }
            channel->programs = resized;
        }

        program = &channel->programs[channel->program_count];
        memset(program, 0, sizeof(*program));
        safe_copy(program->file_name, sizeof(program->file_name), entry->d_name);
        safe_copy(program->file_path, sizeof(program->file_path), file_path);
        derive_program_title(entry->d_name, program->program_title, sizeof(program->program_title));
        program->duration_seconds = probe_duration_seconds(program->file_path);
        channel->program_count += 1;
    }

    closedir(directory);

    if (channel->program_count == 0) {
        free(channel->programs);
        channel->programs = NULL;
        return 0;
    }

    for (int i = channel->program_count - 1; i > 0; i--) {
        int j = rand() % (i + 1);
        ProgramEntry temp = channel->programs[i];
        channel->programs[i] = channel->programs[j];
        channel->programs[j] = temp;
    }

    channel->total_duration_seconds = 0.0;
    for (int i = 0; i < channel->program_count; ++i) {
        channel->programs[i].start_offset_seconds = channel->total_duration_seconds;
        channel->total_duration_seconds += channel->programs[i].duration_seconds;
    }

    return channel->program_count;
}

int channel_list_load(ChannelList *list, const char *media_dir) {
    DIR *directory;
    struct dirent *entry;
    int capacity = 8;

    if (!list || !media_dir) {
        return -1;
    }

    list->items = calloc((size_t) capacity, sizeof(Channel));
    list->count = 0;
    if (!list->items) {
        return -1;
    }

    directory = opendir(media_dir);
    if (!directory) {
        free(list->items);
        list->items = NULL;
        return -1;
    }

    while ((entry = readdir(directory)) != NULL) {
        Channel *channel;
        char channel_dir[PATH_MAX];
        int load_result;

        if (entry->d_name[0] == '.') {
            continue;
        }

        snprintf(channel_dir, sizeof(channel_dir), "%s/%s", media_dir, entry->d_name);
        if (!path_is_directory(channel_dir)) {
            continue;
        }

        if (list->count == capacity) {
            Channel *resized;
            capacity *= 2;
            resized = realloc(list->items, (size_t) capacity * sizeof(Channel));
            if (!resized) {
                closedir(directory);
                channel_list_destroy(list);
                return -1;
            }
            list->items = resized;
        }

        channel = &list->items[list->count];
        memset(channel, 0, sizeof(*channel));
        derive_display_name(entry->d_name, channel->name, sizeof(channel->name));
        safe_copy(channel->description, sizeof(channel->description), "Local programming lineup.");
        parse_channel_metadata(channel, channel_dir);
        load_result = load_channel_programs(channel, channel_dir);
        if (load_result < 0) {
            closedir(directory);
            channel_list_destroy(list);
            return -1;
        }
        if (load_result == 0 || channel->total_duration_seconds <= 0.0) {
            free(channel->programs);
            memset(channel, 0, sizeof(*channel));
            continue;
        }
        list->count += 1;
    }

    closedir(directory);

    if (list->count == 0) {
        channel_list_destroy(list);
        return 0;
    }

    qsort(list->items, (size_t) list->count, sizeof(Channel), compare_channels);
    for (int i = 0; i < list->count; ++i) {
        if (list->items[i].number <= 0) {
            list->items[i].number = 50 + i;
        }
    }

    return list->count;
}

void channel_list_destroy(ChannelList *list) {
    if (!list) {
        return;
    }

    if (list->items) {
        for (int i = 0; i < list->count; ++i) {
            free(list->items[i].programs);
        }
    }
    free(list->items);
    list->items = NULL;
    list->count = 0;
}

time_t channel_wall_time_from_ticks(time_t app_start_time, Uint64 app_start_ticks, Uint64 now_ticks) {
    Uint64 elapsed_ticks;

    if (now_ticks < app_start_ticks) {
        now_ticks = app_start_ticks;
    }

    elapsed_ticks = now_ticks - app_start_ticks;
    return app_start_time + (time_t) (elapsed_ticks / 1000);
}

double channel_schedule_elapsed_seconds(time_t app_start_time, time_t target_time) {
    double elapsed = difftime(target_time, app_start_time);

    if (elapsed < 0.0) {
        return 0.0;
    }

    return elapsed;
}

double channel_live_position(const Channel *channel, time_t app_start_time, time_t now) {
    double elapsed;

    if (!channel || channel->total_duration_seconds <= 0.0) {
        return 0.0;
    }

    elapsed = difftime(now, app_start_time);
    if (elapsed < 0.0) {
        elapsed = 0.0;
    }

    return fmod(elapsed, channel->total_duration_seconds);
}

double channel_live_position_precise(const Channel *channel, Uint64 app_start_ticks, Uint64 now_ticks) {
    double elapsed;

    if (!channel || channel->total_duration_seconds <= 0.0) {
        return 0.0;
    }

    if (now_ticks < app_start_ticks) {
        now_ticks = app_start_ticks;
    }

    elapsed = (double) (now_ticks - app_start_ticks) / 1000.0;
    return fmod(elapsed, channel->total_duration_seconds);
}

const ProgramEntry *channel_resolve_program_at_elapsed(const Channel *channel,
                                                       double elapsed_seconds,
                                                       double *program_seek_seconds,
                                                       int *program_index) {
    double channel_offset;

    if (!channel || channel->program_count == 0 || channel->total_duration_seconds <= 0.0) {
        return NULL;
    }

    if (elapsed_seconds < 0.0) {
        elapsed_seconds = 0.0;
    }

    channel_offset = fmod(elapsed_seconds, channel->total_duration_seconds);
    if (channel_offset < 0.0) {
        channel_offset += channel->total_duration_seconds;
    }

    for (int i = 0; i < channel->program_count; ++i) {
        const ProgramEntry *program = &channel->programs[i];
        double end_offset = program->start_offset_seconds + program->duration_seconds;

        if (program->duration_seconds <= 0.0) {
            continue;
        }

        if (channel_offset < end_offset || i == channel->program_count - 1) {
            if (program_seek_seconds) {
                *program_seek_seconds = channel_offset - program->start_offset_seconds;
                if (*program_seek_seconds < 0.0) {
                    *program_seek_seconds = 0.0;
                }
            }
            if (program_index) {
                *program_index = i;
            }
            return program;
        }
    }

    return &channel->programs[0];
}

const ProgramEntry *channel_program_at_index(const Channel *channel, int program_index) {
    if (!channel || program_index < 0 || program_index >= channel->program_count) {
        return NULL;
    }
    return &channel->programs[program_index];
}

const ProgramEntry *channel_resolve_program(const Channel *channel,
                                            Uint64 app_start_ticks,
                                            Uint64 now_ticks,
                                            double *program_seek_seconds,
                                            int *program_index) {
    double elapsed;

    if (!channel || channel->program_count == 0 || channel->total_duration_seconds <= 0.0) {
        return NULL;
    }

    if (now_ticks < app_start_ticks) {
        now_ticks = app_start_ticks;
    }

    elapsed = (double) (now_ticks - app_start_ticks) / 1000.0;
    return channel_resolve_program_at_elapsed(channel, elapsed, program_seek_seconds, program_index);
}
