/*
 * HalfNES by Andrew Hoffman
 * Licensed under the GNU GPL Version 3. See LICENSE file
 */
package com.grapeshot.halfnes;

import com.grapeshot.halfnes.ui.Oscilloscope;
import com.grapeshot.halfnes.audio.*;
import com.grapeshot.halfnes.mappers.Mapper;
import java.util.ArrayList;

public class APU {

    public int samplerate;
    private final Timer[] timers = {new SquareTimer(8, 2), new SquareTimer(8, 2),
        new TriangleTimer(), new NoiseTimer()};
    private double cyclespersample;
    public final NES nes;
    CPU cpu;
    CPURAM cpuram;
    public int sprdma_count;
    private int apucycle = 0, remainder = 0;
    private int[] noiseperiod;
    // different for PAL
    private long accum = 0;
    private final ArrayList<ExpansionSoundChip> expnSound = new ArrayList<>();
    private boolean soundFiltering;
    private final static int[] TNDLOOKUP = initTndLookup(), SQUARELOOKUP = initSquareLookup();
    private int framectrreload;
    private int framectrdiv = 7456;
    private int dckiller = 0;
    private int lpaccum = 0;
    private boolean apuintflag = true, statusdmcint = false, statusframeint = false;
    private int framectr = 0, ctrmode = 4;
    private final boolean[] lenCtrEnable = {true, true, true, true};
    private final int[] volume = new int[4];
    //dmc instance variables
    private int[] dmcperiods;
    private int dmcrate = 0x36, dmcpos = 0, dmcshiftregister = 0, dmcbuffer = 0,
            dmcvalue = 0, dmcsamplelength = 1, dmcsamplesleft = 0,
            dmcstartaddr = 0xc000, dmcaddr = 0xc000, dmcbitsleft = 8;
    private boolean dmcsilence = true, dmcirq = false, dmcloop = false, dmcBufferEmpty = true;
    //length ctr instance variables
    private final int[] lengthctr = {0, 0, 0, 0};
    private final static int[] lenctrload = {10, 254, 20, 2, 40, 4, 80, 6,
        160, 8, 60, 10, 14, 12, 26, 14, 12, 16, 24, 18, 48, 20, 96, 22,
        192, 24, 72, 26, 16, 28, 32, 30};
    private final boolean[] lenctrHalt = {true, true, true, true};
    //linear counter instance vars
    private int linearctr = 0;
    private int linctrreload = 0;
    private boolean linctrflag = false;
    //instance variables for envelope units
    private final int[] envelopeValue = {15, 15, 15, 15};
    private final int[] envelopeCounter = {0, 0, 0, 0};
    private final int[] envelopePos = {0, 0, 0, 0};
    private final boolean[] envConstVolume = {true, true, true, true};
    private final boolean[] envelopeStartFlag = {false, false, false, false};
    //instance variables for sweep unit
    private final boolean[] sweepenable = {false, false},
            sweepnegate = {false, false},
            sweepsilence = {false, false},
            sweepreload = {false, false};
    private final int[] sweepperiod = {15, 15}, sweepshift = {0, 0}, sweeppos = {0, 0};
    private int cyclesperframe;
    private AudioOutInterface ai;

    public APU(final NES nes, final CPU cpu, final CPURAM cpuram) {
        this.samplerate = 1; //just in case we can't init audio
        //then init the audio stream
        this.nes = nes;
        this.cpu = cpu;
        this.cpuram = cpuram;
        setParameters();
    }

    private static int[] initTndLookup() {
        int[] lookup = new int[203];
        for (int i = 0; i < lookup.length; ++i) {
            lookup[i] = (int) ((163.67 / (24329.0 / i + 100)) * 49151);
        }
        return lookup;
    }

    private static int[] initSquareLookup() {
        //fill square, triangle volume lookup tables
        int[] lookup = new int[31];
        for (int i = 0; i < lookup.length; ++i) {
            lookup[i] = (int) ((95.52 / (8128.0 / i + 100)) * 49151);
        }
        return lookup;
    }

