package turgen.plugin.csturbo;

import turgen.plugin.*;
import turgen.generator.SignalGenerator;
import turgen.config.Configuration;
import turgen.*;
import java.io.*;
import java.util.Iterator;
import javax.swing.*;
import turgen.config.BooleanConfigEntry;
import turgen.config.ConfigClass;
import turgen.config.IntegerConfigEntry;
import turgen.dtb.*;
import turgen.generator.PulseCrate;
import turgen.generator.PulseCreator;
import turgen.playlist.AbstractPlayListItem;
import turgen.tapeimage.DataChunk;
import turgen.tapeimage.TapeImageChunk;
import turgen.tapeimage.TurboChunk;

public class SuperTurboPlugin implements Plugin {

    private CsTurboPluginsPanel panel;
    private final int[] casChunkName = {116, 114, 115, 116};

    private ConfigClass cfcSuperTurbo;
    private IntegerConfigEntry cfgPilotLenA;
    private IntegerConfigEntry cfgPilotLenB;
    private BooleanConfigEntry cfgProlongatePilotTone;
    private IntegerConfigEntry cfgSilenceAfterHeader;
    private IntegerConfigEntry cfgPilotLenBlockLoading;
    private BooleanConfigEntry cfgProlongateBlockLoadingPilotTone;
    private BooleanConfigEntry cfgConvertBinaryLoadersToTurbo2000;
    private BooleanConfigEntry cfgPrependLoader;

    /**
     * Create new SuperTurboPlugin
     */
    public SuperTurboPlugin() {

    }

    
    /**
     * Return plugin name
     */
    public String getName() {
        return "Super Turbo";

    }

    
    /*Convert playlist itme*/
    public int[] convertItem(AbstractPlayListItem _pl) throws Exception {

        /*Get CsTurboPlaylistItem*/
        CsTurboPlaylistItem pl = (CsTurboPlaylistItem) _pl;

        /*Pripraven vystupni proud pro instrukce*/
        InstructionStream is = new InstructionStream();

        /*If loader is to be prepended, then we prepend it*/
        if (cfgPrependLoader.getAsBoolean() == true) {
            is.add(LoaderBinaryData.universalTurboLoader);
        }


        /*Nyni se vybere konverze podle typu v polozce playlistu*/
        switch (pl.conversionType) {

            /*Zde se prevadi cely soubor*/
            case CsTurboPlaylistItem.CV_BINTURBO:
            case CsTurboPlaylistItem.CV_BASIC:
            case CsTurboPlaylistItem.CV_DATA: {
                Utils.addSetupInstruction(is, pl.pulses, casChunkName, SignalGenerator.FLAG_POLARITY_10, SignalGenerator.FLAG_ORDER_HL);
                convertFullFile(is, pl);
                break;
            }
            /*Monoliticky binarni soubor*/
            case CsTurboPlaylistItem.CV_MONOLITHIC: {
                Utils.addSetupInstruction(is, pl.pulses, casChunkName, SignalGenerator.FLAG_POLARITY_10, SignalGenerator.FLAG_ORDER_HL);
                convertMonolithicBinary(is, pl);
                break;
            }
            /*Specialni retezove zavadeni*/
            case CsTurboPlaylistItem.CV_SEGMENTED_CHAIN: {
                convertChainLoading(is, pl);
                break;
            }
            /*Specialni blokove zavadeni*/
            case CsTurboPlaylistItem.CV_SEGMENTED_BLOCK: {
                convertBlockLoading(is, pl);
                break;
            }

        }

        is.add(SignalGenerator.INSTR_END);

        /*Instrukce pro generatory*/
        return is.getInstructions();

    }

    public AbstractPlayListItem getItemFromPanel() {
        CsTurboPlaylistItem dpr = panel.getRecord();
        dpr.pluginClass = Turgen.pluginManager.getPluginClassName(this);
        return dpr;
    }

    public void resetPanel() {
        panel.clear();
    }

    public String validateItemOnPanel() {
        return panel.verify();
    }

    public JPanel getItemPanel() {
        return panel;
    }

