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

import com.grapeshot.halfnes.*;
import com.grapeshot.halfnes.audio.*;
import java.util.Arrays;

/**
 *
 * @author Andrew
 */
public class NSFMapper extends Mapper {
    //a nsf playing mapper
    //TODO: add the extra bankswitches required when playing FDS

    private int load, init, play, song, numSongs;
    public boolean nsfBanking;
    public int[] nsfStartBanks = new int[10], nsfBanks = new int[10];
    private int sndchip;
    boolean vrc6 = false, vrc7 = false, mmc5 = false,
            n163 = false, s5b = false, hasInitSound = false, fds = false;
    private boolean n163autoincrement = false;
    private int n163soundAddr = 0;
    private int mmc5multiplier1, mmc5multiplier2;
    private int vrc7regaddr = 0;
    private int s5bSoundCommand = 0;
    private Namco163SoundChip n163Audio;
    private VRC6SoundChip vrc6Audio;
    private VRC7SoundChip vrc7Audio;
    private Sunsoft5BSoundChip s5bAudio;
    private MMC5SoundChip mmc5Audio;
    private static final String trackstr = "Track --- / ---          <-B A->";
    private FDSSoundChip fdsAudio;

    @Override
    public void loadrom() throws BadMapperException {
        loader.parseHeader();
        prgsize = loader.prgsize;
        mappertype = loader.mappertype;
        prgoff = loader.prgoff;
        for (int i = 0x70; i < 0x78; ++i) {
            if (loader.header[i] != 0) {
                nsfBanking = true;
                nsfStartBanks[i - 0x70] = loader.header[i];
            }
        }
        prgoff = 0;
        load = loader.header[0x08] + (loader.header[0x09] << 8);
        init = loader.header[0x0a] + (loader.header[0x0b] << 8);
        play = loader.header[0x0c] + (loader.header[0x0d] << 8);
        numSongs = loader.header[6] - 1;
        song = loader.header[7] - 1;
        if (loader.header[0x7a] == 1) {
            //pal only tune
            this.region = TVType.PAL;
            //System.err.println("pal only tune");
        } else {
            this.region = TVType.NTSC;
        }
        chroff = 0;
        chrsize = 0;
        scrolltype = MirrorType.V_MIRROR;
        sndchip = loader.header[0x7B];

        if (!nsfBanking && load < 0x8000) {
            //no banking
            //System.err.println("What do I do with this???");
            throw new BadMapperException("NSF with no banking loading low");
        }
        // pad to 4k bank size and copy in starting
        //from where the load addr is in a 4k bank
        //to the end of the file, padding the end to a 4k bank as well
        //so total number of banks can be 2 more than # of 4k
        //chunks in the file.
        int paddingLen = (nsfBanking) ? load & 0x0fff : load - 0x8000;
        prg = new int[1024 * 1024];
        System.arraycopy(loader.load(loader.romlen(), prgoff), 0, prg, paddingLen, loader.romlen());
        crc = crc32(prg);
        haschrram = true;
        chrsize = 8192;
        chr = new int[8192];
        prg_map = new int[(((sndchip & (utils.BIT2)) != 0)) ? 40 : 32];
        if (!nsfBanking) {
            //identity mapping from 1st loaded bank
            for (int i = 0; i < 8; ++i) {
                nsfStartBanks[i] = i;
            }

        }
        //additional headache for NSFs with FDS:
        if (((sndchip & (utils.BIT2)) != 0)) {
            //got to copy some stuff into 6000 - 7fff just because
            nsfStartBanks[8] = nsfStartBanks[6];
            nsfStartBanks[9] = nsfStartBanks[7];

        }
        chr_map = new int[8];
        for (int i = 0; i < 8; ++i) {
            chr_map[i] = (1024 * i) & (chrsize - 1);
        }
        cpuram = new CPURAM(this);
        cpu = new CPU(cpuram);
        ppu = new PPU(this);
        Arrays.fill(pput0, 0x00);
        setmirroring(scrolltype);
        //System.out.println(sndchip);

        //set up the PPU to display titles
        //pick a random color based on the tune's crc (why not?)
        ppu.pal[0] = 0x3f;
        ppu.pal[1] = 0x20 + (int) (crc % 12);
        ppu.pal[2] = 0x20 + (int) (crc % 12);
        ppu.pal[3] = 0x20 + (int) (crc % 12);

        chr = NSFPlayerFont.font;
    }

