/*
 * Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
 * MA 02110-1301  USA
 */
package jace.hardware;

import jace.config.Name;
import jace.core.Card;
import jace.core.Computer;
import jace.core.RAMEvent;
import jace.core.RAMEvent.TYPE;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.sound.midi.InvalidMidiDataException;
import javax.sound.midi.MidiDevice;
import javax.sound.midi.MidiSystem;
import javax.sound.midi.MidiUnavailableException;
import javax.sound.midi.ShortMessage;
import javax.sound.midi.Synthesizer;

/**
 * Partial implementation of Passport midi card, supporting midi output routed
 * to the java midi synth for playback. Compatible with Ultima V. Card
 * operational notes taken from the Passport MIDI interface manual
 * ftp://ftp.apple.asimov.net/pub/apple_II/documentation/hardware/misc/passport_midi.pdf
 *
 * @author Brendan Robert (BLuRry) brendan.robert@gmail.com 
 */
@Name(value = "Passport Midi Interface", description = "MIDI sound card")
public class PassportMidiInterface extends Card {

    public PassportMidiInterface(Computer computer) {
        super(computer);
    }

    @Override
    protected void handleC8FirmwareAccess(int register, TYPE type, int value, RAMEvent e) {
        // There is no rom on this card, so nothing to do here
    }

    // MIDI timing: 31250 BPS, 8-N-1  (roughly 3472k per second)
    public static enum TIMER_MODE {

        CONTINUOUS, SINGLE_SHOT, FREQ_COMPARISON, PULSE_COMPARISON
    };

    public static class PTMTimer {
        // Configuration values

