using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Runtime.InteropServices;
using System.Windows.Interop;

namespace TwoMbit
{
    public class Cart
    {
        Config cfg;
        Language lang;
        App app;

        public enum _System
        {
            SNES, GENESIS, SMS
        };

        struct Gcart
        {
            public string path, fn;
            public string slotApath, slotAfn;
            public string slotBpath, slotBfn;
            public string uniqueFn;
            public Header header;
            public string error;
        };

        Gcart gcart;
        volatile bool loaded = false;
        _System unit;

        Memory rom, sram, rtc;					            //standard Cart
        Memory slotArom, slotAsram, slotBrom, slotBsram;    //Sufami Turbo Cart, BSX

        string loadedFile;

        public Cart(App app, Config cfg, Language lang)
        {
            this.app = app;
            this.cfg = cfg;
            this.lang = lang;

            this.rom = new Memory();
            this.sram = new Memory();
            this.rtc = new Memory();
            this.slotArom = new Memory();
            this.slotAsram = new Memory();
            this.slotBrom = new Memory();
            this.slotBsram = new Memory();
            this.gcart = new Gcart();
        }

        public Memory getMemoryRom() { return rom; }
        public Memory getMemorySram() { return sram; }
        public Memory getMemoryRtc() { return rtc; }
        public Memory getMemorySlotArom() { return slotArom; }
        public Memory getMemorySlotAsram() { return slotAsram; }
        public Memory getMemorySlotBrom() { return slotBrom; }
        public Memory getMemorySlotBsram() { return slotBsram; }

        public string getName()
        {
            return gcart.header.getCartName();
        }

        public Header.Type getType()
        {
            return gcart.header.getType();
        }

        public Cart._System getSystem()
        {
            return unit;
        }

        public Header.Region getRegion()
        {
            return gcart.header.getRegion();
        }

        public bool isLoaded()
        {
            return loaded;
        }

        public Header.Region getCfgRegion() {
            int region = cfg.get<int>("video_region");
            switch (region) {
                case 0:
                default: return getRegion();  //auto by header validation
                case 1: return Header.Region.USA;
                case 2: return Header.Region.JAPAN;
                case 3: return Header.Region.EUROPE;
            }
        }

        public bool load(string fn, _System unit)
        {
            this.unit = unit;
            this.loadedFile = fn;
            if (!loadFile(fn, this.rom, ref this.gcart)) {
                app.showError(this.gcart.error);
                return false;
            }

            switch(this.gcart.header.getType()) {
                case Header.Type.SNES_STROM_BIOS:
                    cfg.set<string>("path_sufami_turbo_bios", fn);
                    app.openLoaderGui("", LoaderWindow.Type.SUFAMI_TURBO);
                    return false;
                case Header.Type.SNES_STROM:
                    if (!loadFile(cfg.get<string>("path_sufami_turbo_bios"), this.rom, ref this.gcart)) {
                        app.showError(lang.get("error_st_bios") + "\n\n" + this.gcart.error);
                        return false;
                    } else if (this.gcart.header.getType() != Header.Type.SNES_STROM_BIOS) {
                        app.showError(lang.get("error_st_bios"));
                        return false;
                    }
                    app.openLoaderGui(fn, LoaderWindow.Type.SUFAMI_TURBO);
                    return false;
                case Header.Type.SNES_BSXROM_BIOS:
                    cfg.set<string>("path_bsx_bios", fn);
                    app.openLoaderGui("", LoaderWindow.Type.BSX);
                    return false;
                case Header.Type.SNES_BSXROM:
                    if (!loadFile(cfg.get<string>("path_bsx_bios"), this.rom, ref this.gcart)) {
                        app.showError(lang.get("error_bsx_bios") + "\n\n" + this.gcart.error);
                        return false;
                    } else if (this.gcart.header.getType() != Header.Type.SNES_BSXROM_BIOS) {
                        app.showError(lang.get("error_bsx_bios"));
                        return false;
                    }
                    return loadBsx(fn);
                case Header.Type.SNES_BSCLOROM:
                case Header.Type.SNES_BSCHIROM:
                    app.openLoaderGui("", LoaderWindow.Type.BSX_SLOTTED);
                    return false;
                case Header.Type.SMS_ROM_GENERIC:
                case Header.Type.SMS_ROM_CODEMASTERS:
                case Header.Type.SMS_ROM_KOREAN:
                case Header.Type.SMS_BIOS:
                    loadSms();
                    return true;
                default:
                    loadSnes();
                    return true;
            }
        }

