/*
$Id: I8254.java,v 1.13 2012-07-08 10:40:44 user Exp $
 */

package mce.arch;

import mce.cpu.CPU;
import mce.cpu.ResPlugin;

import java.util.ArrayList;
import java.util.List;

/**
 * Intel 82c54 chmos programmable interval timer.
 * This module holds implementation of the Intel's industry standard 8254 counter/timer.
 */
public class I8254 implements ResPlugin {

    /**
     * Class implementing one counter
     */
    public class Counter implements ResPlugin {

        // counter number
        private int counterNumber;

        // current work mode
        private int workMode;

        // current read/write mode
        private int rwMode;

        // whether the BCD counting is active
        private boolean bcd;

        // counter running flag
        private boolean countRunning;

        // count hi/lo flipFlop
        private boolean countHiLoRead;
        private boolean countHiLoWrite;

        // current count wave phase (for work mode 3)
        private boolean countWavePhase;

        // current count value
        private int countCurrent;

        // initial count value
        private int countInitial;

        // new initial value has been loaded
        private boolean countInitialNew;

        // new initial value has been loaded and is complete
        private boolean countInitialNewComplete;

        // set to true if the count is not available (aka count is null)
        private boolean countNull = true;

        // latched count value
        private int countLatch;

        // whether the counter latch is available or not
        private boolean countLatchAvailable;

        // status value
        private int statusLatch;

        // whether the status latch is available or not
        private boolean statusLatchAvailable;

        // Output signal
        private boolean signalOutput;

        // Gate signal
        private boolean signalGate;

        /**
         * Public contructor
         *
         * @param counter serial number of this counter
         */
        protected Counter(int counter) {

            this.counterNumber = counter;
        }

        /**
         * Init counter values
         *
         * @param countInitial initial count
         * @param countCurrent current count
         * @param countLatch   currently latched count
         * @param workMode     count mode
         * @param isHiWrite    true if high byte is being currently written
         * @param isHiRead     true if high byte is being currently read
         * @param isLatched    true if latched byte available
         * @param rwMode       read/write mode of the counter
         */
        public void init(int countInitial, int countCurrent, int countLatch, int workMode,
                         boolean isHiWrite, boolean isHiRead, boolean isLatched, int rwMode) {

            this.countInitial = countInitial;
            this.countCurrent = countCurrent;
            this.countLatch = countLatch;
            this.workMode = workMode;
            this.countHiLoWrite = isHiWrite;
            this.countHiLoRead = isHiRead;
            this.countLatchAvailable = isLatched;
            this.rwMode = rwMode;
            setControl(this.workMode, rwMode, bcd);
        }

        /**
         * ResPlugin interface: do reset routine external to CPU unit (called when CPU does a reset)
         *
         * @param ticks number of ticks since last method call
         * @return number of ticks to add to the ticks count
         */
        public int doCPUReset(CPU cpu, int ticks) {

            workMode = rwMode = countCurrent = countInitial = countLatch = statusLatch = 0;
            bcd = countRunning = countHiLoRead = countHiLoWrite = false;
            countWavePhase = countInitialNew = countInitialNewComplete = false;
            countLatchAvailable = statusLatchAvailable = signalOutput = signalGate = false;
            countNull = true;
            return 0;
        }

        /**
         * Set the signal Output
         *
         * @param signal signal level (true/false)
         */
        public void setSignalOutput(boolean signal) {

            signalOutput = signal;
        }

        /**
         * Set the signal Gate
         *
         * @param signal signal level (true/false)
         */
        public void setSignalGate(boolean signal) {

            if (signal != signalGate) {
                signalGate = signal;

                switch (workMode) {

                    case 0:          // interrupt on terminal count
                        countRunning = signalGate;
                        break;

                    case 1:          // hardware retriggerable one-shot
                        if (signalGate && countInitialNewComplete) {
                            countCurrent = countInitial;
                            countRunning = true;
                        }
                        break;

                    case 2:        // Rate generator
                    case 3:        // Square wave mode
                        if (signalGate)
                            countInitialNew = true;
                        else
                            setSignalOutput(true);
                        countRunning = signalGate;
                        break;

                    case 4:        // Software triggered strobe
                        countRunning = signalGate;
                        break;

                    case 5:        // Hardware triggered strobe (retriggerable)
                        if (signalGate) {
                            countInitialNew = true;
                            countRunning = true;
                        }
                        break;
                }
            }
        }