    @Override
    public void init() {
        //now that we've set up the initial CPU state, do it all over again
        //in order to match the NSF spec.

        //set banks back to the way they were originally
        nsfBanks = nsfStartBanks.clone();
        setBanks();
        //clear all ram to 0
        for (int i = 0; i <= 0x7ff; ++i) {
            cpuram.write(i, 0);
        }
        //initialize sound registers
        for (int i = 0x4000; i <= 0x4013; ++i) {
            cpuram.write(i, 0);
        }
        cpuram.write(0x4015, 0x0f);
        //disable frame counter on APU
        cpuram.write(0x4017, 0x40);
        //simulate a jump to the play address
        cpu.push(0xff);
        cpu.push(0xfa);

        cpu.setPC(init);
        cpu.interrupt = -99999; //no interrupts for you
        cpu.setRegA(song);
        if (this.region == TVType.PAL) {
            cpu.setRegX(0x01);
        } else {
            cpu.setRegX(0x00);
        }

        //copy titles to ppu nametable
        for (int i = 0; i < 32 * 24; ++i) {
            //random pattern from basic one liner
            pput0[i] = (Math.random() > 0.5) ? 0x2f : 0x5c;
        }
        for (int i = 0; i < 96; ++i) {
            pput0[i + (32 * 25)] = loader.header[i + 0xe];
        }

        for (int i = 0; i < trackstr.length(); ++i) {
            pput0[i + (32 * 28)] = trackstr.charAt(i);
        }

        if (!hasInitSound) {
            setSoundChip();
            hasInitSound = true;
        }
        if (!fds) { //DON'T CLEAR THIS WHEN STUFF LOADS HERE
            for (int i = 0x6000; i <= 0x7fff; ++i) {
                cpuram.write(i, 0);
            }
        }
    }

    @Override
    public void reset() {
        song = loader.header[7] - 1;
        init();
        cpu.setPC(init);
    }

    //write into the cartridge's address space
    @Override
    public void cartWrite(final int addr, final int data) {
        if (n163 && addr == 0xF800) {
            n163autoincrement = ((data & (utils.BIT7)) != 0);
            n163soundAddr = data & 0x7f;
        } else if (n163 && addr == 0x4800) {
            n163Audio.write(n163soundAddr, data);
            if (n163autoincrement) {
                n163soundAddr = ++n163soundAddr & 0x7f;
            }
        } else if (s5b && addr == 0xE000) {
            s5bAudio.write(s5bSoundCommand, data);
        } else if (s5b && addr == 0xC000) {
            s5bSoundCommand = data & 0xF;
        } else if (vrc6 && addr >= 0xB000 && addr <= 0xB002) {
            vrc6Audio.write((addr & 0xf000) + (addr & 3), data);
        } else if (vrc6 && addr >= 0xA000 && addr <= 0xA002) {
            vrc6Audio.write((addr & 0xf000) + (addr & 3), data);
        } else if (vrc7 && addr == 0x9030) {
            vrc7Audio.write(vrc7regaddr, data);
        } else if (vrc7 && addr == 0x9010) {
            vrc7regaddr = data;
        } else if (vrc6 && addr >= 0x9000 && addr <= 0x9002) {
            vrc6Audio.write((addr & 0xf000) + (addr & 3), data);
        } else if (fds && nsfBanking && addr >= 0x6000) {
            if (addr < 0x8000) {
                int fuuu = prg_map[((addr - 0x6000) >> 10) + 32] + (addr & 1023);
                prg[fuuu] = data;
            } else {
                int fuuu = prg_map[((addr & 0x7fff)) >> 10] + (addr & 1023);
                prg[fuuu] = data;
            }
        } else if (fds && !nsfBanking && addr >= 0x6000) {
            if (addr < 0x8000) {
                prgram[addr - 0x6000] = data;
            } else {
                int fuuu = prg_map[((addr & 0x7fff)) >> 10] + (addr & 1023);
                prg[fuuu] = data;
            }
        } else if (addr >= 0x6000 && addr < 0x8000) {
            //default no-mapper operation just writes if in PRG RAM range
            prgram[addr & 0x1fff] = data;
        } else if ((addr >= 0x5ff8) && (addr < 0x6000)) {
            nsfBanks[addr - 0x5ff8] = data;
            //System.err.println(addr - 0x5ff8 + " " + data);
            setBanks();
        } else if (fds && nsfBanking && (addr == 0x5ff6)) {
            //System.err.println("fds request bank " + data + " in ram0");
            nsfBanks[8] = data;
            setBanks();
        } else if (fds && nsfBanking && (addr >= 0x5ff7)) {
            //System.err.println("fds request bank " + data + " in ram1");
            nsfBanks[9] = data;
            setBanks();
        } else if (mmc5 && (addr >= 0x5C00) && (addr <= 0x5FF5)) {
            prgram[addr - 0x5C00] = data; //RAM emulates ExRAM here
        } else if (mmc5 && (addr == 0x5206)) {
            mmc5multiplier2 = data;
        } else if (mmc5 && (addr == 0x5205)) {
            mmc5multiplier1 = data;
        } else if (mmc5 && (addr >= 0x5000) && (addr <= 0x5015)) {
            mmc5Audio.write(addr - 0x5000, data);
        } else if (fds && (addr >= 0x4040) && (addr <= 0x4092)) {
            fdsAudio.write(addr, data);
        } else {
            //System.err.println("write to " + utils.hex(addr) + " goes nowhere");
        }
    }

