/*
 * clock.c
 *
 * Copyright (C) 2013 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 <clock.h>
#include <exception.h>
#include <syscalls.h>
#include <irq.h>
#include <user.h>

static clock_time_t clock_current_time;
static byte_t clock_settings;

void clock_irq(registers_t *regs, byte_t irq_num)
{
    bool_t pm = FALSE;

    cpu_write_port_byte(CMOS_CMD_PORT, CMOS_RTC_SECONDS_REG);
    byte_t seconds = cpu_read_port_byte(CMOS_DATA_PORT);
    cpu_write_port_byte(CMOS_CMD_PORT, CMOS_RTC_MINUTES_REG);
    byte_t minutes = cpu_read_port_byte(CMOS_DATA_PORT);
    cpu_write_port_byte(CMOS_CMD_PORT, CMOS_RTC_HOURS_REG);
    byte_t hours = cpu_read_port_byte(CMOS_DATA_PORT);
    cpu_write_port_byte(CMOS_CMD_PORT, CMOS_RTC_DAY_REG);
    byte_t day = cpu_read_port_byte(CMOS_DATA_PORT);
    cpu_write_port_byte(CMOS_CMD_PORT, CMOS_RTC_MONTH_REG);
    byte_t month = cpu_read_port_byte(CMOS_DATA_PORT);
    cpu_write_port_byte(CMOS_CMD_PORT, CMOS_RTC_YEAR_REG);
    byte_t year = cpu_read_port_byte(CMOS_DATA_PORT);

    if (hours & CLOCK_PM_BIT) pm = TRUE;
    hours &= ~CLOCK_PM_BIT;

    if (!(clock_settings & CLOCK_BINARY_MODE))
    {
        hours = (hours >> 4) * 10 + (hours & 0x0F);
        minutes = (minutes >> 4) * 10 + (minutes & 0x0F);
        seconds = (seconds >> 4) * 10 + (seconds & 0x0F);
        day = (day >> 4) * 10 + (day & 0x0F);
        month = (month >> 4) * 10 + (month & 0x0F);
        year = (year >> 4) * 10 + (year & 0x0F);
    }

    if (!(clock_settings & CLOCK_24HOUR_MODE))
    {
        if (pm) hours += 12;
        else if (hours == 12) hours = 0;
    }

    clock_current_time.seconds = seconds;
    clock_current_time.minutes = minutes;
    clock_current_time.hours = hours;
    clock_current_time.day = day;
    clock_current_time.month = month;
    clock_current_time.year = year;

    cpu_write_port_byte(CMOS_CMD_PORT, CMOS_RTC_STC_REG);
    cpu_read_port_byte(CMOS_DATA_PORT);
}

bool_t clock_check_time(clock_time_t *time)
{
    byte_t max_days[] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
    if (time->year % 4 == 0 && (time->year % 100 != 0 || time->year % 400 == 0)) max_days[1]++;

    if (time->seconds >= 60) return FALSE;
    if (time->minutes >= 60) return FALSE;
    if (time->hours >= 24) return FALSE;
    if (time->month > 12 || time->month == 0) return FALSE;
    if (time->day > max_days[time->month - 1]) return FALSE;

    return TRUE;
}

sysret_t syscall_clock_get_time(clock_time_t *time)
{
    if (get_previous_mode() != KERNEL_MODE && !check_usermode(time, sizeof(clock_time_t)))
    {
        return ERR_BADPTR;
    }

    EH_TRY
    {
        time->seconds = clock_current_time.seconds;
        time->minutes = clock_current_time.minutes;
        time->hours = clock_current_time.hours;
        time->day = clock_current_time.day;
        time->month = clock_current_time.month;
        time->year = clock_current_time.year;
    }
    EH_CATCH
    {
        EH_ESCAPE(return ERR_BADPTR);
    }
    EH_DONE;

    return ERR_SUCCESS;
}

sysret_t syscall_clock_set_time(clock_time_t *time)
{
    clock_time_t safe_time = {0};

    if (get_previous_mode() == USER_MODE)
    {
        if (!check_usermode(time, sizeof(clock_time_t))) return ERR_BADPTR;

        EH_TRY safe_time = *time;
        EH_CATCH EH_ESCAPE(return ERR_BADPTR);
        EH_DONE;

        if (!check_privileges(PRIVILEGE_SET_TIME)) return ERR_FORBIDDEN;
    }
    else
    {
        safe_time = *time;
    }

    if (!clock_check_time(&safe_time)) return ERR_INVALID;

    byte_t seconds = safe_time.seconds;
    byte_t minutes = safe_time.minutes;
    byte_t hours = safe_time.hours;
    byte_t day = safe_time.day;
    byte_t month = safe_time.month;
    byte_t year = safe_time.year;

    if (!(clock_settings & CLOCK_BINARY_MODE))
    {
        hours = ((hours / 10) << 4) + (hours % 10);
        minutes = ((minutes / 10) << 4) + (minutes % 10);
        seconds = ((seconds / 10) << 4) + (seconds % 10);
        day = ((day / 10) << 4) + (day % 10);
        month = ((month / 10) << 4) + (month % 10);
        year = ((year / 10) << 4) + (year % 10);
    }

    if (!(clock_settings & CLOCK_24HOUR_MODE) && (hours > 12)) hours |= CLOCK_PM_BIT;

    critical_t critical;
    enter_critical(&critical);

    clock_current_time.seconds = safe_time.seconds;
    clock_current_time.minutes = safe_time.minutes;
    clock_current_time.hours = safe_time.hours;
    clock_current_time.day = safe_time.day;
    clock_current_time.month = safe_time.month;
    clock_current_time.year = safe_time.year;

    disable_nmi();
    cpu_write_port_byte(CMOS_CMD_PORT, CMOS_RTC_SECONDS_REG);
    cpu_write_port_byte(CMOS_DATA_PORT, seconds);
    cpu_write_port_byte(CMOS_CMD_PORT, CMOS_RTC_MINUTES_REG);
    cpu_write_port_byte(CMOS_DATA_PORT, minutes);
    cpu_write_port_byte(CMOS_CMD_PORT, CMOS_RTC_HOURS_REG);
    cpu_write_port_byte(CMOS_DATA_PORT, hours);
    cpu_write_port_byte(CMOS_CMD_PORT, CMOS_RTC_DAY_REG);
    cpu_write_port_byte(CMOS_DATA_PORT, day);
    cpu_write_port_byte(CMOS_CMD_PORT, CMOS_RTC_MONTH_REG);
    cpu_write_port_byte(CMOS_DATA_PORT, month);
    cpu_write_port_byte(CMOS_CMD_PORT, CMOS_RTC_YEAR_REG);
    cpu_write_port_byte(CMOS_DATA_PORT, year);
    enable_nmi();

    leave_critical(&critical);
    return ERR_SUCCESS;
}

void clock_init()
{
    critical_t critical;

    enter_critical(&critical);
    cpu_write_port_byte(CMOS_CMD_PORT, CMOS_RTC_STB_REG);
    clock_settings = cpu_read_port_byte(CMOS_DATA_PORT);

    if ((clock_settings & (CLOCK_ALARM_INT | CLOCK_PERIODIC_INT | CLOCK_UPDATE_INT)) != CLOCK_UPDATE_INT)
    {
        clock_settings &= ~(CLOCK_ALARM_INT | CLOCK_PERIODIC_INT);
        clock_settings |= CLOCK_UPDATE_INT;

        disable_nmi();
        cpu_write_port_byte(CMOS_CMD_PORT, CMOS_RTC_STB_REG);
        cpu_write_port_byte(CMOS_DATA_PORT, clock_settings);
        enable_nmi();
    }

    register_irq_handler(CLOCK_IRQ, &clock_irq, FALSE);
    cpu_write_port_byte(CMOS_CMD_PORT, CMOS_RTC_STC_REG);
    cpu_read_port_byte(CMOS_DATA_PORT);
    leave_critical(&critical);
}