        /**
         * Set control word
         *
         * @param workMode Work mode to set the counter to
         * @param rwMode   read/write mode to set the counter to
         * @param bcd      set to true when the counter is set to binary coded decimal (BCD) counting
         */
        public void setControl(int workMode, int rwMode, boolean bcd) {

            this.workMode = workMode & 7;
            this.rwMode = rwMode & 3;
            this.bcd = bcd;
            countNull = true;
            countInitialNew = true;
            countInitialNewComplete = false;

            // if the rwMode is 3, we've to reset HiLo flag
            countHiLoWrite = rwMode == 2;

            // do a counter initialization according to the work mode
            switch (workMode) {
                case 0:        // Interrupt on terminal count
                    setSignalOutput(false);
                    countRunning = false;
                    countNull = true;
                    break;
                case 1:        // Hardware retriggerable one-shot
                case 2:        // Rate generator
                case 3:        // Square wave mode
                case 4:        // Software triggered strobe
                case 5:        // Hardware triggered strobe (retriggerable)
                    setSignalOutput(true);
                    countRunning = false;
                    countWavePhase = true;
                    break;
            }
        }

        /**
         * Latch counter's count
         */
        public void latchCount() {

            // do a counter latch command only if no counter latch is available yet
            if (!countLatchAvailable) {
                countLatch = countCurrent;
                countLatchAvailable = true;
            }
        }

        /**
         * Latch counter's status
         */
        public void latchStatus() {

            // do a status latch command only if no status latch is available yet
            if (!statusLatchAvailable) {
                statusLatch =
                        (signalOutput ? 0x80 : 0) | (countNull ? 0x40 : 0) |
                                (rwMode << 4) | (workMode << 1) | (bcd ? 1 : 0);
                statusLatchAvailable = true;
            }
        }

        /**
         * Set counter data
         *
         * @param data int containing the data for the counter (initial count lo/hi)
         */
        public void setData(int data) {

            // normalize bcd number
            if (bcd) {
                if ((data & 0xf) > 9)
                    data = (data & 0xf0) | 9;
                if ((data & 0xf0) > 0x90)
                    data = (data & 0xf) | 0x90;
            }

            if (countHiLoWrite) {
                countInitial = (countInitial & 0xff) | ((data & 0xff) << 8);
                countInitialNewComplete = true;
                countHiLoWrite = rwMode != 3;

            } else {
                countInitial = (countInitial & 0xff00) | (data & 0xff);
                countInitialNewComplete = rwMode != 3;
                countHiLoWrite = rwMode == 3;
            }

            // change counter (work mode dependent)
            switch (workMode) {

                case 0:        // Interrupt on terminal count
                    if (countInitialNewComplete)
                        countInitialNew = true;
                    else
                        countRunning = false;
                    setSignalOutput(false);
                    break;

                case 1:        // Hardware retriggerable one-shot
                    break;

                case 2:        // Rate generator
                case 3:        // Square wave mode
                    if (!countRunning && countInitialNewComplete) {
                        countInitialNew = true;
                        countRunning = true;
                    }
                    break;

                case 4:        // Software triggered strobe
                    if (countInitialNewComplete) {
                        countInitialNew = true;
                        countRunning = true;
                    }
                    break;
            }
        }

        /**
         * Get data from counter
         *
         * @return int containing the data from the counter (count, latched status or latched count)
         */
        public int getData() {

            // return latched status (if available)
            if (statusLatchAvailable) {
                statusLatchAvailable = false;
                return statusLatch;
            }

            // we are prepared to return current count
            int retCount = countCurrent;

            // rather return latched count (if available)
            if (countLatchAvailable) {
                retCount = countLatch;
                if (rwMode != 3 || countHiLoRead)
                    countLatchAvailable = false;
            }

            // prepare the count for returning and change the HiLo switch accordingly
            retCount = (countHiLoRead ? (retCount >> 8) : retCount) & 0xff;
            if (rwMode == 3)
                countHiLoRead = !countHiLoRead;

            return retCount;
        }