    public final synchronized void setParameters() {
        Mapper.TVType tvtype = cpuram.mapper.getTVType();
        soundFiltering = PrefsSingleton.get().getBoolean("soundFiltering", true);
        samplerate = PrefsSingleton.get().getInt("sampleRate", 44100);
        if (ai != null) {
            ai.destroy();
        }
        ai = new SwingAudioImpl(nes, samplerate, tvtype);
        if (PrefsSingleton.get().getBoolean("showScope", false)) {
            ai = new Oscilloscope(ai);
        }
        //pick the appropriate pitches and lengths for NTSC or PAL
        switch (tvtype) {
            case NTSC:
            default:
                this.dmcperiods = new int[]{428, 380, 340, 320, 286, 254, 226, 214, 190, 160, 142, 128, 106, 84, 72, 54};
                this.noiseperiod = new int[]{4, 8, 16, 32, 64, 96, 128, 160, 202, 254, 380, 508, 762, 1016, 2034, 4068};
                this.framectrreload = 7456;
                cyclespersample = 1789773.0 / samplerate;
                cyclesperframe = 29781;
                break;

            case DENDY:
                this.dmcperiods = new int[]{428, 380, 340, 320, 286, 254, 226, 214, 190, 160, 142, 128, 106, 84, 72, 54};
                this.noiseperiod = new int[]{4, 8, 16, 32, 64, 96, 128, 160, 202, 254, 380, 508, 762, 1016, 2034, 4068};
                this.framectrreload = 7456;
                cyclespersample = 1773448.0 / samplerate;
                cyclesperframe = 35469;
                break;
            case PAL:
                cyclespersample = 1662607.0 / samplerate;
                this.dmcperiods = new int[]{398, 354, 316, 298, 276, 236, 210, 198, 176, 148, 132, 118, 98, 78, 66, 50};
                this.noiseperiod = new int[]{4, 8, 14, 30, 60, 88, 118, 148, 188, 236, 354, 472, 708, 944, 1890, 3778};
                this.framectrreload = 8312;
                cyclesperframe = 33252;
                break;
        }
//        ai = new Reverberator(ai, 2,0.7,0.8,0.99);
//        ai = new Reverberator(ai, 243,0.5,0.7,0.99);
//       ai = new Reverberator(ai, 4001,0.3,0.5,0.99);
//       ai = new Reverberator(ai, 20382,0.2,0.3,0.9);
    }

    public boolean bufferHasLessThan(int samples) {
        return ai.bufferHasLessThan(samples);
    }

    public final int read(final int addr) {
        updateto((int) cpu.clocks);
        switch (addr) {
            case 0x15:
                //returns channel status
                //for future ref: NEED to put those ternary operators in parentheses!
                //otherwise order of operations does the wrong thing.
                final int returnval = ((lengthctr[0] > 0) ? 1 : 0)
                        | ((lengthctr[1] > 0) ? 2 : 0)
                        | ((lengthctr[2] > 0) ? 4 : 0)
                        | ((lengthctr[3] > 0) ? 8 : 0)
                        | ((dmcsamplesleft > 0) ? 16 : 0)
                        | (statusframeint ? 64 : 0)
                        | (statusdmcint ? 128 : 0);
                if (statusframeint) {
                    //System.err.println("Frame interrupt ack at " + cpu.cycles);
                    --cpu.interrupt;
                    statusframeint = false;
                }

                //System.err.println("*" + utils.hex(returnval));
                return returnval;
            case 0x16:
                nes.getcontroller1().strobe();
                return nes.getcontroller1().getbyte() | 0x40;
            case 0x17:
                nes.getcontroller2().strobe();
                return nes.getcontroller2().getbyte() | 0x40;
            default:
                return 0x40; //open bus
        }
    }
    final private static int[][] DUTYLOOKUP = {
        {0, 1, 0, 0, 0, 0, 0, 0},
        {0, 1, 1, 0, 0, 0, 0, 0},
        {0, 1, 1, 1, 1, 0, 0, 0},
        {1, 0, 0, 1, 1, 1, 1, 1}
    };

    public void addExpnSound(ExpansionSoundChip chip) {
        expnSound.add(chip);
    }

    public void destroy() {
        ai.destroy();
    }

    public void pause() {
        ai.pause();
    }

    public void resume() {
        ai.resume();
    }

