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.playlist.AbstractPlayListItem;
import turgen.tapeimage.*;

public class Turbo2000Plugin implements Plugin {

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

    private ConfigClass cfcTurbo2000;
    private IntegerConfigEntry cfgPilotLenA;
    private IntegerConfigEntry cfgPilotLenB;
    private IntegerConfigEntry cfgSilenceAfterHeader;
    private BooleanConfigEntry cfgPauseAfterHeader;
    private IntegerConfigEntry cfgPilotLenBlockLoading;
    private BooleanConfigEntry cfgPrependLoader;
    

    /**
     * Nova instance SuperTurboPlugin
     */
    public Turbo2000Plugin() {

    }

    /**
     * Identifikacni jmeno modulu
     */
    public String getName() {
        return "Turbo 2000";

    }

    /**
     * **********************************************************
     */
    /*Tvorba instrukci pro generator*/
    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);
        }


        /*Nastaveni, pro vsechny mozne konverze je nastaveni stejne,
         *bloky nebo soubory jsou vzdy v Super Turbu o dane rychlosti
         */
        Utils.addSetupInstruction(is, pl.pulses, casChunkName, SignalGenerator.FLAG_POLARITY_10, SignalGenerator.FLAG_ORDER_HL);

        /*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: {
                convertFullFile(is, pl);
                break;
            }
            /*Monoliticky binarni soubor*/
            case CsTurboPlaylistItem.CV_MONOLITHIC: {
                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(false);

        /*Initialize configuration class*/
        cfcTurbo2000 = new ConfigClass("Turbo2000", "Turbo 2000");

        /*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);
        cfgSilenceAfterHeader = new IntegerConfigEntry("silenceAfterHeader","Silence after header [s]",0,0,30);
        cfgPauseAfterHeader = new BooleanConfigEntry("pauseAfterHeader","Pause after header",false);
        cfgPilotLenBlockLoading = new IntegerConfigEntry("pilotLenBlockLoading", "Pilot tone length for BlockLoading [pulse]", 2048, 256, 8192);
        cfgPrependLoader = new BooleanConfigEntry("prependLoader", "Prepend Universal Turbo loader", false);

        /*Add entries to the class*/
        cfcTurbo2000.addEntry(cfgPilotLenA);
        cfcTurbo2000.addEntry(cfgPilotLenB);
        cfcTurbo2000.addEntry(cfgSilenceAfterHeader);
        cfcTurbo2000.addEntry(cfgPauseAfterHeader);
        cfcTurbo2000.addEntry(cfgPilotLenBlockLoading);
        cfcTurbo2000.addEntry(cfgPrependLoader);

        /*Register configuration class*/
        Configuration.registerClass(cfcTurbo2000);

    }

    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();

        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.createT2KHeader(cpi.fname, hdrType, hdrLoad, hdrLength, hdrRun);
        addBlock(is,hdr,-1,true,plenA);
        addSilenceAfterHeader(is);
        
        /*Data*/
        addBlock(is,readFileData(cpi.inFile, 0, 512*1024),255,false,plenB);
    }
    
    public void convertMonolithicBinaryUsingData(InstructionStream is,int[] data,String title,boolean addSetupInstruction) throws Exception {
        
        /* Add setup instruction - for callers that may require it*/
        if (addSetupInstruction==true) {
            Utils.addSetupInstruction(is, Turgen.pulseCache.getPulsesForPlugin(this)[0], 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);
    }
    
    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)
                );
    }

    private void convertMonolithicBinaryImpl(InstructionStream is,DOS2Binary dtb, String hdrFileName,int hdrLoad,int hdrRun,int hdrLength, int hdrType) 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 = cfgPilotLenB.getAsInt();

        /*Header*/
        int[] hdr = Utils.createT2KHeader(hdrFileName, hdrType, hdrLoad, hdrLength, hdrRun);
        addBlock(is,hdr,-1,true,plenA);
        addSilenceAfterHeader(is);
        
        /*Data*/
        int[] fileData = dtb.getFirstDataSegment().data;
        addBlock(is,fileData,255,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();
        
        /*Convert loader*/
        if (chainLoader2Data==null) {
            chainLoader2Data=Utils.getLoaderFromResource("/turgen/plugin/csturbo/loaders/chainloader2.xex");
        }
        convertMonolithicBinaryUsingData(is, chainLoader2Data,cpi.fname,false);
        
        /*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,0,false,currentPilotLenA);
            
            /*Convert segment data*/
            addBlock(is,seg.data,255,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,0,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*/
        if (blockLoaderData==null) {
            blockLoaderData=Utils.getLoaderFromResource("/turgen/plugin/csturbo/loaders/blockloader.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;
        }
        
        convertMonolithicBinaryUsingData(is, loaderData, cpi.fname,false);
        
        /*Certain silence after loader*/
        is.add(SignalGenerator.INSTR_LONGSILENCE);
        is.add(2);
        
        /*Convert segments to turbo 2000 blocks*/
        int pilotTonePulses=cfgPilotLenBlockLoading.getAsInt();
        
        Iterator<Segment> it = dtb.allSegments.iterator();
        while (it.hasNext()) {

            /*Convert segment*/
            Segment sct = it.next();
            addBlock(is,sct.data,0,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[]{"tr2k"};
    }

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

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

        /*trXX chunk*/
        if (chunk instanceof TurboChunk) {
            Utils.addSetupInstruction(is, Turgen.pulseCache.getPulsesForPlugin(this)[0], this.casChunkName, 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());

    }
    
    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 turbo 2000 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);
            if (cfgPauseAfterHeader.getAsBoolean()==true) silenceAfterHeader=-silenceAfterHeader;
            is.add(silenceAfterHeader);
        }
        else {
            if (cfgPauseAfterHeader.getAsBoolean()==true) {
                is.add(SignalGenerator.INSTR_PAUSE);
            }
        }
    }
    

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

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

    public boolean isUsingAlternateFileChooser() {
        return false;
    }

    

}
