/*
 * 86Box	A hypervisor and IBM PC system emulator that specializes in
 *		running old operating systems and software designed for IBM
 *		PC systems and compatibles from 1981 through fairly recent
 *		system designs based on the PCI bus.
 *
 *		This file is part of the 86Box distribution.
 *
 *		Implementation of the SMC FDC37C669 Super I/O Chip.
 *
 *
 *
 * Author:	Miran Grca, <mgrca8@gmail.com>
 *		Copyright 2016-2018 Miran Grca.
 */
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <wchar.h>
#include <86box/86box.h>
#include <86box/io.h>
#include <86box/timer.h>
#include <86box/device.h>
#include <86box/pci.h>
#include <86box/lpt.h>
#include <86box/serial.h>
#include <86box/hdc.h>
#include <86box/hdc_ide.h>
#include <86box/fdd.h>
#include <86box/fdc.h>
#include <86box/sio.h>


typedef struct {
    uint8_t id, tries,
	    regs[42];
    int locked, rw_locked,
	cur_reg;
    fdc_t *fdc;
    serial_t *uart[2];
} fdc37c669_t;


static int	next_id = 0;


static uint16_t
make_port(fdc37c669_t *dev, uint8_t reg)
{
    uint16_t p = 0;
    uint16_t mask = 0;

    switch(reg) {
	case 0x20:
	case 0x21:
	case 0x22:
		mask = 0xfc;
		break;
	case 0x23:
		mask = 0xff;
		break;
	case 0x24:
	case 0x25:
		mask = 0xfe;
		break;
    }

    p = ((uint16_t) (dev->regs[reg] & mask)) << 2;
    if (reg == 0x22)
	p |= 6;

    return p;
}


static void
fdc37c669_write(uint16_t port, uint8_t val, void *priv)
{
    fdc37c669_t *dev = (fdc37c669_t *) priv;
    uint8_t index = (port & 1) ? 0 : 1;
    uint8_t valxor = 0;
    uint8_t max = 42;

    if (index) {
	if ((val == 0x55) && !dev->locked) {
		if (dev->tries) {
			dev->locked = 1;
			dev->tries = 0;
		} else
			dev->tries++;
	} else {
		if (dev->locked) {
			if (val < max)
				dev->cur_reg = val;
			if (val == 0xaa)
				dev->locked = 0;
		} else {
			if (dev->tries)
				dev->tries = 0;
		}
	}
	return;
    } else {
	if (dev->locked) {
		if ((dev->cur_reg < 0x18) && (dev->rw_locked))
			return;
		if ((dev->cur_reg >= 0x26) && (dev->cur_reg <= 0x27))
			return;
		if (dev->cur_reg == 0x29)
			return;
		valxor = val ^ dev->regs[dev->cur_reg];
		dev->regs[dev->cur_reg] = val;
	} else
		return;
    }

    switch(dev->cur_reg) {
	case 0:
		if (!dev->id && (valxor & 8)) {
			fdc_remove(dev->fdc);
			if ((dev->regs[0] & 8) && (dev->regs[0x20] & 0xc0))
				fdc_set_base(dev->fdc, make_port(dev, 0x20));
		}
		break;
	case 1:
		if (valxor & 4) {
			if (dev->id) {
				lpt2_remove();
				if ((dev->regs[1] & 4) && (dev->regs[0x23] >= 0x40)) 
					lpt2_init(make_port(dev, 0x23));
			} else {
				lpt1_remove();
				if ((dev->regs[1] & 4) && (dev->regs[0x23] >= 0x40)) 
					lpt1_init(make_port(dev, 0x23));
			}
		}
		if (valxor & 7)
			dev->rw_locked = (val & 8) ? 0 : 1;
		break;
	case 2:
		if (valxor & 8) {
			serial_remove(dev->uart[0]);
			if ((dev->regs[2] & 8) && (dev->regs[0x24] >= 0x40))
				serial_setup(dev->uart[0], make_port(dev, 0x24), (dev->regs[0x28] & 0xf0) >> 4);
		}
		if (valxor & 0x80) {
			serial_remove(dev->uart[1]);
			if ((dev->regs[2] & 0x80) && (dev->regs[0x25] >= 0x40))
				serial_setup(dev->uart[1], make_port(dev, 0x25), dev->regs[0x28] & 0x0f);
		}
		break;
	case 3:
		if (!dev->id && (valxor & 2))
			fdc_update_enh_mode(dev->fdc, (val & 2) ? 1 : 0);
		break;
	case 5:
		if (!dev->id && (valxor & 0x18))
			fdc_update_densel_force(dev->fdc, (val & 0x18) >> 3);
		if (!dev->id && (valxor & 0x20))
			fdc_set_swap(dev->fdc, (val & 0x20) >> 5);
		break;
	case 0xB:
		if (!dev->id && (valxor & 3))
			fdc_update_rwc(dev->fdc, 0, val & 3);
		if (!dev->id && (valxor & 0xC))
			fdc_update_rwc(dev->fdc, 1, (val & 0xC) >> 2);
		break;
	case 0x20:
		if (!dev->id && (valxor & 0xfc)) {
			fdc_remove(dev->fdc);
			if ((dev->regs[0] & 8) && (dev->regs[0x20] & 0xc0))
				fdc_set_base(dev->fdc, make_port(dev, 0x20));
		}
		break;
	case 0x23:
		if (valxor) {
			if (dev->id) {
				lpt2_remove();
				if ((dev->regs[1] & 4) && (dev->regs[0x23] >= 0x40)) 
					lpt2_init(make_port(dev, 0x23));
			} else {
				lpt1_remove();
				if ((dev->regs[1] & 4) && (dev->regs[0x23] >= 0x40)) 
					lpt1_init(make_port(dev, 0x23));
			}
		}
		break;
	case 0x24:
		if (valxor & 0xfe) {
			serial_remove(dev->uart[0]);
			if ((dev->regs[2] & 8) && (dev->regs[0x24] >= 0x40))
				serial_setup(dev->uart[0], make_port(dev, 0x24), (dev->regs[0x28] & 0xf0) >> 4);
		}
		break;
	case 0x25:
		if (valxor & 0xfe) {
			serial_remove(dev->uart[1]);
			if ((dev->regs[2] & 0x80) && (dev->regs[0x25] >= 0x40))
				serial_setup(dev->uart[1], make_port(dev, 0x25), dev->regs[0x28] & 0x0f);
		}
		break;
	case 0x27:
		if (valxor & 0xf) {
			if (dev->id)
				lpt2_irq(val & 0xf);
			else
				lpt1_irq(val & 0xf);
		}
		break;
	case 0x28:
		if (valxor & 0xf) {
			serial_remove(dev->uart[1]);
			if ((dev->regs[2] & 0x80) && (dev->regs[0x25] >= 0x40))
				serial_setup(dev->uart[1], make_port(dev, 0x25), dev->regs[0x28] & 0x0f);
		}
		if (valxor & 0xf0) {
			serial_remove(dev->uart[0]);
			if ((dev->regs[2] & 8) && (dev->regs[0x24] >= 0x40))
				serial_setup(dev->uart[0], make_port(dev, 0x24), (dev->regs[0x28] & 0xf0) >> 4);
		}
		break;
    }
}