    @Override
    public int cartRead(final int addr) {
        // by default has wram at 0x6000 and cartridge at 0x8000-0xfff
        // but some mappers have different so override for those
        if (addr >= 0x8000) {
            if (addr > 0xfffa) {
                //reads of last part of RAM should always
                //give the reset vectors here, no matter what
                //NSF bank is mapped there.
                switch (addr) {
                    case 0xfffb:
                        return 0x4c;
                    case 0xfffc:
                        return 0xfb;
                    case 0xfffd:
                        return 0xff;
                    default:
                        return 0x00;
                }
            }
            int fuuu = prg_map[((addr & 0x7fff)) >> 10] + (addr & 1023);
            return prg[fuuu];
        } else if (addr >= 0x6000 && hasprgram) {
            if (fds && nsfBanking) {
                int fuuu = prg_map[((addr - 0x6000) >> 10) + 32] + (addr & 1023);
                return prg[fuuu];
            } else {
                return prgram[addr & 0x1fff];
            }
        } else if ((addr >= 0x5ff8)) {
            return nsfBanks[addr - 0x5ff8];
        } else if (fds && nsfBanking && (addr == 0x5ff6)) {
            return nsfBanks[8];
        } else if (fds && nsfBanking && (addr == 0x5ff7)) {
            return nsfBanks[9];
        } else if (mmc5 && addr >= 0x5C00) {
            return prgram[addr - 0x5C00]; //RAM emulates ExRAM here
        } else if (mmc5 && addr == 0x5206) {
            return ((mmc5multiplier1 * mmc5multiplier2) >> 8) & 0xff;
        } else if (mmc5 && addr == 0x5205) {
            return (mmc5multiplier1 * mmc5multiplier2) & 0xff;
        } else if (mmc5 && addr == 0x5015) {
            return mmc5Audio.status();
        } else if (n163 && addr == 0x4800) {
            //System.err.println("readback");
            int retval = n163Audio.read(n163soundAddr);
            if (n163autoincrement) {
                n163soundAddr = ++n163soundAddr & 0x7f;
            }
            return retval;
        } else if (fds && (addr >= 0x4040) && (addr < 0x4093)) {
            return fdsAudio.read(addr);
        }
        //System.err.println("reading open bus " + utils.hex(addr));
        return addr >> 8; //open bus
    }

    @Override
    public void ppuWrite(int addr, final int data) {
    }

    int control, prevcontrol;
    int unfinishedcounter = 0;
    int time = 4;

    @Override
    public void notifyscanline(final int scanline) {
        if (scanline == 240) {
            //make sure init isn't still running
            if (cpu.PC != 0xFFFB) {
                //if not in idle loop
                if (unfinishedcounter < time) {
                    ++unfinishedcounter;
                    /*System.err.println("Init routine hasn't returned in "
                            + unfinishedcounter + " frames");
                     */
                    return;
                } else if (unfinishedcounter == time) {
                    ++unfinishedcounter;
                    //System.err.println("giving up");
                }
                //if we've given it a few frames
                //and it still hasn't returned from init, then it probably
                //isn't going to (supernsf)
                //so we move blithely forward.
            } else {
                unfinishedcounter = 0;
            }
            //set PPU registers to enable rendering
            ppu.write(6, 0);
            ppu.write(6, 0);
            ppu.write(5, 0);
            ppu.write(0, 0);
            ppu.write(1, utils.BIT1 | utils.BIT3 | utils.BIT4);

            //write track number to screen
            writeTracks();

            //todo: visualization effects
            //read the controller
            prevcontrol = control;
            control = 0;
            //strobe
            cpuram.write(0x4016, 1);
            cpuram.write(0x4016, 0);
            //read each button out
            for (int i = 0; i < 8; ++i) {
                control = (control << 1) + (((cpuram.read(0x4016) & 3) != 0) ? 1 : 0);
            }
            //change song number if we get a button press
            if (((control & 0x80) != 0) && ((prevcontrol & 0x80) == 0)) {
                ++song;
                if (song > numSongs) {
                    song = 0;
                }
                //System.err.println("next song");
                init();
            } else if (((control & 0x40) != 0) && ((prevcontrol & 0x40) == 0)) {
                --song;
                if (song < 0) {
                    song = numSongs;
                }
                //System.err.println("previous song");
                init();
            } else //fake a jsr to the play address from wherever 
            //unless this is a supernsf
            if (unfinishedcounter <= time) {
                cpu.push((cpu.PC - 1) >> 8);
                cpu.push((cpu.PC - 1) & 0xff);
                cpu.setPC(play);
            }
        }
    }