    public final void write(final int reg, final int data) {
        //This is how values written to any of the APU's memory
        //mapped registers change the state of the system.
        updateto((int) cpu.clocks - 1);
        //System.err.println("Wrote " + utils.hex(data) + " to " + utils.hex(reg) + " @ cycle " + cpu.cycles);
        switch (reg) {
            case 0x0:
                //length counter 1 halt
                lenctrHalt[0] = ((data & (utils.BIT5)) != 0);
                // pulse 1 duty cycle
                timers[0].setduty(DUTYLOOKUP[data >> 6]);
                // and envelope
                envConstVolume[0] = ((data & (utils.BIT4)) != 0);
                envelopeValue[0] = data & 15;
                //setvolumes();
                break;
            case 0x1:
                //pulse 1 sweep setup
                //sweep enabled
                sweepenable[0] = ((data & (utils.BIT7)) != 0);
                //sweep divider period
                sweepperiod[0] = (data >> 4) & 7;
                //sweep negate flag
                sweepnegate[0] = ((data & (utils.BIT3)) != 0);
                //sweep shift count
                sweepshift[0] = (data & 7);
                sweepreload[0] = true;
                break;
            case 0x2:
                // pulse 1 timer low bit
                timers[0].setperiod((timers[0].getperiod() & 0xfe00) + (data << 1));
                break;
            case 0x3:
                // length counter load, timer 1 high bits
                if (lenCtrEnable[0]) {
                    lengthctr[0] = lenctrload[data >> 3];
                }
                timers[0].setperiod((timers[0].getperiod() & 0x1ff) + ((data & 7) << 9));
                // sequencer restarted
                timers[0].reset();
                //envelope also restarted
                envelopeStartFlag[0] = true;
                break;
            case 0x4:
                //length counter 2 halt
                lenctrHalt[1] = ((data & (utils.BIT5)) != 0);
                // pulse 2 duty cycle
                timers[1].setduty(DUTYLOOKUP[data >> 6]);
                // and envelope
                envConstVolume[1] = ((data & (utils.BIT4)) != 0);
                envelopeValue[1] = data & 15;
                //setvolumes();
                break;
            case 0x5:
                //pulse 2 sweep setup
                //sweep enabled
                sweepenable[1] = ((data & (utils.BIT7)) != 0);
                //sweep divider period
                sweepperiod[1] = (data >> 4) & 7;
                //sweep negate flag
                sweepnegate[1] = ((data & (utils.BIT3)) != 0);
                //sweep shift count
                sweepshift[1] = (data & 7);
                sweepreload[1] = true;
                break;
            case 0x6:
                // pulse 2 timer low bit
                timers[1].setperiod((timers[1].getperiod() & 0xfe00) + (data << 1));
                break;
            case 0x7:
                if (lenCtrEnable[1]) {
                    lengthctr[1] = lenctrload[data >> 3];
                }
                timers[1].setperiod((timers[1].getperiod() & 0x1ff) + ((data & 7) << 9));
                // sequencer restarted
                timers[1].reset();
                //envelope also restarted
                envelopeStartFlag[1] = true;
                break;
            case 0x8:
                //triangle linear counter load
                linctrreload = data & 0x7f;
                //and length counter halt
                lenctrHalt[2] = ((data & (utils.BIT7)) != 0);
                break;
            case 0x9:
                break;
            case 0xA:
                // triangle low bits of timer
                timers[2].setperiod((((timers[2].getperiod() * 1) & 0xff00) + data));
                break;
            case 0xB:
                // triangle length counter load
                // and high bits of timer
                if (lenCtrEnable[2]) {
                    lengthctr[2] = lenctrload[data >> 3];
                }
                timers[2].setperiod((((timers[2].getperiod() * 1) & 0xff) + ((data & 7) << 8)));
                linctrflag = true;
                break;
            case 0xC:
                //noise halt and envelope
                lenctrHalt[3] = ((data & (utils.BIT5)) != 0);
                envConstVolume[3] = ((data & (utils.BIT4)) != 0);
                envelopeValue[3] = data & 0xf;
                //setvolumes();
                break;
            case 0xD:
                break;
            case 0xE:
                timers[3].setduty(((data & (utils.BIT7)) != 0) ? 6 : 1);
                timers[3].setperiod(noiseperiod[data & 15]);
                break;
            case 0xF:
                //noise length counter load, envelope restart
                if (lenCtrEnable[3]) {
                    lengthctr[3] = lenctrload[data >> 3];
                }
                envelopeStartFlag[3] = true;
                break;
            case 0x10:
                dmcirq = ((data & (utils.BIT7)) != 0);
                dmcloop = ((data & (utils.BIT6)) != 0);
                dmcrate = dmcperiods[data & 0xf];
                if (!dmcirq && statusdmcint) {
                    --cpu.interrupt;
                    statusdmcint = false;
                }
                //System.err.println(dmcirq ? "dmc irq on" : "dmc irq off");
                break;
            case 0x11:
                dmcvalue = data & 0x7f;
                break;
            case 0x12:
                dmcstartaddr = (data << 6) + 0xc000;
                break;
            case 0x13:
                dmcsamplelength = (data << 4) + 1;
                break;
            case 0x14:
                //sprite dma
                for (int i = 0; i < 256; ++i) {
                    cpuram.write(0x2004, cpuram.read((data << 8) + i));
                }
                //account for time stolen from cpu
                sprdma_count = 2;
                break;
            case 0x15:
                //status register
                // counter enable(silence channel when bit is off)
                for (int i = 0; i < 4; ++i) {
                    lenCtrEnable[i] = ((data & (1 << i)) != 0);
                    //THIS was the channels not cutting off bug! If you toggle a channel's
                    //status on and off very quickly then the length counter should
                    //IMMEDIATELY be forced to zero.
                    if (!lenCtrEnable[i]) {
                        lengthctr[i] = 0;
                    }
                }
                if (((data & (utils.BIT4)) != 0)) {
                    if (dmcsamplesleft == 0) {
                        restartdmc();
                    }
                } else {
                    dmcsamplesleft = 0;
                    dmcsilence = true;
                }
                if (statusdmcint) {
                    --cpu.interrupt;
                    statusdmcint = false;
                }
                break;
            case 0x16:
                // latch controller 1 + 2
                nes.getcontroller1().output(((data & (utils.BIT0)) != 0));
                nes.getcontroller2().output(((data & (utils.BIT0)) != 0));
                break;
            case 0x17:
                ctrmode = ((data & (utils.BIT7)) != 0) ? 5 : 4;
                //System.err.println("reset " + ctrmode + ' ' + cpu.cycles);
                apuintflag = ((data & (utils.BIT6)) != 0);
                //set is no interrupt, clear is an interrupt
                framectr = 0;
                framectrdiv = framectrreload + 8; //Why +8?
                if (apuintflag && statusframeint) {
                    statusframeint = false;
                    --cpu.interrupt;
                    //System.err.println("Frame interrupt off at " + cpu.cycles);
                }
                if (ctrmode == 5) {
                    //everything frame counter runs is clocked no matter what
                    setenvelope();
                    setlinctr();
                    setlength();
                    setsweep();
                }
                break;
            default:
                break;
        }
    }

