/*
 * This file is part of libsidplayfp, a SID player engine.
 *
 * Copyright 2011-2014 Leandro Nini <drfiemost@users.sourceforge.net>
 * Copyright 2007-2010 Antti Lankila
 * Copyright 2009-2014 VICE Project
 * Copyright 2000 Simon White
 *
 * 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., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 */

#include "mos6526.h"

#include <cstring>

#include "sidendian.h"

enum
{
    INTERRUPT_NONE         = 0,
    INTERRUPT_UNDERFLOW_A  = 1 << 0,
    INTERRUPT_UNDERFLOW_B  = 1 << 1,
    INTERRUPT_ALARM        = 1 << 2,
    INTERRUPT_SP           = 1 << 3,
    INTERRUPT_FLAG         = 1 << 4,
    INTERRUPT_REQUEST      = 1 << 7
};

enum
{
    PRA     = 0,
    PRB     = 1,
    DDRA    = 2,
    DDRB    = 3,
    TAL     = 4,
    TAH     = 5,
    TBL     = 6,
    TBH     = 7,
    TOD_TEN = 8,
    TOD_SEC = 9,
    TOD_MIN = 10,
    TOD_HR  = 11,
    SDR     = 12,
    ICR     = 13,
    IDR     = 13,
    CRA     = 14,
    CRB     = 15
};

void TimerA::underFlow()
{
    parent->underflowA();
}

void TimerA::serialPort()
{
    parent->serialPort();
}

void TimerB::underFlow()
{
    parent->underflowB();
}

const char *MOS6526::credit =
{
    "MOS6526 (CIA) Emulation:\n"
    "\tCopyright (C) 2001-2004 Simon White\n"
    "\tCopyright (C) 2009-2014 VICE Project\n"
    "\tCopyright (C) 2009-2010 Antti S. Lankila\n"
    "\tCopyright (C) 2011-2014 Leandro Nini\n"
};

MOS6526::MOS6526(EventContext *context) :
    pra(regs[PRA]),
    prb(regs[PRB]),
    ddra(regs[DDRA]),
    ddrb(regs[DDRB]),
    timerA(context, this),
    timerB(context, this),
    idr(0),
    event_context(*context),
    tod(context, this, regs),
    bTickEvent("CIA B counts A", *this, &MOS6526::bTick),
    triggerEvent("Trigger Interrupt", *this, &MOS6526::trigger)
{
    reset();
}

void MOS6526::serialPort()
{
    if (regs[CRA] & 0x40)
    {
        if (sdr_count)
        {
            if (--sdr_count == 0)
            {
                trigger(INTERRUPT_SP);
            }
        }
        if (sdr_count == 0 && sdr_buffered)
        {
            sdr_out = regs[SDR];
            sdr_buffered = false;
            sdr_count = 16;
            // Output rate 8 bits at ta / 2
        }
    }
}

void MOS6526::clear()
{
    if (idr & INTERRUPT_REQUEST)
        interrupt(false);
    idr = 0;
}


void MOS6526::reset()
{
    sdr_out = 0;
    sdr_count = 0;
    sdr_buffered = false;
    // Clear off any IRQs
    clear();
    icr = idr = 0;
    memset(regs, 0, sizeof(regs));

    // Reset timers
    timerA.reset();
    timerB.reset();

    // Reset tod
    tod.reset();

    triggerScheduled = false;

    event_context.cancel(bTickEvent);
    event_context.cancel(triggerEvent);
}