    public void init() {
        panel = new CsTurboPluginsPanel(true);

        /*Initialize configuration class*/
        cfcSuperTurbo = new ConfigClass("SuperTurbo", "Super Turbo");

        /*Initialize configuration entries*/
        cfgPilotLenA = new IntegerConfigEntry("pilotLenA", "Header block pilot tone length [pulse]", 3072, 256, 8192);
        cfgPilotLenB = new IntegerConfigEntry("pilotLenB", "Data block pilot tone length [pulse]", 3072, 256, 8192);
        cfgProlongatePilotTone = new BooleanConfigEntry("prolongatePilotTone", "Prolongate pilot tone", true);
        cfgSilenceAfterHeader = new IntegerConfigEntry("silenceAfterHeader", "Silence after header [s]", 0, 0, 30);
        cfgPilotLenBlockLoading = new IntegerConfigEntry("pilotLenBlockLoading", "Pilot tone length for BlockLoading [pulse]", 2048, 256, 8192);
        cfgProlongateBlockLoadingPilotTone = new BooleanConfigEntry("prolongateBlockLoadingPilotTone", "Prolongate pilot tone for BlockLoading", true);
        cfgConvertBinaryLoadersToTurbo2000 = new BooleanConfigEntry("convertBinaryLoadersToTurbo2000", "Convert binary loaders to Turbo 2000", false);
        cfgPrependLoader = new BooleanConfigEntry("prependLoader", "Prepend Universal Turbo loader", false);

        /*Add entries to the class*/
        cfcSuperTurbo.addEntry(cfgPilotLenA);
        cfcSuperTurbo.addEntry(cfgPilotLenB);
        cfcSuperTurbo.addEntry(cfgPilotLenBlockLoading);
        cfcSuperTurbo.addEntry(cfgProlongatePilotTone);
        cfcSuperTurbo.addEntry(cfgSilenceAfterHeader);
        cfcSuperTurbo.addEntry(cfgProlongateBlockLoadingPilotTone);
        cfcSuperTurbo.addEntry(cfgConvertBinaryLoadersToTurbo2000);
        cfcSuperTurbo.addEntry(cfgPrependLoader);

        /*Register configuration class*/
        Configuration.registerClass(cfcSuperTurbo);
    }

    public void fillPanel(AbstractPlayListItem plr) {
        CsTurboPlaylistItem dpr = (CsTurboPlaylistItem) plr;
        panel.fill(dpr);
    }

    public void setInOutProvider(InOutProvider prov) {
        panel.setIOProvider(prov);
    }

    private void convertFullFile(InstructionStream is, CsTurboPlaylistItem cpi) throws Exception {

        int plenA = cfgPilotLenA.getAsInt();
        int plenB = cfgPilotLenA.getAsInt();

        if (cfgProlongatePilotTone.getAsBoolean() == true) {
            plenA = PulseCreator.prolongatePilotTone(plenA, 32, cpi.pulses.pilotTonePulseWidth);
            plenB = PulseCreator.prolongatePilotTone(plenB, 32, cpi.pulses.pilotTonePulseWidth);
        }

        int hdrLoad = Integer.parseInt(cpi.load);
        int hdrRun = Integer.parseInt(cpi.run);
        int hdrLength = Integer.parseInt(cpi.length);
        int hdrType = Integer.parseInt(cpi.type);

        /*Header*/
        int[] hdr = Utils.createSTHeader(cpi.fname, hdrType, hdrLoad, hdrLength, hdrRun);
        addBlock(is, hdr, -1, true, plenA);
        addSilenceAfterHeader(is);

        /*Data*/
        addBlock(is, readFileData(cpi.inFile, 0, 512 * 1024), 237, false, plenB);
    }

    public void convertMonolithicBinaryUsingData(InstructionStream is, int[] data, String title, boolean addSetupInstruction, PulseCrate pulses) throws Exception {

        /* Add setup instruction - for callers that may require it*/
        if (addSetupInstruction == true) {
            if (pulses == null) {
                pulses = Turgen.pulseCache.getPulsesForPlugin(this)[0];
            }
            Utils.addSetupInstruction(is, pulses, casChunkName, SignalGenerator.FLAG_POLARITY_10, SignalGenerator.FLAG_ORDER_HL);
        }

        /* Get DOS 2 binary file*/
        DOS2Binary dtb = new DOS2Binary("");
        dtb.analyzeFromData(data);

        /* Check if the file is monolithic*/
        if (dtb.isMonolithic() == false) {
            throw new DOS2BinaryProcessingException("Input binary file is not a monolithic binary file");
        }

        convertMonolithicBinaryImpl(
                is,
                dtb,
                title,
                dtb.getFirstDataSegment().start,
                dtb.getCanonicalRunAddress(),
                dtb.getFirstDataSegment().data.length,
                3,
                Turgen.pulseCache.getPulsesForPlugin(this)[0]);
    }