        public boolean prescaledTimer = false; // Only available on Timer 3
        public boolean enableClock = false; // False == use CX clock input
        public boolean dual8BitMode = false;
        public TIMER_MODE mode = TIMER_MODE.CONTINUOUS;
        public boolean irqEnabled = false;
        public boolean counterOutputEnable = false;
        // Set by data latches
        public Long duration = 0L;
        // Run values
        public boolean irqRequested = true;
        public Long value = 0L;
    }
    // I/O registers
    // --- 6840 PTM
    public static final int TIMER_CONTROL_1 = 0;
    public static final int TIMER_CONTROL_2 = 1;
    public static final int TIMER1_MSB = 2;
    public static final int TIMER1_LSB = 3;
    public static final int TIMER2_MSB = 4;
    public static final int TIMER2_LSB = 5;
    //     (Most likely not used)
    public static final int TIMER3_MSB = 6;
    public static final int TIMER3_LSB = 7;
    // --- 6850 ACIA registers (write)
    public static final int ACIA_CONTROL = 8;
    public static final int ACIA_SEND = 9;
    // --- 6850 ACIA registers (read)
    public static final int ACIA_STATUS = 8;
    public static final int ACIA_RECV = 9;
    // --- Drums
    public static final int DRUM_SYNC_SET = 0x0e;
    public static final int DRUM_SYNC_CLEAR = 0x0f;
    //---------------------------------------------------------
    // PTM control values (register 1,2 and 3)
    public static final int PTM_START_TIMERS = 0;
    public static final int PTM_STOP_TIMERS = 1;
    public static final int PTM_RESET = 67;
    // PTM select values (register 2 only) -- modifies what Reg 1 points to
    public static final int PTM_SELECT_REG_1 = 1;
    public static final int PTM_SELECT_REG_3 = 0;
    // PTM select values (register 3 only)
    public static final int TIMER3_PRESCALED = 1;
    public static final int TIMER3_NOT_PRESCALED = 0;
    // PTM bit values
    public static final int PTM_CLOCK_SOURCE = 2;       // Bit 1
    // 0 = external, 2 = internal clock
    public static final int PTM_LATCH_IS_16BIT = 4;    // Bit 2
    // 0 = 16-bit, 4 = dual 8-bit
    // Bits 3-5
    // 5 4 3
    public static final int PTM_CONTINUOUS = 0;         // 0 x 0
    public static final int PTM_SINGLE_SHOT = 32;       // 1 x 0
    public static final int PTM_FREQ_COMP = 8;          // x 0 1
    public static final int PTM_PULSE_COMP = 24;        // x 1 1
    public static final int PTM_IRQ_ENABLED = 64;       // Bit 6
    // 64 = IRQ Enabled, 0 = IRQ Masked
    public static final int PTM_OUTPUT_ENABLED = 128;    // Bit 7
    // 128 = Timer output enabled, 0 = disabled
    // ACIA control values
    // Reset == Master reset + even parity + 2 stop bits + 8 bit + No interrupts (??)
    public static final int ACIA_RESET = 19;
    public static final int ACIA_MASK_INTERRUPTS = 17;
    public static final int ACIA_OFF = 21;
    // Counter * 1 + RTS = low, transmit interrupt enabled
    public static final int ACIA_INT_ON_SEND = 49;
    // Counter * 1 + RTS = high, transmit interrupt disabled + Interrupt on receive
    public static final int ACIA_INT_ON_RECV = 145;
    // Counter * 1 + RTS = low, transmit interrupt enabled + Interrupt on receive
    public static final int ACIA_INT_ON_SEND_AND_RECV = 177;
    // ACIA control register values
    // --- Bits 1 and 0 control counter divide select
    public static final int ACIA_COUNTER_1 = 0;
    public static final int ACIA_COUNTER_16 = 1;
    public static final int ACIA_COUNTER_64 = 2;
    public static final int ACIA_MASTER_RESET = 3;
    // Midi is always transmitted 8-N-1
    public static final int ACIA_ODD_PARITY = 4;    // 4 = odd, 0 = even
    public static final int ACIA_STOP_BITS_1 = 8;    // 8 = 1 stop bit, 0 = 2 stop bits
    public static final int ACIA_WORD_LENGTH_8 = 16;    // 16 = 8-bit, 0 = 7-bit
    // --- Bits 5 and 6 control interrupts
    // 6 5
    // 0 0  RTS = low, transmit interrupt disabled
    // 0 1  RTS = low, transmit interrupt enabled
    // 1 0  RTS = high, transmit interrupt disabled
    // 1 1  RTS = low, Transmit break, trasmit interrupt disabled
    public static final int ACIA_RECV_INTERRUPT = 128;  // 128 = interrupt on receive, 0 = no interrupt
    // PTM configuration
    private boolean ptmTimer3Selected = false; // When true, reg 1 points at timer 3
    private boolean ptmTimersActive = false; // When true, timers run constantly
    private final PTMTimer[] ptmTimer = {
        new PTMTimer(),
        new PTMTimer(),
        new PTMTimer()
    };
    private boolean ptmStatusReadSinceIRQ = false;
    // ---------------------- ACIA CONFIGURATION
    private final boolean aciaInterruptOnSend = false;
    private final boolean aciaInterruptOnReceive = false;
    // ---------------------- ACIA STATUS BITS
    // True when MIDI IN receives a byte
    private final boolean receivedACIAByte = false;
    // True when data is not transmitting (always true because we aren't really doing wire transmission);
    private final boolean transmitACIAEmpty = true;
    // True if another byte is received before the previous byte was processed
    private final boolean receiverACIAOverrun = false;
    // True if ACIA generated interrupt request
    private final boolean irqRequestedACIA = false;
    //--- the synth
    private Synthesizer synth;

    @Override
    public void reset() {
        // TODO: Deactivate card
        suspend();
    }

    @Override
    public boolean suspend() {
        // TODO: Deactivate card
        suspendACIA();
        return super.suspend();
    }

    @Override
    protected void handleFirmwareAccess(int register, TYPE type, int value, RAMEvent e) {
        // No firmware, so do nothing
    }