        public void loadSufamiTurbo(string slota, string slotb)
        {
            Gcart staCart = new Gcart();
            Gcart stbCart = new Gcart();

            if (slota != "") {
                if (!loadFile(slota, slotArom, ref staCart)) {
                    app.showError("Slot A: " + staCart.error);
                    return;
                } else if (staCart.header.getType() != Header.Type.SNES_STROM) {
                    app.showError("Slot A: " + lang.get("error_st_rom"));
                    return;
                }
                slotAsram.map(new Byte[0x020000], 0x020000);
                loadMemory(staCart.fn, "srm", slotAsram);
                gcart.slotAfn = staCart.fn;
                gcart.slotApath = staCart.path;
            }
            if (slotb != "") {
                if (!loadFile(slotb, slotBrom, ref stbCart)) {
                    app.showError("Slot B: " + stbCart.error);
                    return;
                } else if (staCart.header.getType() != Header.Type.SNES_STROM) {
                    app.showError("Slot B: " + lang.get("error_st_rom"));
                    return;
                }
                slotBsram.map(new Byte[0x020000], 0x020000);
                loadMemory(stbCart.fn, "srm", slotBsram);
                gcart.slotBfn = stbCart.fn;
                gcart.slotBpath = stbCart.path;
            }

            if (slota == "" && slotb == "") gcart.uniqueFn = gcart.fn;
            else if (slota == "") gcart.uniqueFn = gcart.slotBfn;
            else if (slotb == "") gcart.uniqueFn = gcart.slotAfn;
            else gcart.uniqueFn = gcart.slotAfn + " + " + gcart.slotBfn;
            gcart.header.setRegion(Header.Region.JAPAN);
            gcart.header.setCartName(gcart.uniqueFn);

            loaded = true;
            app.completeLoad();
        }

        public bool loadBsx(string slota)
        {
            Gcart bsxCart = new Gcart();

            if (slota != "") {
                if (!loadFile(slota, slotArom, ref bsxCart)) {
                    app.showError("BS-X Rom: " + bsxCart.error);
                    return false;
                } else if (bsxCart.header.getType() != Header.Type.SNES_BSXROM) {
                    app.showError(lang.get("error_bsx_rom"));
                    return false;
                }
            }

            sram.map(new Byte[32 * 1024], 32 * 1024);
            loadMemory(gcart.fn, "srm", sram);
            slotAsram.map(new Byte[512 * 1024], 512 * 1024);
            loadMemory(gcart.fn, "psr", slotAsram);

            gcart.slotAfn = gcart.fn;
            gcart.slotApath = gcart.path;
            gcart.uniqueFn = slota == "" ? gcart.fn : bsxCart.fn;
            gcart.header.setCartName(gcart.uniqueFn);
            gcart.header.setRegion(Header.Region.JAPAN);

            loaded = true;
            app.completeLoad();
            return true;
        }

        public void loadBsxSlotted(string slota)
        {
            Gcart flashCart = new Gcart();

            if (slota != "") {
                if (!loadFile(slota, slotArom, ref flashCart)) {
                    app.showError("BSX Flash " + flashCart.error);
                    return;
                } 
            }

            if (gcart.header.getSramSize() > 0) {
                this.sram.map(new Byte[gcart.header.getSramSize()], gcart.header.getSramSize());
                loadMemory(gcart.fn, "srm", this.sram);
            }

            gcart.uniqueFn = gcart.fn;
            if (slota != "") {
                gcart.uniqueFn += " + " + flashCart.fn;
            }
            gcart.header.setCartName(gcart.uniqueFn);
            gcart.header.setRegion(Header.Region.JAPAN);

            loaded = true;
            app.completeLoad();
        }

        void loadSnes()
        {
            if (gcart.header.getSramSize() > 0) {
                this.sram.map(new Byte[gcart.header.getSramSize()], gcart.header.getSramSize());
                loadMemory(gcart.fn, "srm", this.sram);
            }

            if (gcart.header.getChipState().srtc || gcart.header.getChipState().spc7110rtc) {
                this.rtc.map(new Byte[20], 20);
                loadMemory(gcart.fn, "rtc", this.rtc);
            }

            string cartName = gcart.header.getCartName();
            gcart.header.setCartName((cartName.IndexOf("?") == -1 && cartName != "") ? cartName : gcart.fn);
            gcart.uniqueFn = gcart.fn;

            loaded = true;
            app.completeLoad();
        }