    private void convertMonolithicBinary(InstructionStream is, CsTurboPlaylistItem cpi) throws Exception {

        DOS2Binary dtb = new DOS2Binary(cpi.inFile);
        dtb.analyzeFromFile();

        /* Check if the file is monolithic*/
        if (dtb.isMonolithic() == false) {
            throw new DOS2BinaryProcessingException("Input binary file is not a monolithic binary file");
        }

        convertMonolithicBinaryImpl(
                is,
                dtb,
                cpi.fname,
                Integer.parseInt(cpi.load),
                Integer.parseInt(cpi.run),
                Integer.parseInt(cpi.length),
                Integer.parseInt(cpi.type),
                cpi.pulses
        );
    }

    private void convertMonolithicBinaryImpl(InstructionStream is, DOS2Binary dtb, String hdrFileName, int hdrLoad, int hdrRun, int hdrLength, int hdrType, PulseCrate pulses) throws Exception {

        /* Check if the file is monolithic*/
        if (dtb.isMonolithic() == false) {
            throw new DOS2BinaryProcessingException("Input file is not a monolithic binary file");
        }

        int plenA = cfgPilotLenA.getAsInt();
        int plenB = cfgPilotLenA.getAsInt();
        if (cfgProlongatePilotTone.getAsBoolean() == true) {
            plenA = PulseCreator.prolongatePilotTone(plenA, 32, pulses.pilotTonePulseWidth);
            plenB = PulseCreator.prolongatePilotTone(plenB, 32, pulses.pilotTonePulseWidth);
        }

        /*Header*/
        int[] hdr = Utils.createSTHeader(hdrFileName, hdrType, hdrLoad, hdrLength, hdrRun);
        addBlock(is, hdr, -1, true, plenA);
        addSilenceAfterHeader(is);

        /*Data*/
        int[] fileData = dtb.getFirstDataSegment().data;
        addBlock(is, fileData, 237, false, plenB);

    }

    private void convertChainLoading(InstructionStream is, CsTurboPlaylistItem cpi) throws Exception {

        int currentPause = 0;
        int numPauses = cpi.silenceList.length;

        /*Create DOS 2 Binary and analyze it*/
        DOS2Binary dtb = new DOS2Binary(cpi.inFile);
        dtb.analyzeFromFile();

        /*Pulses Used*/
        int currentPilotLenA = cfgPilotLenA.getAsInt();
        int currentPilotLenB = cfgPilotLenB.getAsInt();

        if (cfgProlongatePilotTone.getAsBoolean() == true) {
            currentPilotLenA = PulseCreator.prolongatePilotTone(currentPilotLenA, 32, cpi.pulses.pilotTonePulseWidth);
            currentPilotLenB = PulseCreator.prolongatePilotTone(currentPilotLenB, 32, cpi.pulses.pilotTonePulseWidth);
        }

        /*Convert loader*/
        if (chainLoader2Data == null) {
            chainLoader2Data = Utils.getLoaderFromResource("/turgen/plugin/csturbo/loaders/superchain2.xex");
        }

        /*Chainloader to Super Turbo, or Turbo 2000*/
        if (cfgConvertBinaryLoadersToTurbo2000.getAsBoolean() == false) {
            Utils.addSetupInstruction(is, cpi.pulses, casChunkName, SignalGenerator.FLAG_POLARITY_10, SignalGenerator.FLAG_ORDER_HL);
            if ((cpi.specialFlags & CsTurboPlaylistItem.SF_NO_BINARY_LOADER)==0) {
                convertMonolithicBinaryUsingData(is, chainLoader2Data, cpi.fname, false, cpi.pulses);
            }
        }
        else {
            /*Check for turbo 2000 plugin*/
            Turbo2000Plugin t2kPlugin = (Turbo2000Plugin) Turgen.pluginManager.getPlugin("turgen.plugin.csturbo.Turbo2000Plugin");
            if (t2kPlugin == null) {
                throw new PluginException("The Turbo 2000 plugin required to convert the binary loader is not present");
            }
            if ((cpi.specialFlags & CsTurboPlaylistItem.SF_NO_BINARY_LOADER)==0) {
                t2kPlugin.convertMonolithicBinaryUsingData(is, chainLoader2Data, cpi.fname, true);
            }
            Utils.addSetupInstruction(is, cpi.pulses, casChunkName, SignalGenerator.FLAG_POLARITY_10, SignalGenerator.FLAG_ORDER_HL);
        }

        /*Certain silence after loader*/
        is.add(SignalGenerator.INSTR_LONGSILENCE);
        is.add(2);

        /*Convert segments of the binary file*/
        Segment seg;
        Iterator<Segment> it = dtb.allSegments.iterator();

        int[] headerData = new int[4];

        while (it.hasNext()) {
            seg = it.next();

            /*Convert segment header*/
            headerData[0] = seg.start % 256;
            headerData[1] = seg.start / 256;
            headerData[2] = seg.stop % 256;
            headerData[3] = seg.stop / 256;
            addBlock(is, headerData, 183, false, currentPilotLenA);

            /*Convert segment data*/
            addBlock(is, seg.data, 237, false, currentPilotLenB);

            /*Handle silence*/
            if (currentPause < numPauses) {
                if (seg.type == Segment.INIT_SEGMENT || seg.type == Segment.RUNINIT_SEGMENT) {
                    is.add(SignalGenerator.INSTR_LONGSILENCE);
                    is.add(cpi.silenceList[currentPause]);
                    currentPause++;
                }
            }
        }

        /*Convert termination header*/
        for (int i = 0; i < headerData.length; i++) {
            headerData[i] = 255;
        }
        addBlock(is, headerData, 183, false, currentPilotLenA);

    }