    public final void updateto(final int cpucycle) {
        //still have to run this even if sound is disabled, some games rely on DMC IRQ etc.
        if (soundFiltering) {
            //linear sampling code
            //should really be a FIR filter + decimator instead
            //but I don't have the DSP experience to design something like that
            //that would be fast enough to work / not require calculating every sample
            //this works well enough at eliminating aliasing anyway.
            while (apucycle < cpucycle) {
                ++remainder;
                clockdmc();
                if (--framectrdiv <= 0) {
                    framectrdiv = framectrreload;
                    clockframecounter();
                }
                timers[0].clock();
                timers[1].clock();
                if (lengthctr[2] > 0 && linearctr > 0) {
                    timers[2].clock();
                }
                timers[3].clock();
                if (!expnSound.isEmpty()) {
                    for (ExpansionSoundChip c : expnSound) {
                        c.clock(1);
                    }
                }
                accum += getOutputLevel();

                if ((apucycle % cyclespersample) < 1) {
                    //not quite right - there's a non-integer # cycles per sample.
                    ai.outputSample(lowpass_filter(highpass_filter((int) (accum / remainder))));
                    remainder = 0;
                    accum = 0;
                }
                ++apucycle;
            }
        } else {
            //point sampling code
            while (apucycle < cpucycle) {
                ++remainder;
                clockdmc();
                if (--framectrdiv <= 0) {
                    framectrdiv = framectrreload;
                    clockframecounter();
                }
                if ((apucycle % cyclespersample) < 1) {
                    //not quite right - there's a non-integer # cycles per sample.
                    timers[0].clock(remainder);
                    timers[1].clock(remainder);
                    if (lengthctr[2] > 0 && linearctr > 0) {
                        timers[2].clock(remainder);
                    }
                    timers[3].clock(remainder);
                    int mixvol = getOutputLevel();
                    if (!expnSound.isEmpty()) {
                        for (ExpansionSoundChip c : expnSound) {
                            c.clock(remainder);
                        }
                    }
                    remainder = 0;
                    ai.outputSample(lowpass_filter(highpass_filter(mixvol)));
                }
                ++apucycle;
            }
        }
    }