uint8_t MOS6526::read(uint_least8_t addr)
{
    addr &= 0x0f;

    timerA.syncWithCpu();
    timerA.wakeUpAfterSyncWithCpu();
    timerB.syncWithCpu();
    timerB.wakeUpAfterSyncWithCpu();

    switch (addr)
    {
    case PRA: // Simulate a serial port
        return (regs[PRA] | ~regs[DDRA]);
    case PRB:{
        uint8_t data = regs[PRB] | ~regs[DDRB];
        // Timers can appear on the port
        if (regs[CRA] & 0x02)
        {
            data &= 0xbf;
            if (timerA.getPb(regs[CRA]))
                data |= 0x40;
        }
        if (regs[CRB] & 0x02)
        {
            data &= 0x7f;
            if (timerB.getPb(regs[CRB]))
                data |= 0x80;
        }
        return data;}
    case TAL:
        return endian_16lo8(timerA.getTimer());
    case TAH:
        return endian_16hi8(timerA.getTimer());
    case TBL:
        return endian_16lo8(timerB.getTimer());
    case TBH:
        return endian_16hi8(timerB.getTimer());
    case TOD_TEN:
    case TOD_SEC:
    case TOD_MIN:
    case TOD_HR:
        return tod.read(addr - TOD_TEN);
    case IDR:{
        if (triggerScheduled)
        {
            event_context.cancel(triggerEvent);
            triggerScheduled = false;
        }
        // Clear IRQs, and return interrupt
        // data register
        const uint8_t ret = idr;
        clear();
        return ret;}
    case CRA:
        return (regs[CRA] & 0xee) | (timerA.getState() & 1);
    case CRB:
        return (regs[CRB] & 0xee) | (timerB.getState() & 1);
    default:
        return regs[addr];
    }
}

void MOS6526::write(uint_least8_t addr, uint8_t data)
{
    addr &= 0x0f;

    timerA.syncWithCpu();
    timerB.syncWithCpu();

    const uint8_t oldData = regs[addr];
    regs[addr] = data;

    switch (addr)
    {
    case PRA:
    case DDRA:
        portA();
        break;
    case PRB:
    case DDRB:
        portB();
        break;
    case TAL:
        timerA.latchLo(data);
        break;
    case TAH:
        timerA.latchHi(data);
        break;
    case TBL:
        timerB.latchLo(data);
        break;
    case TBH:
        timerB.latchHi(data);
        break;
    case TOD_TEN:
    case TOD_SEC:
    case TOD_MIN:
    case TOD_HR:
        tod.write(addr - TOD_TEN, data);
        break;
    case SDR:
        if (regs[CRA] & 0x40)
            sdr_buffered = true;
        break;
    case ICR:
        if (data & 0x80)
        {
            icr |= data & ~INTERRUPT_REQUEST;
            trigger(INTERRUPT_NONE);
        }
        else
        {
            icr &= ~data;
        }
        break;
    case CRA:{
        if ((data & 1) && !(oldData & 1))
        {
            // Reset the underflow flipflop for the data port
            timerA.setPbToggle(true);
        }
        timerA.setControlRegister(data);
        break;}
    case CRB:{
        if ((data & 1) && !(oldData & 1))
        {
            // Reset the underflow flipflop for the data port
            timerB.setPbToggle(true);
        }
        timerB.setControlRegister(data | (data & 0x40) >> 1);
        break;}
    }

    timerA.wakeUpAfterSyncWithCpu();
    timerB.wakeUpAfterSyncWithCpu();
}

void MOS6526::trigger()
{
    idr |= INTERRUPT_REQUEST;
    interrupt(true);
    triggerScheduled = false;
}

void MOS6526::trigger(uint8_t interruptMask)
{
    idr |= interruptMask;
    if ((icr & idr) && !(idr & INTERRUPT_REQUEST))
    {
        if (!triggerScheduled)
        {
            // Schedules an IRQ asserting state transition for next cycle.
            event_context.schedule(triggerEvent, 1, EVENT_CLOCK_PHI1);
            triggerScheduled = true;
        }
    }
}

void MOS6526::bTick()
{
    timerB.cascade();
}

void MOS6526::underflowA()
{
    trigger(INTERRUPT_UNDERFLOW_A);
    if ((regs[CRB] & 0x41) == 0x41)
    {
        if (timerB.started())
        {
            event_context.schedule(bTickEvent, 0, EVENT_CLOCK_PHI2);
        }
    }
}

void MOS6526::underflowB()
{
    trigger(INTERRUPT_UNDERFLOW_B);
}

void MOS6526::todInterrupt()
{
    trigger(INTERRUPT_ALARM);
}