    /**
     * Convert binary file - blockloading
     */
    private void convertBlockLoading(InstructionStream is, CsTurboPlaylistItem cpi) throws Exception {

        int currentPause = 0;
        int numPauses = cpi.silenceList.length;


        /*Get DOS 2 Binary and analyze it*/
        DOS2Binary dtb = new DOS2Binary(cpi.inFile);
        dtb.analyzeFromFile();
        dtb.prepareForDumbLoader();

        /*Create copy of the loader skeleton*/
        /*Convert loader*/
        if (blockLoaderData == null) {
            blockLoaderData = Utils.getLoaderFromResource("/turgen/plugin/csturbo/loaders/superblock.xex");
        }

        int[] loaderData = new int[blockLoaderData.length];
        System.arraycopy(blockLoaderData, 0, loaderData, 0, blockLoaderData.length);

        /*Update the loader skeleton with segment information*/
        int sl = dtb.allSegments.size();
        if (sl > 62) {
            throw new DOS2BinaryProcessingException("Blockloader cannot process binary file with more than 62 segments");
        }

        Segment s;
        int index = BLOCKLOADER_TABLE_OFFSET;
        loaderData[BLOCKLOADER_COUNTER_OFFSET] = sl * 4;

        for (int i = 0; i < sl; i++) {
            s = (Segment) dtb.allSegments.get(i);
            loaderData[index] = (s.start) % 256;
            loaderData[index + 1] = (s.start) / 256;
            loaderData[index + 2] = (s.stop + 1) % 256;
            loaderData[index + 3] = (s.stop + 1) / 256;
            index += 4;
        }

        /*BlockLoader to Super Turbo, or Turbo 2000*/
        if (cfgConvertBinaryLoadersToTurbo2000.getAsBoolean() == false) {
            Utils.addSetupInstruction(is, cpi.pulses, casChunkName, SignalGenerator.FLAG_POLARITY_10, SignalGenerator.FLAG_ORDER_HL);
            if ((cpi.specialFlags & CsTurboPlaylistItem.SF_NO_BINARY_LOADER)==0) {
                convertMonolithicBinaryUsingData(is, loaderData, cpi.fname, false, cpi.pulses);
            }
        }
        else {
            /*Check for turbo 2000 plugin*/
            Turbo2000Plugin t2kPlugin = (Turbo2000Plugin) Turgen.pluginManager.getPlugin("turgen.plugin.csturbo.Turbo2000Plugin");
            if (t2kPlugin == null) {
                throw new PluginException("The Turbo 2000 plugin required to convert the binary loader is not present");
            }
            if ((cpi.specialFlags & CsTurboPlaylistItem.SF_NO_BINARY_LOADER)==0) { 
                t2kPlugin.convertMonolithicBinaryUsingData(is, loaderData, cpi.fname, true);
            }
            Utils.addSetupInstruction(is, cpi.pulses, casChunkName, SignalGenerator.FLAG_POLARITY_10, SignalGenerator.FLAG_ORDER_HL);
        }

        /*Certain silence after loader*/
        is.add(SignalGenerator.INSTR_LONGSILENCE);
        is.add(2);

        /*Convert segments to super turbo blocks*/
        int pilotTonePulses = cfgPilotLenBlockLoading.getAsInt();
        if (cfgProlongatePilotTone.getAsBoolean() == true) {
            pilotTonePulses = PulseCreator.prolongatePilotTone(pilotTonePulses, 32, cpi.pulses.pilotTonePulseWidth);
        }

        Iterator<Segment> it = dtb.allSegments.iterator();
        while (it.hasNext()) {

            /*Convert segment*/
            Segment sct = it.next();
            addBlock(is, sct.data, 183, false, pilotTonePulses);

            /*Silence after INIT segments*/
            if (sct.type == Segment.INIT_SEGMENT || sct.type == Segment.RUNINIT_SEGMENT) {
                if (currentPause < numPauses) {
                    is.add(SignalGenerator.INSTR_LONGSILENCE);
                    is.add(cpi.silenceList[currentPause]);
                    currentPause++;
                }
            }

        }

    }

