/*
 * video.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 <video.h>
#include <exception.h>
#include <pci.h>
#include <heap.h>
#include <memory.h>
#include <process.h>
#include <syscalls.h>
#include <stdio.h>

static dword_t video_device_init(void);
static dword_t video_device_cleanup(void);
static dword_t video_device_read_write(device_t *device, void *buffer, size_t length, size_t *bytes_read);
static dword_t video_device_ioctl(device_t *device, dword_t control_code, const void *in_buffer, size_t in_length, void *out_buffer, size_t out_length);

static dword_t video_default_init(list_entry_t *video_devices);
static dword_t video_default_cleanup(void);

static char_dev_driver_t video_subsystem =
{
    .init_proc = video_device_init,
    .cleanup_proc = video_device_cleanup,
    .read_proc = (char_dev_read_proc_t)video_device_read_write,
    .write_proc = (char_dev_write_proc_t)video_device_read_write,
    .ioctl_proc = video_device_ioctl,
};

static video_driver_t video_default_driver =
{
    .init_proc = video_default_init,
    .cleanup_proc = video_default_cleanup,
    .control_proc = video_default_control,
};

static DECLARE_LIST(video_devices);

static dword_t video_default_bitblt(video_device_t *device, video_bitblt_parameters_t *parameters)
{
    dword_t i, j;
    dword_t *framebuffer;
    video_map_framebuffer_t map;

    if (device->current_mode.bpp > 32) return ERR_INVALID;

    dword_t top_x = MIN(parameters->source_x, parameters->dest_x);
    dword_t top_y = MIN(parameters->source_y, parameters->dest_y);
    dword_t bottom_x = MAX(parameters->source_x, parameters->dest_x);
    dword_t bottom_y = MAX(parameters->source_y, parameters->dest_y);

    map.address = NULL;
    map.offset = top_y * device->current_mode.scanline_size + ((top_x * device->current_mode.bpp) >> 3);
    map.size = (bottom_y * device->current_mode.scanline_size + ((bottom_x * device->current_mode.bpp + 7) >> 3)) - map.offset;

    processor_mode_t old_previous_mode = get_current_thread()->previous_mode;
    get_current_thread()->previous_mode = KERNEL_MODE;
    dword_t ret = device->driver->control_proc(device, IOCTL_VIDEO_MAP_FRAMEBUFFER, &map, sizeof(map), &framebuffer, sizeof(framebuffer));
    get_current_thread()->previous_mode = old_previous_mode;
    if (ret != ERR_SUCCESS) return ret;

    for (i = 0; i < parameters->height; i++)
    {
        dword_t sp_y = parameters->source_y >= parameters->dest_y ? i : parameters->height - i - 1;
        dword_t dp_y = sp_y + parameters->dest_y - parameters->source_y;

        for (j = 0; j < parameters->width; j++)
        {
            dword_t sp_x = parameters->source_x >= parameters->dest_x ? j : parameters->width - j - 1;
            dword_t dp_x = sp_x + parameters->dest_x - parameters->source_x;

            dword_t source_pixel = 0;
            dword_t dest_pixel = 0;

            uintptr_t source_offset = (sp_y * device->current_mode.scanline_size << 3) + sp_x * device->current_mode.bpp - (map.offset << 3);
            uintptr_t dest_offset = (dp_y * device->current_mode.scanline_size << 3) + dp_x * device->current_mode.bpp - (map.offset << 3);

            qword_t mask = ((qword_t)((1 << device->current_mode.bpp) - 1)) << (source_offset & 0x1F);
            dword_t low_mask = (dword_t)mask;
            dword_t high_mask = (dword_t)(mask >> 32);

            if (parameters->operation & BITBLT_SOURCE_ENABLED)
            {
                source_pixel = framebuffer[source_offset >> 5] >> (source_offset & 0x1F);
                if (high_mask) source_pixel |= framebuffer[(source_offset >> 5) + 1] << ((-dest_offset) & 0x1F);
                source_pixel &= (1 << device->current_mode.bpp) - 1;
            }

            if (parameters->operation & BITBLT_DEST_ENABLED)
            {
                dest_pixel = framebuffer[dest_offset >> 5] >> (dest_offset & 0x1F);
                if (high_mask) dest_pixel |= framebuffer[(dest_offset >> 5) + 1] << ((-dest_offset) & 0x1F);
                dest_pixel &= (1 << device->current_mode.bpp) - 1;
            }

            if (parameters->operation & BITBLT_SOURCE_INVERT) source_pixel = ~source_pixel;
            if (parameters->operation & BITBLT_DEST_INVERT) dest_pixel = ~dest_pixel;

            switch (parameters->operation & 3)
            {
            case BITBLT_OPERATION_NONE:
                dest_pixel = source_pixel;
                break;

            case BITBLT_OPERATION_AND:
                dest_pixel &= source_pixel;
                break;

            case BITBLT_OPERATION_OR:
                dest_pixel |= source_pixel;
                break;

            case BITBLT_OPERATION_XOR:
                dest_pixel ^= source_pixel;
                break;
            }

            if (parameters->operation & BITBLT_RESULT_INVERT) dest_pixel = ~dest_pixel;

            framebuffer[dest_offset >> 5] = (framebuffer[dest_offset >> 5] & ~low_mask) | (dest_pixel << (dest_offset & 0x1F));

            if (high_mask)
            {
                framebuffer[(dest_offset >> 5) + 1] = (framebuffer[(dest_offset >> 5) + 1] & ~high_mask) | (dest_pixel >> ((-dest_offset) & 0x1F));
            }
        }
    }

    unmap_memory(framebuffer);
    return ERR_SUCCESS;
}

static dword_t video_device_init(void)
{
    list_entry_t *pci_device_list = get_pci_device_list_head();
    list_entry_t *ptr;
    int number = 0;

    for (ptr = pci_device_list->next; ptr != pci_device_list; ptr = ptr->next)
    {
        pci_device_t *device = CONTAINER_OF(ptr, pci_device_t, list);

        if (device->class == PCI_DISPLAY_DEVICE)
        {
            video_device_t *video = (video_device_t*)malloc(sizeof(video_device_t));
            ASSERT(video != NULL);

            video->header.driver = &video_subsystem;
            sprintf(video->header.name, "Video%d", number);
            lock_init(&video->header.lock);
            video->pci_device = device;
            video->driver = &video_default_driver;

            memset(&video->current_mode, 0, sizeof(video->current_mode));
            video->current_mode.number = 3;
            video->current_mode.flags = VIDEO_MODE_ALPHANUMERIC | VIDEO_MODE_USES_PALETTE;
            video->current_mode.width = TEXT_WIDTH;
            video->current_mode.height = TEXT_HEIGHT;
            video->current_mode.bpp = 16;
            video->current_mode.scanline_size = TEXT_WIDTH * 2;
            video->current_mode.framebuffer_phys = TEXT_VIDEO_MEMORY;

            dword_t ret = register_char_device(&video->header);
            ASSERT(ret == ERR_SUCCESS);

            list_append(&video_devices, &video->list);
            number++;
        }
    }

    return ERR_SUCCESS;
}

static dword_t video_device_cleanup(void)
{
    return ERR_SUCCESS;
}

static dword_t video_device_read_write(device_t *device, void *buffer, size_t length, size_t *bytes_read)
{
    return ERR_INVALID;
}

static dword_t video_device_ioctl(device_t *device,
                                  dword_t control_code,
                                  const void *in_buffer,
                                  size_t in_length,
                                  void *out_buffer,
                                  size_t out_length)
{
    video_device_t *video = CONTAINER_OF(device, video_device_t, header);
    return video->driver->control_proc(video, control_code, in_buffer, in_length, out_buffer, out_length);
}

static dword_t video_default_init(list_entry_t *video_devices)
{
    UNUSED_PARAMETER(video_devices);
    return ERR_SUCCESS;
}

static dword_t video_default_cleanup(void)
{
    return ERR_SUCCESS;
}

dword_t video_default_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_GET_MODE:
        if (out_length >= sizeof(video_mode_t))
        {
            *(video_mode_t*)out_buffer = device->current_mode;
            return ERR_SUCCESS;
        }
        else
        {
            return ERR_SMALLBUF;
        }

    case IOCTL_VIDEO_SET_MODE:
        if (in_length >= sizeof(dword_t))
        {
            return (*(dword_t*)in_buffer == device->current_mode.number) ? ERR_SUCCESS : ERR_INVALID;
        }
        else
        {
            return ERR_SMALLBUF;
        }

    case IOCTL_VIDEO_LIST_MODES:
        if (out_length >= sizeof(dword_t))
        {
            dword_t *list = (dword_t*)out_buffer;
            list[0] = 1;

            if (out_length >= 2 * sizeof(dword_t))
            {
                list[1] = device->current_mode.number;
                return ERR_SUCCESS;
            }
            else
            {
                return ERR_SMALLBUF;
            }
        }
        else
        {
            return ERR_SMALLBUF;
        }

    case IOCTL_VIDEO_QUERY_MODE:
        if (in_length >= sizeof(dword_t) && out_length >= sizeof(video_mode_t))
        {
            if (*(dword_t*)in_buffer != device->current_mode.number) return ERR_INVALID;

            *(video_mode_t*)out_buffer = device->current_mode;
            return ERR_SUCCESS;
        }
        else
        {
            return ERR_SMALLBUF;
        }

    case IOCTL_VIDEO_MAP_FRAMEBUFFER:
        if (in_length >= sizeof(video_map_framebuffer_t) && out_length >= sizeof(void*))
        {
            video_map_framebuffer_t *params = (video_map_framebuffer_t*)in_buffer;
            void *address = params->address;
            dword_t size = device->current_mode.height * device->current_mode.scanline_size * (device->current_mode.bpp >> 3);
            if (params->offset >= size || params->size > size || (params->offset + params->size) > size) return ERR_INVALID;

            if (get_previous_mode() == USER_MODE)
            {
                dword_t ret = map_memory_in_address_space(&get_current_process()->memory_space,
                                                          (void*)(device->current_mode.framebuffer_phys + params->offset),
                                                          &address,
                                                          params->size,
                                                          MEMORY_BLOCK_ACCESSIBLE | MEMORY_BLOCK_WRITABLE | MEMORY_BLOCK_USERMODE);
                if (ret != ERR_SUCCESS) return ret;
            }
            else
            {
                dword_t ret = map_memory((void*)(device->current_mode.framebuffer_phys + params->offset),
                                         &address,
                                         params->size,
                                         MEMORY_BLOCK_ACCESSIBLE | MEMORY_BLOCK_WRITABLE);
                if (ret != ERR_SUCCESS) return ret;
            }

            *(void**)out_buffer = address;
            return ERR_SUCCESS;
        }
        else
        {
            return ERR_SMALLBUF;
        }

    case IOCTL_VIDEO_BITBLT:
        if (in_length >= sizeof(video_bitblt_parameters_t))
        {
            return video_default_bitblt(device, (video_bitblt_parameters_t*)in_buffer);
        }
        else
        {
            return ERR_SMALLBUF;
        }

    case IOCTL_VIDEO_READ_PALETTE:
    case IOCTL_VIDEO_WRITE_PALETTE:
    case IOCTL_VIDEO_READ_FONT:
    case IOCTL_VIDEO_WRITE_FONT:
        return ERR_NOSYSCALL; // TODO

    case IOCTL_VIDEO_SET_TEXT_CURSOR:
        if (in_length >= sizeof(video_cursor_location_t))
        {
            video_cursor_location_t *location = (video_cursor_location_t*)in_buffer;
            word_t index = location->row * TEXT_WIDTH + location->column;

            cpu_write_port_byte(VGA_CRTC_INDEX, 0x0F);
            cpu_write_port_byte(VGA_CRTC_DATA, (byte_t) (index & 0xFF));
            cpu_write_port_byte(VGA_CRTC_INDEX, 0x0E);
            cpu_write_port_byte(VGA_CRTC_DATA, (byte_t) ((index >> 8) & 0xFF));
        }
        else
        {
            return ERR_SMALLBUF;
        }

    default:
        return ERR_INVALID;
    }
}

dword_t register_video_driver(video_driver_t *driver)
{
    dword_t ret = driver->init_proc(&video_devices);
    return ret;
}

dword_t unregister_video_driver(video_driver_t *driver)
{
    driver->cleanup_proc();
    return ERR_SUCCESS;
}

void video_init()
{
    dword_t ret = register_char_dev_driver(&video_subsystem);
    ASSERT(ret == ERR_SUCCESS);

    video_map_framebuffer_t params = {
        .address = (void*)VIDEO_MEMORY,
        .offset = 0,
        .size = TEXT_WIDTH * TEXT_HEIGHT * 2
    };

    void *address;
    handle_t handle;

    ASSERT(get_previous_mode() == KERNEL_MODE);

    ret = syscall_open_file("CharDevices/Video0", &handle, 0, 0);
    ASSERT(ret == ERR_SUCCESS);

    ret = syscall_device_ioctl(handle, IOCTL_VIDEO_MAP_FRAMEBUFFER, &params, sizeof(params), &address, sizeof(address));
    ASSERT(ret == ERR_SUCCESS);

    syscall_close_object(handle);
}