    @Override
    protected void handleIOAccess(int register, TYPE type, int value, RAMEvent e) {
        switch (type) {
            case READ_DATA:
                int returnValue = 0;
                switch (register) {
                    case ACIA_STATUS:
                        returnValue = getACIAStatus();
                        break;
                    case ACIA_RECV:
                        returnValue = getACIARecieve();
                        break;
                    //TODO: Implement PTM registers
                    case TIMER_CONTROL_1:
                        // Technically it's not supposed to return anything...
                        returnValue = getPTMStatus();
                        break;
                    case TIMER_CONTROL_2:
                        returnValue = getPTMStatus();
                        break;
                    case TIMER1_LSB:
                        returnValue = (int) (ptmTimer[0].value & 0x0ff);
                        if (ptmStatusReadSinceIRQ) {
                            ptmTimer[0].irqRequested = false;
                        }
                        break;
                    case TIMER1_MSB:
                        returnValue = (int) (ptmTimer[0].value >> 8) & 0x0ff;
                        if (ptmStatusReadSinceIRQ) {
                            ptmTimer[0].irqRequested = false;
                        }
                        break;
                    case TIMER2_LSB:
                        returnValue = (int) (ptmTimer[1].value & 0x0ff);
                        if (ptmStatusReadSinceIRQ) {
                            ptmTimer[1].irqRequested = false;
                        }
                        break;
                    case TIMER2_MSB:
                        returnValue = (int) (ptmTimer[1].value >> 8) & 0x0ff;
                        if (ptmStatusReadSinceIRQ) {
                            ptmTimer[1].irqRequested = false;
                        }
                        break;
                    case TIMER3_LSB:
                        returnValue = (int) (ptmTimer[2].value & 0x0ff);
                        if (ptmStatusReadSinceIRQ) {
                            ptmTimer[2].irqRequested = false;
                        }
                        break;
                    case TIMER3_MSB:
                        returnValue = (int) (ptmTimer[2].value >> 8) & 0x0ff;
                        if (ptmStatusReadSinceIRQ) {
                            ptmTimer[2].irqRequested = false;
                        }
                        break;
                    default:
                        System.out.println("Passport midi read unrecognized, port " + register);
                }

                e.setNewValue(returnValue);
//                System.out.println("Passport I/O read register " + register + " == " + returnValue);
                break;
            case WRITE:
                int v = e.getNewValue() & 0x0ff;
//                System.out.println("Passport I/O write register " + register + " == " + v);
                switch (register) {
                    case ACIA_CONTROL:
                        processACIAControl(v);
                        break;
                    case ACIA_SEND:
                        processACIASend(v);
                        break;
                    case TIMER_CONTROL_1:
                        if (ptmTimer3Selected) {
//                            System.out.println("Configuring timer 3");
                            ptmTimer[2].prescaledTimer = ((v & TIMER3_PRESCALED) != 0);
                            processPTMConfiguration(ptmTimer[2], v);
                        } else {
//                            System.out.println("Configuring timer 1");
                            if ((v & PTM_STOP_TIMERS) == 0) {
                                startPTM();
                            } else {
                                stopPTM();
                            }
                            processPTMConfiguration(ptmTimer[0], v);
                        }
                        break;
                    case TIMER_CONTROL_2:
//                        System.out.println("Configuring timer 2");
                        ptmTimer3Selected = ((v & PTM_SELECT_REG_1) == 0);
                        processPTMConfiguration(ptmTimer[1], v);
                        break;
                    case TIMER1_LSB:
                        ptmTimer[0].duration = (ptmTimer[0].duration & 0x0ff00) | v;
                        break;
                    case TIMER1_MSB:
                        ptmTimer[0].duration = (ptmTimer[0].duration & 0x0ff) | (v << 8);
                        break;
                    case TIMER2_LSB:
                        ptmTimer[1].duration = (ptmTimer[1].duration & 0x0ff00) | v;
                        break;
                    case TIMER2_MSB:
                        ptmTimer[1].duration = (ptmTimer[1].duration & 0x0ff) | (v << 8);
                        break;
                    case TIMER3_LSB:
                        ptmTimer[2].duration = (ptmTimer[2].duration & 0x0ff00) | v;
                        break;
                    case TIMER3_MSB:
                        ptmTimer[2].duration = (ptmTimer[2].duration & 0x0ff00) | (v << 8);
                        break;
                    default:
                        System.out.println("Passport midi write unrecognized, port " + register);
                }

                break;
        }
    }

