/*
 * VESA BIOS Extensions Video Driver
 * main.c
 *
 * Copyright (C) 2016 Aleksandar Andrejevic <theflash@sdf.lonestar.org>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 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 Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

#include "vesa.h"
#include <memory.h>
#include <heap.h>
#include <vm86.h>

const char driver_name[] = "vesa";

static word_t available_mode_numbers[MAX_NUM_MODES];
static vbe_mode_info_t available_modes[MAX_NUM_MODES];
static int num_available_modes = 0;

static dword_t vesa_init(list_entry_t *video_devices);
static dword_t vesa_cleanup(void);
static dword_t vesa_control(
    video_device_t *device,
    dword_t control_code,
    const void *in_buffer,
    size_t in_length,
    void *out_buffer,
    size_t out_length
);

static video_driver_t driver =
{
    .init_proc = vesa_init,
    .cleanup_proc = vesa_cleanup,
    .control_proc = vesa_control,
};

static dword_t vesa_query_mode(dword_t mode_number, video_mode_t *mode_info)
{
    int i;
    for (i = 0; i < num_available_modes; i++) if (available_mode_numbers[i] == mode_number) break;
    if (i == num_available_modes) return ERR_INVALID;

    mode_info->number = mode_number;
    mode_info->flags = (available_modes[i].mem_model_type == 0) ? VIDEO_MODE_ALPHANUMERIC : 0;
    mode_info->width = available_modes[i].width;
    mode_info->height = available_modes[i].height;
    mode_info->bpp = available_modes[i].bpp;
    mode_info->scanline_size = available_modes[i].bytes_per_sl;
    mode_info->framebuffer_phys = available_modes[i].video_buffer;
    mode_info->red_mask_size = available_modes[i].red_mask_size;
    mode_info->red_field_pos = available_modes[i].red_field_pos;
    mode_info->green_mask_size = available_modes[i].green_mask_size;
    mode_info->green_field_pos = available_modes[i].green_field_pos;
    mode_info->blue_mask_size = available_modes[i].blue_mask_size;
    mode_info->blue_field_pos = available_modes[i].blue_field_pos;
    mode_info->reserved_mask_size = available_modes[i].reserved_mask_size;
    mode_info->reserved_field_pos = available_modes[i].reserved_field_pos;

    return ERR_SUCCESS;
}

static dword_t vesa_set_mode(dword_t mode_number)
{
    vm86_registers_t regs = { .eax = 0x4F02, .ebx = mode_number };
    vm86_interrupt(0x10, &regs);
    return (regs.eax == 0x004F) ? ERR_SUCCESS : ERR_HARDWARE;
}

dword_t vesa_init(list_entry_t *video_devices)
{
    int i;
    dword_t ret;
    vbe_controller_info_t info;

    if (video_devices->next == video_devices) return ERR_NOTFOUND;

    word_t data_segment = vm86_alloc(VBE_BUFFER_SIZE);
    if (data_segment == 0) return ERR_NOMEMORY;

    vm86_registers_t regs = {.eax = 0x4F00,.es = data_segment, .edi = 0x0000 };
    vm86_interrupt(0x10, &regs);
    if (regs.eax != 0x4F)
    {
        ret = ERR_HARDWARE;
        goto cleanup;
    }

    ret = read_physical((void*)(data_segment << 4), &info, sizeof(vbe_controller_info_t));
    if (ret != ERR_SUCCESS) goto cleanup;

    word_t *modes = (word_t*)((info.videomodes[1] << 4) + info.videomodes[0]);
    word_t *numbers = __builtin_alloca(sizeof(word_t) * MAX_NUM_MODES);
    ret = read_physical(modes, numbers, sizeof(word_t) * MAX_NUM_MODES);
    if (ret != ERR_SUCCESS) goto cleanup;

    num_available_modes = 0;

    for (i = 0; (numbers[i] != 0xFFFF) && (i < MAX_NUM_MODES); i++)
    {
        vbe_mode_info_t mode_info;

        regs.eax = 0x4F01;
        regs.ecx = numbers[i];
        regs.es = data_segment;
        regs.edi = 0x0000;
        vm86_interrupt(0x10, &regs);
        if (regs.eax != 0x4F) continue;

        ret = read_physical((void*)(data_segment << 4), &mode_info, sizeof(vbe_mode_info_t));
        if (mode_info.mem_model_type != 0 && mode_info.mem_model_type != 4 && mode_info.mem_model_type != 6) continue;

        available_mode_numbers[num_available_modes] = numbers[i];
        available_modes[num_available_modes] = mode_info;
        num_available_modes++;
    }

    if (ret == ERR_SUCCESS)
    {
        CONTAINER_OF(video_devices->next, video_device_t, list)->driver = &driver;
    }

cleanup:
    vm86_free(data_segment);
    return ret;
}

static dword_t vesa_cleanup(void)
{
    return ERR_SUCCESS;
}

static dword_t vesa_control(video_device_t *device,
                            dword_t control_code,
                            const void *in_buffer,
                            size_t in_length,
                            void *out_buffer,
                            size_t out_length)
{
    switch (control_code)
    {
    case IOCTL_VIDEO_SET_MODE:
        if (in_length >= sizeof(dword_t))
        {
            video_mode_t mode_info;
            dword_t mode_number = *((dword_t*)in_buffer);

            dword_t ret = vesa_query_mode(mode_number, &mode_info);
            if (ret != ERR_SUCCESS) return ret;

            ret = vesa_set_mode(mode_number);
            if (ret != ERR_SUCCESS) return ret;

            device->current_mode = mode_info;
            return ERR_SUCCESS;
        }
        else
        {
            return ERR_SMALLBUF;
        }

    case IOCTL_VIDEO_LIST_MODES:
        if (out_length >= sizeof(dword_t))
        {
            dword_t i;
            dword_t *list = (dword_t*)out_buffer;

            list[0] = num_available_modes;

            for (i = 0; i < num_available_modes && out_length >= ((i + 2) * sizeof(dword_t)); i++)
            {
                list[i + 1] = available_mode_numbers[i];
            }

            return (i < num_available_modes) ? ERR_SMALLBUF : ERR_SUCCESS;
        }
        else
        {
            return ERR_SMALLBUF;
        }

    case IOCTL_VIDEO_QUERY_MODE:
        if (in_length >= sizeof(dword_t) && out_length >= sizeof(video_mode_t))
        {
            return vesa_query_mode(*(dword_t*)in_buffer, (video_mode_t*)out_buffer);
        }
        else
        {
            return ERR_SMALLBUF;
        }

    default:
        return video_default_control(device, control_code, in_buffer, in_length, out_buffer, out_length);
    }
}

dword_t driver_load(void)
{
    return register_video_driver(&driver);
}