    private int getOutputLevel() {
        int vol;
        vol = SQUARELOOKUP[volume[0] * timers[0].getval()
                + volume[1] * timers[1].getval()];
        vol += TNDLOOKUP[3 * timers[2].getval()
                + 2 * volume[3] * timers[3].getval()
                + dmcvalue];
        if (!expnSound.isEmpty()) {
            vol *= 0.8;
            for (ExpansionSoundChip c : expnSound) {
                vol += c.getval();
            }
        }
        return vol; //as usual, lack of unsigned types causes unending pain.
    }

    private int highpass_filter(int sample) {
        //for killing the dc in the signal
        sample += dckiller;
        dckiller -= sample >> 8;//the actual high pass part
        dckiller += (sample > 0 ? -1 : 1);//guarantees the signal decays to exactly zero
        return sample;
    }

    private int lowpass_filter(int sample) {
        sample += lpaccum;
        lpaccum -= sample * 0.9;
        return lpaccum;
    }

    public final void finishframe() {
        updateto(cyclesperframe);
        apucycle = 0;
        ai.flushFrame(nes.isFrameLimiterOn());
    }

    private void clockframecounter() {
        //System.err.println("frame ctr clock " + framectr + ' ' + cpu.cycles);
        //should be ~4x a frame, 240 Hz
        //but the problem is this isn't exactly related to the video signal,
        //it's a completely separate timer, so the phase can shift in relation to the
        //video signal. also in the current implementation APU interrupts can only be fired when
        //an APU register is written/read from, or @ end of frame. So both of those need work
        if ((ctrmode == 4)
                || (ctrmode == 5 && (framectr != 3))) {
            setenvelope();
            setlinctr();
        }
        if ((ctrmode == 4 && (framectr == 1 || framectr == 3))
                || (ctrmode == 5 && (framectr == 1 || framectr == 4))) {
            setlength();
            setsweep();
        }
        if (!apuintflag && (framectr == 3) && (ctrmode == 4) && !statusframeint) {
            ++cpu.interrupt;
            //System.err.println("frame interrupt set at " + cpu.cycles);
            statusframeint = true;

        }
        ++framectr;
        framectr %= ctrmode;
        setvolumes();
    }

    private void setvolumes() {
        volume[0] = ((lengthctr[0] <= 0 || sweepsilence[0]) ? 0 : (((envConstVolume[0]) ? envelopeValue[0] : envelopeCounter[0])));
        volume[1] = ((lengthctr[1] <= 0 || sweepsilence[1]) ? 0 : (((envConstVolume[1]) ? envelopeValue[1] : envelopeCounter[1])));
        volume[3] = ((lengthctr[3] <= 0) ? 0 : ((envConstVolume[3]) ? envelopeValue[3] : envelopeCounter[3]));
        //System.err.println("setvolumes " + volume[1]);
    }

