/*
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 2 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 General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 */

/*
 * Purpose: virtual interrupt router (another one)
 *
 * Author: Stas Sergeev.
 *
 */
#include <stdint.h>
#include <assert.h>
#include "cpu.h"
#include "int.h"
#include "hlt.h"
#include "memory.h"
#include "port.h"
#include "chipset.h"
#include "emu.h"
#include "vint.h"

#define VINT_MAX 2
static int vi_used;
static uint16_t vint_hlt;

#define ON_PIC1(n) (vih[n].orig_irq >= 8)

struct vihandler {
    void (*handler)(int, int);
    void (*mask)(int, int);
    uint8_t irq;
    uint8_t orig_irq;
    uint8_t interrupt;
    int tweaked;
    unsigned tw_flags;
};
struct vihandler vih[VINT_MAX];

static void poll_pic0(uint8_t irq)
{
    port_outb(0x20, 0x0c); // OCW3, enter poll mode
    port_outb(0x20, irq);  // extension, may not work on real PIC
    /* see if it worked */
    assert(pic_get_isr() & (1 << irq));
}

static void poll_pic1(uint8_t irq)
{
    port_outb(0x20, 0x0c); // OCW3, enter poll mode
    port_outb(0x20, 2);    // extension, may not work on real PIC
    port_outb(0xa0, 0x0c);
    port_outb(0xa0, irq - 8);
    /* see if it worked */
    assert((pic_get_isr() & ((1 << irq) | 4)) == ((1 << irq) | 4));
}

static void full_eoi(void)
{
    port_outb(0xa0, 0x20);
    port_outb(0x20, 0x20);
}

static void do_ret(int vi_num)
{
    clear_IF();
    vih[vi_num].mask(vi_num, 0);
    do_iret();
}

int vint_is_masked(int vi_num, uint8_t *imr)
{
    uint16_t real_imr = (imr[1] << 8) | imr[0];
    return !!(real_imr & (1 << vih[vi_num].orig_irq));
}

static void vint_handler(uint16_t idx, HLT_ARG(arg))
{
    uint8_t imr[2];
    int masked;
    int vi_num = idx >> 1;

    if (idx & 1) {
        do_ret(vi_num);
        return;
    }

    imr[0] = port_inb(0x21);
    imr[1] = port_inb(0xa1);
    masked = vint_is_masked(vi_num, imr);
    if (masked) {
        h_printf("vint: masked, iret\n");
        do_eoi2_iret();
    } else {
        uint8_t irq = vih[vi_num].orig_irq;
        uint16_t port = (irq >= 8 ? PIC1_VECBASE_PORT : PIC0_VECBASE_PORT);
        uint8_t inum = port_inb(port) + (irq & 7);
        full_eoi();
        if (ON_PIC1(vi_num))
            poll_pic1(irq);
        else
            poll_pic0(irq);
        if (vih[vi_num].tweaked) {
            _IP++;  // skip hlt
            h_printf("vint: call to inum %x\n", inum);
            real_run_int(inum);
            vih[vi_num].mask(vi_num, 1);
        } else {
            h_printf("vint: jump to inum %x\n", inum);
            jmp_to(ISEG(inum), IOFF(inum));
        }
    }

    if (vih[vi_num].handler)
        vih[vi_num].handler(vi_num, masked);
}

void vint_post_irq_dpmi(int vi_num, int masked)
{
    full_eoi();
    if (!masked) {
        uint8_t irq = vih[vi_num].orig_irq;
        if (ON_PIC1(vi_num))
            poll_pic1(irq);
        else
            poll_pic0(irq);
    }
}

void vint_init(void)
{
    emu_hlt_t hlt_hdlr = HLT_INITIALIZER;

    hlt_hdlr.name = "vint";
    hlt_hdlr.func = vint_handler;
    hlt_hdlr.len = VINT_MAX * 2;
    vint_hlt = hlt_register_handler_vm86(hlt_hdlr);
}

void vint_setup(void)
{
    int i;

    for (i = 0; i < VINT_MAX; i++) {
        if (vih[i].interrupt)
            SETIVEC(vih[i].interrupt, BIOS_HLT_BLK_SEG, vint_hlt + 2 * i);
    }
}

int vint_register(void (*ack_handler)(int, int),
                  void (*mask_handler)(int, int),
                  int irq, int orig_irq, int inum)
{
    struct vihandler *vi = &vih[vi_used];
    assert(vi_used < VINT_MAX);
    vi->handler = ack_handler;
    vi->mask = mask_handler;
    vi->irq = irq;
    vi->orig_irq = orig_irq;
    vi->interrupt = inum;
    return vi_used++;
}

void vint_set_tweaked(int vi_num, int on, unsigned flags)
{
    struct vihandler *vi = &vih[vi_num];
    assert(vi_num < VINT_MAX);
    vi->tweaked = on;
    vi->tw_flags = flags;
}