    @Override
    public String getrominfo() {
        return "NSF INFO: \n"
                + "Filename:     " + loader.name + "\n"
                + "Size:     " + loader.romlen() / 1024 + " K\n"
                + "Expansion Sound:  " + expSound() + "\n"
                + "Track: " + (song + 1) + " / " + (numSongs + 1) + "\n"
                + "Load Address:  " + utils.hex(load) + "\n"
                + "Init Address:  " + utils.hex(init) + "\n"
                + "Play Address:  " + utils.hex(play) + "\n"
                + "Banking?      " + (nsfBanking ? "Yes" : "No") + "\n"
                + "CRC:          " + utils.hex(this.crc);
    }

    private String expSound() {
        String chips = "";
        if (vrc6) {
            chips += "VRC6 ";
        }
        if (vrc7) {
            chips += "VRC7 ";
        }
        if (n163) {
            chips += "Namco 163 ";
        }
        if (mmc5) {
            chips += "MMC5 ";
        }
        if (s5b) {
            chips += "Sunsoft 5B ";
        }
        if (fds) {
            chips += "FDS ";
        }
        return ((chips.length() > 0) ? chips : "None");
    }

    private void setBanks() {
        for (int i = 0; i < prg_map.length; ++i) {
            prg_map[i] = (4096 * nsfBanks[i / 4]) + (1024 * (i % 4));
            if ((prg_map[i]) >= prg.length) {
                //System.err.println("broken banks");
                prg_map[i] %= prg.length; //probably a bad idea in general though
                //but who knows what a NSF wants when it tries
                //to switch a bank not in the file?
                //there's no "alias it to a power of 2" for nsfs since the file
                //size doesn't need to be a power of 2 and you can start loading
                //the file halfway into a bank.
            }
        }
        //utils.printarray(prg_map);
        //utils.printarray(nsfStartBanks);
    }

    private void setSoundChip() {
        if (((sndchip & (utils.BIT0)) != 0)) {
            //VRC6 audio
            vrc6 = true;
            vrc6Audio = new VRC6SoundChip();
            cpuram.apu.addExpnSound(vrc6Audio);
        }
        if (((sndchip & (utils.BIT1)) != 0)) {
            //VRC7 audio
            vrc7 = true;
            vrc7Audio = new VRC7SoundChip();
            cpuram.apu.addExpnSound(vrc7Audio);
        }
        if (((sndchip & (utils.BIT2)) != 0)) {
            //FDS audio, not yet implemented
            fds = true;
            fdsAudio = new FDSSoundChip();
            cpuram.apu.addExpnSound(fdsAudio);
        }
        if (((sndchip & (utils.BIT3)) != 0)) {
            //MMC5 audio
            mmc5 = true;
            mmc5Audio = new MMC5SoundChip();
            cpuram.apu.addExpnSound(mmc5Audio);
        }
        if (((sndchip & (utils.BIT4)) != 0)) {
            //Namco 163 audio
            n163 = true;
            n163Audio = new Namco163SoundChip();
            cpuram.apu.addExpnSound(n163Audio);
        }
        if (((sndchip & (utils.BIT5)) != 0)) {
            //Sunsoft 5B audio
            s5b = true;
            s5bAudio = new Sunsoft5BSoundChip();
            cpuram.apu.addExpnSound(s5bAudio);
        }
    }

    private void writeTracks() {
        String cur = String.format("%3d / %-3d", song + 1, numSongs + 1);
        for (int i = 0; i < cur.length(); ++i) {
            pput0[i + (32 * 28) + 6] = cur.charAt(i);
        }
    }
}