    @Override
    public void tick() {
        if (ptmTimersActive) {
            for (PTMTimer t : ptmTimer) {
//                if (t.duration == 0) {
//                    continue;
//                }
                t.value--;
                if (t.value < 0) {
                    // TODO: interrupt dual 8-bit mode, whatver that is!
                    if (t.irqEnabled) {
//                        System.out.println("Timer generating interrupt!");
                        t.irqRequested = true;
                        computer.getCpu().generateInterrupt();
                        ptmStatusReadSinceIRQ = false;
                    }
                    if (t.mode == TIMER_MODE.CONTINUOUS || t.mode == TIMER_MODE.FREQ_COMPARISON) {
                        t.value = t.duration;
                    }
                }
            }
        }
    }

    @Override
    public String getDeviceName() {
        return "Passport MIDI Controller";
    }

    //------------------------------------------------------ PTM
    private void processPTMConfiguration(PTMTimer timer, int val) {
        timer.enableClock = (val & PTM_CLOCK_SOURCE) != 0;
        timer.dual8BitMode = (val & PTM_LATCH_IS_16BIT) != 0;
        switch (val & 56) {
            // Evaluate bits 3, 4 and 5 to determine mode
            case PTM_CONTINUOUS:
                timer.mode = TIMER_MODE.CONTINUOUS;
                break;
            case PTM_PULSE_COMP:
                timer.mode = TIMER_MODE.PULSE_COMPARISON;
                break;
            case PTM_FREQ_COMP:
                timer.mode = TIMER_MODE.FREQ_COMPARISON;
                break;
            case PTM_SINGLE_SHOT:
                timer.mode = TIMER_MODE.SINGLE_SHOT;
                break;
            default:
                timer.mode = TIMER_MODE.CONTINUOUS;
                break;
        }
        timer.irqEnabled = (val & PTM_IRQ_ENABLED) != 0;
        timer.counterOutputEnable = (val & PTM_OUTPUT_ENABLED) != 0;
    }

    private void stopPTM() {
//        System.out.println("Passport timers halted");
        ptmTimersActive = false;
    }

    private void startPTM() {
//        System.out.println("Passport timers started");
        ptmTimersActive = true;
        ptmTimer[0].irqRequested = false;
        ptmTimer[1].irqRequested = false;
        ptmTimer[2].irqRequested = false;
        ptmTimer[0].value = ptmTimer[0].duration;
        ptmTimer[1].value = ptmTimer[1].duration;
        ptmTimer[2].value = ptmTimer[2].duration;

    }

    // Bits 0, 1 and 2 == IRQ requested from timer 1, 2 or 3
    // Bit 7 = Any IRQ
    private int getPTMStatus() {
        int status = 0;
        for (int i = 0; i < 3; i++) {
            PTMTimer t = ptmTimer[i];
            if (t.irqRequested && t.irqEnabled) {
                ptmStatusReadSinceIRQ = true;
                status |= (1 << i);
                status |= 128;
            }
        }
        return status;
    }
    //------------------------------------------------------ ACIA
    /*
     ACIA status register
     Bit 0 = Receive data register full
     Bit 1 = Transmit data register empty
     Bits 2 and 3 pertain to modem (DCD and CTS, so ignore)
     Bit 4 = Framing error
     Bit 5 = Receiver overrun
     Bit 6 = Partity error (not used by MIDI)
     Bit 7 = Interrupt request
     */

    private int getACIAStatus() {
        int status = 0;
        if (receivedACIAByte) {
            status |= 1;
        }
        if (transmitACIAEmpty) {
            status |= 2;
        }
        if (receiverACIAOverrun) {
            status |= 32;
        }
        if (irqRequestedACIA) {
            status |= 128;
        }
        return status;
    }