    private void clockdmc() {
        if (dmcBufferEmpty && dmcsamplesleft > 0) {
            dmcfillbuffer();
        }
        dmcpos = (dmcpos + 1) % dmcrate;
        if (dmcpos == 0) {
            if (dmcbitsleft <= 0) {
                dmcbitsleft = 8;
                if (dmcBufferEmpty) {
                    dmcsilence = true;
                } else {
                    dmcsilence = false;
                    dmcshiftregister = dmcbuffer;
                    dmcBufferEmpty = true;
                }
            }
            if (!dmcsilence) {
                dmcvalue += (((dmcshiftregister & (utils.BIT0)) != 0) ? 2 : -2);
                //DMC output register doesn't wrap around
                if (dmcvalue > 0x7f) {
                    dmcvalue = 0x7f;
                }
                if (dmcvalue < 0) {
                    dmcvalue = 0;
                }
                dmcshiftregister >>= 1;
                --dmcbitsleft;

            }
        }
    }

    private void dmcfillbuffer() {
        if (dmcsamplesleft > 0) {
            dmcbuffer = cpuram.read(dmcaddr++);
            dmcBufferEmpty = false;
            cpu.stealcycles(4);
            //DPCM Does steal cpu cycles - this should actually vary between 1-4
            //can't do this properly without a cycle accurate cpu/ppu
            if (dmcaddr > 0xffff) {
                dmcaddr = 0x8000;
            }
            --dmcsamplesleft;
            if (dmcsamplesleft == 0) {
                if (dmcloop) {
                    restartdmc();
                } else if (dmcirq && !statusdmcint) {
                    //this is supposed to fire after we've just READ the
                    //last byte, not when coming back AFTER reading the last byte
                    //and finding that there are no more bytes left to read.
                    //that meant all dmc timing was too long.
                    ++cpu.interrupt;
                    statusdmcint = true;
                    //System.err.println("dmc irq fire");
                }

            }
        } else {
            dmcsilence = true;
        }
    }

    private void restartdmc() {
        dmcaddr = dmcstartaddr;
        dmcsamplesleft = dmcsamplelength;
        dmcsilence = false;
    }

    private void setlength() {
        for (int i = 0; i < 4; ++i) {
            if (!lenctrHalt[i] && lengthctr[i] > 0) {
                --lengthctr[i];
                if (lengthctr[i] == 0) {
                    setvolumes();
                }
            }
        }
    }

    private void setlinctr() {
        if (linctrflag) {
            linearctr = linctrreload;
        } else if (linearctr > 0) {
            --linearctr;
        }
        if (!lenctrHalt[2]) {
            linctrflag = false;
        }
    }

    private void setenvelope() {
        //System.err.println("envelope");
        for (int i = 0; i < 4; ++i) {
            if (envelopeStartFlag[i]) {
                envelopeStartFlag[i] = false;
                envelopePos[i] = envelopeValue[i] + 1;
                envelopeCounter[i] = 15;
            } else {
                --envelopePos[i];
            }
            if (envelopePos[i] <= 0) {
                envelopePos[i] = envelopeValue[i] + 1;
                if (envelopeCounter[i] > 0) {
                    --envelopeCounter[i];
                } else if (lenctrHalt[i] && envelopeCounter[i] <= 0) {
                    envelopeCounter[i] = 15;
                }
            }
        }
    }

    private void setsweep() {
        //System.err.println("sweep");
        for (int i = 0; i < 2; ++i) {
            sweepsilence[i] = false;
            if (sweepreload[i]) {
                sweepreload[i] = false;
                sweeppos[i] = sweepperiod[i];
            }
            ++sweeppos[i];
            final int rawperiod = (timers[i].getperiod() >> 1);
            int shiftedperiod = (rawperiod >> sweepshift[i]);
            if (sweepnegate[i]) {
                //invert bits of period
                //add 1 on second channel only
                shiftedperiod = -shiftedperiod + i;
            }
            shiftedperiod += rawperiod;
            if ((rawperiod < 8) || shiftedperiod > 0x7ff) {
                // silence channel
                sweepsilence[i] = true;
            } else if (sweepenable[i] && (sweepshift[i] != 0) && lengthctr[i] > 0
                    && sweeppos[i] > sweepperiod[i]) {
                sweeppos[i] = 0;
                timers[i].setperiod(shiftedperiod << 1);
            }
        }
    }
}
