/*
 * 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.PPU;
import java.util.Arrays;
import java.util.prefs.Preferences;
import java.util.zip.CRC32;

public abstract class Mapper {

    protected ROMLoader loader;
    protected int mappertype, submapper, prgsize, prgoff, chroff, chrsize;
    public CPU cpu;
    public CPURAM cpuram;
    public PPU ppu;
    protected int[] prg, chr, chr_map, prg_map, prgram = new int[8192];
    protected MirrorType scrolltype;
    protected boolean haschrram = false, hasprgram = true, savesram = false;
    // PPU nametables
    protected final int[] pput0 = new int[0x400], pput1 = new int[0x400],
            pput2 = new int[0x400], pput3 = new int[0x400];
    //99% of games only use 2 of these, but we have to create 4 and use ptrs to them
    //for those with extra RAM for 4 screen mirror
    protected int[] nt0, nt1, nt2, nt3;
    //and these are pointers to the nametables, so  for singlescreen when we switch
    //and then switch back the data in the other singlescreen NT isn't gone.
    long crc;
    TVType region;
    Preferences prefs = PrefsSingleton.get();

    public boolean supportsSaves() {
        return savesram;
    }

    public void destroy() {
        cpu = null;
        cpuram = null;
        ppu = null;
    }

    public static enum MirrorType {

        H_MIRROR, V_MIRROR, SS_MIRROR0, SS_MIRROR1, FOUR_SCREEN_MIRROR
    };

    public static enum TVType {

        NTSC,
        PAL,
        DENDY;
    }

    public static long crc32(int[] array) {
        CRC32 c = new CRC32();
        for (int i : array) {
            c.update(i);
        }
        return c.getValue();
    }

    public void loadrom() throws BadMapperException {
        loader.parseHeader();
        prgsize = loader.prgsize;
        mappertype = loader.mappertype;
        prgoff = loader.prgoff;
        chroff = loader.chroff;
        chrsize = loader.chrsize;
        scrolltype = loader.scrolltype;
        savesram = loader.savesram;
        prg = loader.load(prgsize, prgoff);
        region = loader.tvtype;
        submapper = loader.submapper;
        crc = crc32(prg);
        //System.err.println(utils.hex(crc));
        //crc "database" for certain impossible-to-recognize games
        if ((crc == 0x41243492L) //low g man (u)
                || (crc == 0x98CCD385L)//low g man (e)
                ) {
            hasprgram = false;
        }
        chr = loader.load(chrsize, chroff);

        if (chrsize == 0) {//chr ram
            haschrram = true;
            chrsize = 8192;
            chr = new int[8192];
        }
        prg_map = new int[32];
        for (int i = 0; i < 32; ++i) {
            prg_map[i] = (1024 * i) & (prgsize - 1);
        }
        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, 0xa0);
        Arrays.fill(pput1, 0xb0);
        Arrays.fill(pput2, 0xc0);
        Arrays.fill(pput3, 0xd0);
        setmirroring(scrolltype);
    }

    public void reset() {
        //this is empty so that mappers w/o some specific instructions
        //on soft reset need not implement this
    }

    //write into the cartridge's address space
    public void cartWrite(final int addr, final int data) {
        //default no-mapper operation just writes if in PRG RAM range
        if (addr >= 0x6000 && addr < 0x8000) {
            prgram[addr & 0x1fff] = data;
        }
    }

    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) {
            return prg[prg_map[((addr & 0x7fff)) >> 10] + (addr & 1023)];
        } else if (addr >= 0x6000 && hasprgram) {
            return prgram[addr & 0x1fff];
        }
        return addr >> 8; //open bus
    }

    public int ppuRead(int addr) {
        if (addr < 0x2000) {
            return chr[chr_map[addr >> 10] + (addr & 1023)];
        } else {
            switch (addr & 0xc00) {
                case 0:
                    return nt0[addr & 0x3ff];
                case 0x400:
                    return nt1[addr & 0x3ff];
                case 0x800:
                    return nt2[addr & 0x3ff];
                case 0xc00:
                default:
                    if (addr >= 0x3f00) {
                        addr &= 0x1f;
                        if (addr >= 0x10 && ((addr & 3) == 0)) {
                            addr -= 0x10;
                        }
                        return ppu.pal[addr];
                    } else {
                        return nt3[addr & 0x3ff];
                    }
            }
        }
    }

    public void ppuWrite(int addr, final int data) {
        addr &= 0x3fff;
        if (addr < 0x2000) {
            if (haschrram) {
                // Shame on you, Milon's Secret Castle. What possible
                // reason could you have to write to your own chr rom?
                chr[chr_map[addr >> 10] + (addr & 1023)] = data;
                // anyway, only allowing writes when there's actual ram here.
            }
        } else {
            switch (addr & 0xc00) {
                case 0x0:
                    nt0[addr & 0x3ff] = data;
                    break;
                case 0x400:
                    nt1[addr & 0x3ff] = data;
                    break;
                case 0x800:
                    nt2[addr & 0x3ff] = data;
                    break;
                case 0xc00:
                    if (addr >= 0x3f00 && addr <= 0x3fff) {
                        addr &= 0x1f;
                        //System.err.println("wrote "+utils.hex(data)+" to palette index " + utils.hex(addr));
                        if (addr >= 0x10 && ((addr & 3) == 0)) { //0x10,0x14,0x18 etc are mirrors of 0x0, 0x4,0x8 etc
                            addr -= 0x10;
                        }
                        ppu.pal[addr] = (data & 0x3f);
                    } else {
                        nt3[addr & 0x3ff] = data;
                    }
                    break;
                default:
                    System.err.println("where?");
            }
        }
    }

    public void notifyscanline(final int scanline) {
        //this is empty so that mappers w/o a scanline counter need not implement
    }

    public void cpucycle(int cycles) {
        //do it right for once
    }

    public static Mapper getCorrectMapper(final ROMLoader l) throws BadMapperException {
        int type = l.mappertype;
        boolean haschr = (l.chrsize == 0);
        switch (type) {
            case -1:
                return new NSFMapper();
            case 0:
                return new NromMapper();
            case 1:
                return new MMC1Mapper();
            case 2:
                return new UnromMapper();
            case 3:
                return new CnromMapper();
            case 4:
                return new MMC3Mapper();
            case 5:
                return new MMC5Mapper();
            case 7:
                return new AnromMapper();
            case 9:
                return new MMC2Mapper();
            case 10:
                return new MMC4Mapper();
            case 11:
                return new ColorDreamsMapper();
            case 15:
            case 169:
                return new Mapper15();
            case 19:
                return new NamcoMapper();
            case 21:
            case 23:
            case 25:
                //VRC4 has three different mapper numbers for six different address line layouts
                //some of which really should be VRC2
                //but they're all handled in the same file
                //there's a proposal for submapper #s in iNES 2.0
                return new VRC4Mapper(type);
            case 22:
                return new VRC2Mapper();
            case 24:
            case 26:
                return new VRC6Mapper(type);
            case 31:
                return new Mapper31();
            case 33:
                return new Mapper33();
            case 34:
                if (haschr) {
                    return new BnromMapper();
                } else {
                    return new NINA_001_Mapper();
                }
            case 36:
                return new Mapper36();
            case 38:
                return new CrimeBustersMapper();
            case 41:
                return new CaltronMapper();
            case 47:
                return new Mapper47();
            case 48:
                return new Mapper48();
            case 58:
                return new Mapper58();
            case 60:
                return new Mapper60();
            case 61:
                return new Mapper61();
            case 62:
                return new Mapper62();
            case 64:
                return new TengenRamboMapper();
            case 65:
                return new IremH3001Mapper();
            case 66:
                return new GnromMapper();
            case 67:
                return new Sunsoft03Mapper();
            case 68:
                return new AfterburnerMapper();
            case 69:
                return new FME7Mapper();
            case 70:
                return new Mapper70();
            case 71:
                return new CodemastersMapper();
            case 72:
                return new Mapper72();
            case 73:
                return new VRC3Mapper();
            case 75:
                return new VRC1Mapper();
            case 76:
                return new Mapper76();
            case 78:
                return new Mapper78();
            case 79:
            case 113:
                return new NINA_003_006_Mapper(type);
            case 85:
                return new VRC7Mapper();
            case 86:
                return new Mapper86();
            case 87:
                return new Mapper87();
            case 88:
            case 154:
                return new Namcot34x3Mapper(type);
            case 89:
            case 93:
                return new Sunsoft02Mapper(type);
            case 92:
                return new Mapper92();
            case 94:
                return new Mapper94();
            case 97:
                return new Mapper97();
            case 107:
                return new Mapper107();
            case 112:
                return new Mapper112();
            case 119:
                return new Mapper119();
            case 140:
                return new Mapper140();
            case 152:
                return new Mapper152();
            case 180:
                return new CrazyClimberMapper();
            case 182:
                return new Mapper182();
            case 184:
                return new Sunsoft01Mapper();
            case 185:
                return new Mapper185();
            case 200:
                return new Mapper200();
            case 201:
                return new Mapper201();
            case 203:
                return new Mapper203();
            case 206:
                return new MIMICMapper();
            case 212:
                return new Mapper212();
            case 213:
                return new Mapper213();
            case 214:
                return new Mapper214();
            case 225:
                return new Mapper225();
            case 226:
                return new Mapper226();
            case 228:
                return new Action52Mapper();
            case 229:
                return new Mapper229();
            case 231:
                return new Mapper231();
            case 240:
                return new Mapper240();
            case 241:
                return new Mapper241();
            case 242:
                return new Mapper242();
            case 244:
                return new Mapper244();
            case 246:
                return new Mapper246();
            case 255:
                return new Mapper255();
            default:
                System.err.println("unsupported mapper # " + type);
                throw new BadMapperException("Unsupported mapper: " + type);
        }
    }

    public String getrominfo() {
        return ("ROM INFO: \n"
                + "Filename:     " + loader.name + "\n"
                + "Mapper:       " + mappertype + "\n"
                + "PRG Size:     " + prgsize / 1024 + " K\n"
                + "CHR Size:     " + (haschrram ? 0 : chrsize / 1024) + " K\n"
                + "Mirroring:    " + scrolltype.toString() + "\n"
                + "Battery Save: " + ((savesram) ? "Yes" : "No")) + "\n"
                + "CRC:          " + utils.hex(this.crc);
    }

    public boolean hasSRAM() {
        return savesram;
    }

    public void setLoader(final ROMLoader l) {
        loader = l;
    }

    public CPURAM getCPURAM() {
        return cpuram;
    }

    public void checkA12(int addr) {
        //needed for mmc3 irq counter
    }

    public void setPRGRAM(final int[] newprgram) {
        prgram = newprgram.clone();

    }

    public int[] getPRGRam() {
        return prgram.clone();
    }

    public final void setmirroring(final Mapper.MirrorType type) {
        switch (type) {
            case H_MIRROR:
                nt0 = pput0;
                nt1 = pput0;
                nt2 = pput1;
                nt3 = pput1;
                break;
            case V_MIRROR:
                nt0 = pput0;
                nt1 = pput1;
                nt2 = pput0;
                nt3 = pput1;

                break;
            case SS_MIRROR0:
                nt0 = pput0;
                nt1 = pput0;
                nt2 = pput0;
                nt3 = pput0;
                break;
            case SS_MIRROR1:
                nt0 = pput1;
                nt1 = pput1;
                nt2 = pput1;
                nt3 = pput1;
                break;
            case FOUR_SCREEN_MIRROR:
            default:
                nt0 = pput0;
                nt1 = pput1;
                nt2 = pput2;
                nt3 = pput3;
                break;
        }
    }

    public TVType getTVType() {
        int prefsregion = prefs.getInt("region", 0);
        switch (prefsregion) {
            case 0:
            default://auto detect
                return region;
            case 1: //ntsc
                return TVType.NTSC;
            case 2: //pal
                return TVType.PAL;
            case 3: //dendy
                return TVType.DENDY;
        }
    }

    public void init() {
    } //needed for NSF initialization
}