    // TODO: Implement MIDI IN... some day
    private int getACIARecieve() {
        return 0;
    }

    private void processACIAControl(int value) {
        if ((value & 0x03) == ACIA_MASTER_RESET) {
            resume();
        }
    }
    ShortMessage currentMessage;
    int currentMessageStatus;
    int currentMessageData1;
    int currentMessageData2;
    int messageSize = 255;
    int currentMessageReceived = 0;

    private void processACIASend(int value) {
        if (!isRunning()) {
//            System.err.println("ACIA not active!");
            return;
        } else {
//            System.out.println("ACIA send "+value);
        }
        // First off try to finish off previous command already in play
        boolean sendMessage = false;
        if (currentMessage != null) {
            if ((value & 0x080) > 0) {
                // Any command byte received means we finished receiving another command
                // and valid or not, process it as-is
                if (currentMessage != null) {
                    sendMessage = true;
                }
                // If there is no current message, then we'll pick this up afterwards...
            } else {
                // If we receive a data byte ( < 128 ) then check if we have the right size
                // if so, then the command was completely received, and it's time to send it.
                currentMessageReceived++;
                if (currentMessageReceived >= messageSize) {
                    sendMessage = true;
                }
                if (currentMessageReceived == 1) {
                    currentMessageData1 = value;
                } else {
                    // Possibly redundant, but there's no reason a message should be longer than this...
                    currentMessageData2 = value;
                    sendMessage = true;
                }
            }
        }

        // If we have a command to send, then do it
        if (sendMessage == true) {
            if (synth != null && synth.isOpen()) {
                // Send message
                try {
//                    System.out.println("Sending MIDI message "+currentMessageStatus+","+currentMessageData1+","+currentMessageData2);
                    currentMessage.setMessage(currentMessageStatus, currentMessageData1, currentMessageData2);
                    synth.getReceiver().send(currentMessage, -1L);
                } catch (InvalidMidiDataException | MidiUnavailableException ex) {
                    Logger.getLogger(PassportMidiInterface.class.getName()).log(Level.SEVERE, null, ex);
                }
            }
            currentMessage = null;
        }

        // Do we have a new command byte?
        if ((value & 0x080) > 0) {
            // Start a new message
            currentMessage = new ShortMessage();
            currentMessageStatus = value;
            currentMessageData1 = 0;
            currentMessageData2 = 0;
            try {
                currentMessage.setMessage(currentMessageStatus, 0, 0);
                messageSize = currentMessage.getLength();
            } catch (InvalidMidiDataException ex) {
                messageSize = 0;
            }
            currentMessageReceived = 0;
        }

    }

    @Override
    public void resume() {
        if (isRunning() && synth != null && synth.isOpen()) {
            return;
        }
        try {
            MidiDevice.Info[] devices = MidiSystem.getMidiDeviceInfo();
            if (devices.length == 0) {
                System.out.println("No MIDI devices found");
            } else {
                for (MidiDevice.Info dev : devices) {
                    System.out.println("MIDI Device found: " + dev);
                    if (dev.getName().contains("Java Sound")) {
                        if (dev instanceof Synthesizer) {
                            synth = (Synthesizer) dev;
                            break;
                        }
                    }
                }
            }
            if (synth == null) {
                synth = MidiSystem.getSynthesizer();
            }
            if (synth != null) {
                System.out.println("Selected MIDI device: " + synth.getDeviceInfo().getName());
                synth.open();
                super.resume();
            }
        } catch (MidiUnavailableException ex) {
            System.out.println("Could not open MIDI synthesizer");
            Logger.getLogger(PassportMidiInterface.class.getName()).log(Level.SEVERE, null, ex);
        }
    }

    private void suspendACIA() {
        // TODO: Stop ACIA thread...
        if (synth != null && synth.isOpen()) {
            synth.close();
            synth = null;
        }
    }
}