        /**
         * Do counter tick given number of times
         *
         * @param ticks number of ticks to process
         */
        public void tick(int ticks) {

            // do given number of ticks
            if (countInitialNew || countRunning) {
                while (ticks-- > 0) {

                    // do a counter work according to the work mode
                    switch (workMode) {

                        // ------------------ Interrupt on terminal count --------------------
                        case 0:
                            // load new value into the counter
                            if (countInitialNew) {
                                countCurrent = countInitial;
                                countInitialNew = false;
                                countRunning = true;
                                countNull = false;

                                // process old value
                            } else if (countRunning && signalGate) {
                                countCurrent = decrementCounter(countCurrent, bcd);
                                if ((countCurrent == 0xffff && !bcd) || (countCurrent == 0x9999 && bcd)) {
                                    setSignalOutput(true);
                                    processListeners(counterNumber);
                                }
                            }
                            break;

                        // ------------------ Hardware retriggerable one-shot ------------------
                        case 1:
                            // process counter value
                            if (countRunning) {
                                setSignalOutput(false);
                                countCurrent = decrementCounter(countCurrent, bcd);
                                if ((countCurrent == 0xffff && !bcd) || (countCurrent == 0x9999 && bcd)) {
                                    countRunning = false;
                                    setSignalOutput(true);
                                    processListeners(counterNumber);
                                }
                            }
                            break;

                        // -------------------------- Rate generator ---------------------------
                        case 2:
                            // load new value into the counter
                            if (countInitialNew) {
                                countCurrent = countInitial;
                                countInitialNew = false;
                                setSignalOutput(true);
                                countNull = false;

                                // process counter value
                            } else if (countRunning) {
                                countCurrent = decrementCounter(countCurrent, bcd);
                                if (countCurrent == 1) {
                                    setSignalOutput(false);
                                    countInitialNew = true;
                                    processListeners(counterNumber);
                                }
                            }
                            break;

                        // -------------------------- Square wave mode -------------------------
                        case 3:
                            // load new value into the counter
                            if (countInitialNew) {
                                countCurrent = countInitial;
                                countInitialNew = false;
                                setSignalOutput(countWavePhase);
                                countNull = false;
                                if (!countWavePhase)
                                    countCurrent = decrementCounter(countCurrent, bcd);
                                countWavePhase = !countWavePhase;

                                // process counter value
                            } else if (countRunning) {
                                countCurrent = decrementCounter(countCurrent, bcd);
                                if ((countCurrent == 0xffff && !bcd) || (countCurrent == 0x9999 && bcd)) {
                                    countInitialNew = true;
                                    processListeners(counterNumber);
                                } else {
                                    countCurrent = decrementCounter(countCurrent, bcd);
                                    if ((countCurrent == 0xffff && !bcd) || (countCurrent == 0x9999 && bcd)) {
                                        countInitialNew = true;
                                        processListeners(counterNumber);
                                    }
                                }
                            }
                            break;

                        // ---------------------- Software triggered strobe -----------------------
                        case 4:

                            // ---------------------- Hardware triggered strobe -----------------------
                        case 5:
                            // load new value into the counter
                            if (countInitialNew) {
                                countCurrent = countInitial;
                                countInitialNew = false;
                                setSignalOutput(true);
                                countNull = false;

                                // process counter value
                            } else if (countRunning) {
                                countCurrent = decrementCounter(countCurrent, bcd);
                                if ((countCurrent == 0xffff && !bcd) || (countCurrent == 0x9999 && bcd)) {
                                    setSignalOutput(false);
                                    countInitialNew = true;
                                    processListeners(counterNumber);
                                }
                            }
                            break;
                    }
                }
            }
        }

        /**
         * Decrement counter by one
         *
         * @param num number to decrement
         */
        private int decrementCounter(int num, boolean bcd) {

            // handle special case decrement
            if (num == 0)
                return bcd ? 0x9999 : 0xffff;

            // non-bcd numbers are easy
            if (!bcd)
                return --num;

            // decrement the number
            num--;

            // normalize the bcd number
            if ((num & 0xf) == 0xf) {
                num &= 0xfff9;
                num -= 0x10;
                if ((num & 0xf0) == 0xf0) {
                    num &= 0xff9f;
                    num -= 0x100;
                    if ((num & 0xf00) == 0xf00) {
                        num &= 0xf9ff;
                        num -= 0x1000;
                    }
                }
            }

            return num;
        }

        /**
         * Returns text representation of the timer
         *
         * @return text representation
         */
        public String toString() {

            return "workMode " + workMode + ", rwMode " + rwMode + ", bcd " + bcd + ", running " + countRunning +
                    ", hiLoRead " + countHiLoRead + ", hiLoWrite " + countHiLoWrite + ", wavePhase " + countWavePhase +
                    ", current " + countCurrent + ", initial " + countInitial + ", initialNew " + countInitialNew +
                    ", initialNewComplete " + countInitialNewComplete + ", countNull " + countNull + ", latch " + countLatch +
                    ", latchAvail " + countLatchAvailable + ", statusLatch" + statusLatch + ", statusLatchAvail " + statusLatchAvailable +
                    ", signalOut " + signalOutput + ", signalGate " + signalGate;
        }
    }