        public void loadSms() {
            string bios_path = "";
            Header.Region _region = getCfgRegion();

            if (this.gcart.header.getType() != Header.Type.SMS_BIOS) {
                if (cfg.get<bool>("sms_use_bios")) {
                    switch (_region) {
                        case Header.Region.JAPAN:
                            bios_path = cfg.get<string>("sms_path_jp_bios"); break;
                        case Header.Region.USA:
                        case Header.Region.EUROPE:
                            bios_path = cfg.get<string>("sms_path_us_bios"); break;
                    }
                }
            }
            if (bios_path != "") {
                Gcart biosCart = new Gcart();
                if (loadFile(bios_path, slotArom, ref biosCart)) {
                    gcart.slotAfn = biosCart.fn;
                    gcart.slotApath = biosCart.path;
                } else {
                    app.showWarning("Slot Rom: " + biosCart.error);
                }
            }

            if (this.gcart.header.getSramSize() > 0) {
                this.sram.map(new Byte[this.gcart.header.getSramSize()], this.gcart.header.getSramSize());
                loadMemory(this.gcart.fn, "srm", this.sram);
            }

            gcart.header.setCartName(gcart.fn);
            gcart.uniqueFn = gcart.fn;

            loaded = true;
            app.completeLoad();
        }

        void loadMemory(string fn, string extension, Memory memory)
        {
            string loadPath = getSramPath() + fn + "." + extension;
            File file = new File(loadPath);
            byte[] data = file.fetch();
            if (file.getError() == File.Error.FINE) {
                Buffer.BlockCopy(data, 0, memory.getData(), 0, (int)Math.Min(file.getSize(), memory.getSize()));
            }
        }

        void saveMemory(string fn, string extension, Memory memory)
        {
            if (memory.getSize() == 0) return;
            if (!hasMemoryContent(memory.getData(), memory.getSize())) return;
            string savePath = getSramPath() + fn + "." + extension;
            File file = new File(savePath);
            file.write(savePath, memory.getData(), memory.getSize());
        }

        bool hasMemoryContent(byte[] data, uint size)
        {   //don't save if only filled with zero
            for (uint i=0; i < size; i++) {
                if (data[i] != 0) return true;
            }
            return false;
        }

        public void unload()
        {
            if (!loaded) return;
            unpinning();
            saveMemory(gcart.fn, "srm", sram);
            saveMemory(gcart.fn, "rtc", rtc);
            saveMemory(gcart.slotAfn, "srm", slotAsram);
            saveMemory(gcart.slotBfn, "srm", slotBsram);
            loaded = false;
        }

        string getSramPath()
        {
            string sramPath = "\\";
            foreach (KeyValuePair<Cart._System, string> ident in App.systemIdent) {
                if (ident.Key == this.unit) {
                    sramPath = cfg.get<string>(ident.Value + "_path_sram") + "\\";
                    break;
                }
            }
            
            if (sramPath == "\\") {
                sramPath = App.APP_DIR + "sram\\";
            }
            if (File.generateDir(sramPath)) {
                return sramPath;
            }
            return App.APP_DIR;
        }

        public bool testFile(_System unit, string fn, Header.Type type)
        {
            this.unit = unit;
            if (loadFile(fn, this.rom, ref this.gcart)) {
                if (gcart.header.getType() == type) {
                    return true;
                }
            }
            return false;
        }

        bool loadFile(string fn, Memory memory, ref Gcart cart)
        {
            File file = new File(fn);
            File.Error error;
            byte[] data = file.fetch();
            if ((error = file.getError()) != File.Error.FINE) {
                cart.error = formatError(error, fn);
                return false;
            }
            uint romSize = file.getSize();

            if ((romSize & 0x7fff) == 0x0200) { //remove 512 Byte header of diverse copy stations
                romSize -= 512;
                Buffer.BlockCopy(data, 512, data, 0, (int)romSize);
            }
            if ((romSize % 1024) != 0) { //damnnnn, what the ... fill with zeros
                uint missing = 1024 - (romSize % 1024);
                byte[] tempData = new byte[romSize + missing];
                Buffer.BlockCopy(data, 0, tempData, 0, (int)romSize);
                data = tempData;
                romSize += missing;
            }

            memory.map(data, romSize);
            cart.fn = file.getFilename();
            cart.path = file.getPath();
            Header header = new Header(romSize, data);
            header.detect(this.unit);
            cart.header = header;
            return true;
        }

        string formatError(File.Error error, string fn)
        {
            switch (error) {
                case File.Error.OPEN:
                default:
                    return lang.get("load_error") + "\n" + fn;
                case File.Error.EXTRACTOR_DLL:
                    return lang.get("extract_error") + "\n" + fn;
                case File.Error.TAR_GZ:
                    return lang.get("tgz_error") + "\n" + fn;
            }
        }

        public void pinning() {
            rom.pinning();
            sram.pinning();
            rtc.pinning();
            slotArom.pinning();
            slotBrom.pinning();
            slotAsram.pinning();
            slotBsram.pinning();
        }

        public void unpinning() {
            rom.unpinning();
            sram.unpinning();
            rtc.unpinning();
            slotArom.unpinning();
            slotAsram.unpinning();
            slotBrom.unpinning();
            slotBsram.unpinning();
        }
    }
}