    private int[] readFileData(String file, int skipBytes, int maxLen) throws Exception {

        byte[] b;
        RandomAccessFile raf = new RandomAccessFile(file, "r");

        int flen = (int) raf.length();
        if (flen > maxLen) {
            raf.close();
            throw new FileTooLargeException("File exceeds " + Integer.toString(maxLen) + " bytes");
        }

        b = new byte[flen - skipBytes];
        raf.skipBytes(skipBytes);
        raf.readFully(b);
        raf.close();


        /*Konverze na int*/
        int[] fileData = new int[b.length];
        for (int i = 0; i < b.length; i++) {
            fileData[i] = b[i] < 0 ? b[i] + 256 : b[i];
        }

        return fileData;
    }

    public String[] getSupportedTapeImageChunks() {
        return new String[]{"trst"};
    }

    private void addBlock(InstructionStream is, int[] data, int idByte, boolean isFull, int pilotTonePulses) {

        /*Pilot tone and sync pulse*/
        is.add(SignalGenerator.INSTR_PILOTTONE);
        is.add(pilotTonePulses);
        is.add(SignalGenerator.INSTR_SYNC);

        /*A full block is just a full block*/
        if (isFull == true) {
            is.add(SignalGenerator.INSTR_DATA);
            is.add(data.length);
            is.add(data);
        }
        /*From raw data, we create noble super turbo blocks*/
        else {
            int chsum = idByte;
            is.add(SignalGenerator.INSTR_DATA);
            is.add(data.length + 2);
            is.add(idByte);
            is.add(data);
            for (int i = 0; i < data.length; i++) {
                chsum = chsum ^ data[i];
            }
            is.add(chsum);

        }

        /*Safety pulse + safety pause*/
        is.add(SignalGenerator.INSTR_NARROW);
        is.add(SignalGenerator.INSTR_BLOCKSEP);
    }

    private void addSilenceAfterHeader(InstructionStream is) {
        int silenceAfterHeader = cfgSilenceAfterHeader.getAsInt();
        if (silenceAfterHeader != 0) {
            is.add(SignalGenerator.INSTR_LONGSILENCE);
            is.add(silenceAfterHeader);
        }
    }

    private int[] chainLoader2Data = null;
    private int[] blockLoaderData = null;
    private static final int BLOCKLOADER_TABLE_OFFSET = 0x17C + 0x0C;
    private static final int BLOCKLOADER_COUNTER_OFFSET = 0x17B + 0x0C;

    public void addInstructionsForChunk(InstructionStream is, TapeImageChunk chunk) throws Exception {

        /*trXX chunk*/
        if (chunk instanceof TurboChunk) {
            int bdr = ((TurboChunk) (chunk)).getBaudRate();

            /*Upkeep baud rate range*/
            if (bdr < 2270) {
                bdr = 2270;
            }
            if (bdr > 9600) {
                bdr = 9600;
            }

            /*Choose closest known pulses*/
            PulseCrate speed = Utils.getNearestBaudRate(bdr, Turgen.pulseCache.getPulsesForPlugin(this));

            /*Generate setup instruction*/
            Utils.addSetupInstruction(is, speed, chunk.getType(), SignalGenerator.FLAG_POLARITY_10, SignalGenerator.FLAG_ORDER_HL);

            return;
        }

        /*Only data chunk accepted*/
        if (!(chunk instanceof DataChunk)) {
            return;
        }

        DataChunk dataChunk = (DataChunk) chunk;
        addBlock(is, dataChunk.getData(), -1, true, dataChunk.getAux());

    }

    public BinaryConvertor[] getBinaryConvertors() {
        BinaryConvertor[] bc = new BinaryConvertor[3];
        MonolithicBinaryConvertor mbc = new MonolithicBinaryConvertor(true, this);
        ChainloadingBinaryConvertor cbc = new ChainloadingBinaryConvertor(true, 1803, 2205, this);
        BlockloadingBinaryConvertor bbc = new BlockloadingBinaryConvertor(true, 2048, 2682, this);
        bc[0] = mbc;
        bc[1] = cbc;
        bc[2] = bbc;
        return bc;
    }

    public String getCountry() {
        return "CS";
    }

    public boolean isUsingAlternateFileChooser() {
        return false;
    }

}