    // ------------------------ Global I8254 handling routines ---------------------------

    // available counters
    protected Counter[] cnt;

    // counter signal listeners
    private List<I8254Listener> listeners = new ArrayList<I8254Listener>();

    /**
     * Public contructor: initialize counters
     */
    public I8254() {

        // create new counters
        cnt = new Counter[3];
        cnt[0] = new Counter(0);
        cnt[1] = new Counter(1);
        cnt[2] = new Counter(2);
    }

    /**
     * ResPlugin interface: do reset routine external to CPU unit (called when CPU does a reset)
     *
     * @param ticks number of ticks since last method call
     * @return number of ticks to add to the ticks count
     */
    public int doCPUReset(CPU cpu, int ticks) {

        cnt[0].doCPUReset(cpu, ticks);
        cnt[1].doCPUReset(cpu, ticks);
        cnt[2].doCPUReset(cpu, ticks);
        return 0;
    }

    /**
     * Returns I8254 counter
     *
     * @param counter counter number
     * @return counter object
     */
    public Counter getCounter(int counter) {

        return counter >= 0 && counter < 3 ? cnt[counter] : null;
    }

    /**
     * Get byte from I8254 port
     *
     * @param addr int containing the port number (ports 0 - 3 are relevant)
     * @return int containing the unsigned byte
     */
    public int getPort(int addr) {

        addr &= 3;
        return addr < 3 ? cnt[addr].getData() : 0xff;
    }

    /**
     * Set byte to I8254 port
     *
     * @param addr  int containing the port number  (ports 0 - 3 are relevant)
     * @param value int containing the unsigned byte
     */
    public void setPort(int addr, int value) {

        addr &= 3;

        // set counter data
        if (addr < 3) {
            cnt[addr].setData(value);

            // process the command
        } else {
            int sc = (value >> 6) & 3;

            // process standard command
            if (sc != 3) {
                int rw = (value >> 4) & 3;

                // process mode set command
                if (rw != 0) {
                    int mode = (value >> 1) & 7;
                    boolean bcd = (value & 1) == 1;
                    cnt[sc].setControl(mode, rw, bcd);

                    // process counter latch command
                } else
                    cnt[sc].latchCount();

                // process read-back command
            } else {
                if ((value & 2) == 2) {
                    if ((value & 0x10) == 0)
                        cnt[0].latchStatus();
                    if ((value & 0x20) == 0)
                        cnt[0].latchCount();
                }
                if ((value & 4) == 4) {
                    if ((value & 0x10) == 0)
                        cnt[1].latchStatus();
                    if ((value & 0x20) == 0)
                        cnt[1].latchCount();
                }
                if ((value & 8) == 8) {
                    if ((value & 0x10) == 0)
                        cnt[2].latchStatus();
                    if ((value & 0x20) == 0)
                        cnt[2].latchCount();
                }
            }
        }
    }

    /**
     * Do one counter ticks
     *
     * @param counter counter number to process (0-2)
     * @param ticks   number of ticks to process the timer with
     */
    public void processTimer(int counter, int ticks) {

        if (counter >= 0 && counter < 3)
            cnt[counter].tick(ticks);
    }

    /**
     * Sets the signal Gate on the given counter
     *
     * @param counter counter number on which to set the gate
     * @param signal  signal level (true/false)
     */
    public void setSignalGate(int counter, boolean signal) {

        if (counter >= 0 && counter < 3)
            cnt[counter].setSignalGate(signal);
    }

    /**
     * Adds listener to the timer
     *
     * @param listener listener to add
     */
    public synchronized void addListener(I8254Listener listener) {

        if (!listeners.contains(listener))
            listeners.add(listener);
    }

    /**
     * Removes listener to the timer
     *
     * @param listener listener to remove
     */
    public synchronized void removeListener(I8254Listener listener) {

        listeners.remove(listener);
    }

    /**
     * Processes counter signal using configured listeners
     *
     * @param counter counter number to process the signal from
     */
    private void processListeners(int counter) {

        for (I8254Listener listener : listeners)
            listener.processCounterInterrupt(counter);
    }

    /**
     * Returns text representation of the timer
     *
     * @return text representation
     */
    public String toString() {

        return "cnt0: " + cnt[0] + ", cnt1: " + cnt[1] + ", cnt2: " + cnt[2];
    }
}