static uint8_t
fdc37c669_read(uint16_t port, void *priv)
{
    fdc37c669_t *dev = (fdc37c669_t *) priv;
    uint8_t index = (port & 1) ? 0 : 1;
    uint8_t ret = 0xff;

    if (dev->locked) {
	    if (index)
		ret = dev->cur_reg;
	    else if ((dev->cur_reg >= 0x18) || !dev->rw_locked)
		ret = dev->regs[dev->cur_reg];
    }

    return ret;
}


static void
fdc37c669_reset(fdc37c669_t *dev)
{
    serial_remove(dev->uart[0]);
    serial_setup(dev->uart[0], SERIAL1_ADDR, SERIAL1_IRQ);

    serial_remove(dev->uart[1]);
    serial_setup(dev->uart[1], SERIAL2_ADDR, SERIAL2_IRQ);

    memset(dev->regs, 0, 42);
    dev->regs[0x00] = 0x28;
    dev->regs[0x01] = 0x9c;
    dev->regs[0x02] = 0x88;
    dev->regs[0x03] = 0x78;
    dev->regs[0x06] = 0xff;
    dev->regs[0x0d] = 0x03;
    dev->regs[0x0e] = 0x02;
    dev->regs[0x1e] = 0x80;	/* Gameport controller. */
    dev->regs[0x20] = (0x3f0 >> 2) & 0xfc;
    dev->regs[0x21] = (0x1f0 >> 2) & 0xfc;
    dev->regs[0x22] = ((0x3f6 >> 2) & 0xfc) | 1;
    if (dev->id == 1) {
	dev->regs[0x23] = (0x278 >> 2);

	lpt2_remove();
	lpt2_init(0x278);

	dev->regs[0x24] = (SERIAL3_ADDR >> 2) & 0xfe;
	dev->regs[0x25] = (SERIAL4_ADDR >> 2) & 0xfe;
    } else {
	fdc_reset(dev->fdc);

	lpt1_remove();
	lpt1_init(0x378);

	dev->regs[0x23] = (0x378 >> 2);

	dev->regs[0x24] = (SERIAL1_ADDR >> 2) & 0xfe;
	dev->regs[0x25] = (SERIAL2_ADDR >> 2) & 0xfe;
    }
    dev->regs[0x26] = (2 << 4) | 3;
    dev->regs[0x27] = (6 << 4) | (dev->id ? 5 : 7);
    dev->regs[0x28] = (4 << 4) | 3;

    dev->locked = 0;
    dev->rw_locked = 0;
}


static void
fdc37c669_close(void *priv)
{
    fdc37c669_t *dev = (fdc37c669_t *) priv;

    next_id = 0;

    free(dev);
}


static void *
fdc37c669_init(const device_t *info)
{
    fdc37c669_t *dev = (fdc37c669_t *) malloc(sizeof(fdc37c669_t));
    memset(dev, 0, sizeof(fdc37c669_t));

    dev->id = next_id;

    if (next_id != 1)
	dev->fdc = device_add(&fdc_at_smc_device);

    dev->uart[0] = device_add_inst(&ns16550_device, (next_id << 1) + 1);
    dev->uart[1] = device_add_inst(&ns16550_device, (next_id << 1) + 2);

    io_sethandler(info->local ? 0x370 : (next_id ? 0x370 : 0x3f0), 0x0002,
		  fdc37c669_read, NULL, NULL, fdc37c669_write, NULL, NULL, dev);

    fdc37c669_reset(dev);

    next_id++;

    return dev;
}


const device_t fdc37c669_device = {
    "SMC FDC37C669 Super I/O",
    "fdc37c669",
    0,
    0,
    fdc37c669_init, fdc37c669_close, NULL,
    { NULL }, NULL, NULL,
    NULL
};


const device_t fdc37c669_370_device = {
    "SMC FDC37C669 Super I/O (Port 370h)",
    "fdc37c669_370",
    0,
    1,
    fdc37c669_init, fdc37c669_close, NULL,
    { NULL }, NULL, NULL,
    NULL
};
