package turgen.plugin.standard;

import java.io.RandomAccessFile;
import java.util.ArrayList;
import java.util.Iterator;
import javax.swing.JPanel;
import turgen.Turgen;
import turgen.Utils;
import turgen.config.ConfigClass;
import turgen.config.Configuration;
import turgen.config.IntegerConfigEntry;
import turgen.dtb.DOS2Binary;
import turgen.dtb.DOS2BinaryProcessingException;
import turgen.dtb.Segment;
import turgen.generator.SignalGenerator;
import turgen.playlist.AbstractPlayListItem;
import turgen.plugin.BinaryConvertor;
import turgen.plugin.FileTooLargeException;
import turgen.plugin.InOutProvider;
import turgen.plugin.InstructionStream;
import turgen.plugin.Plugin;
import turgen.tapeimage.TapeImageChunk;

/**
 * Plugin converts files to standard tape records
  */

public class StandardPlugin implements Plugin {

    private StandardPluginPanel panel;
    private static final int BLOCK_COMPLETE = 0xFC;
    private static final int BLOCK_PARTIAL = 0xFA;
    private static final int BLOCK_EOF = 0xFE;

    private ConfigClass cfcStandard;
    private IntegerConfigEntry cfgLeaderDuration;
    private IntegerConfigEntry cfgLeaderAfterInitSegmentDuration;
    private IntegerConfigEntry cfgBinaryLoaderIRGDuration;

    public String getName() {
        return "Standard";
    }

    public int[] convertItem(AbstractPlayListItem plr) throws Exception {
        /*Get Standard playlist item*/
        StandardPlaylistItem pl = (StandardPlaylistItem) plr;

        RandomAccessFile raf = null;

        /*Ready stream for instructions*/
        InstructionStream is = new InstructionStream();

        switch (pl.inputType) {
            case (StandardPlaylistItem.IT_BASIC_SOURCE): {
                convertPlainData(pl, is, false);
                break;
            }
            case (StandardPlaylistItem.IT_BASIC_TOKENIZED):
            case (StandardPlaylistItem.IT_DATA): {
                convertPlainData(pl, is, pl.shortIRG);
                break;
            }
            case (StandardPlaylistItem.IT_MONOLITHIC_BINARY): {
                convertMonolithicBinary(pl, is);
                break;
            }
            case (StandardPlaylistItem.IT_BINARY): {
                convertSegmentedBinary(pl, is);
                break;
            }
        }

        /*Terminate*/
        is.add(SignalGenerator.INSTR_END);
        return is.getInstructions();

    }

    public void init() {
        panel = new StandardPluginPanel(this);
        /*Initialize configuration class*/
        cfcStandard = new ConfigClass("Standard", "Standard");

        /*Initialize configuration entries*/
        cfgLeaderDuration = new IntegerConfigEntry("leaderDuration", "Leader duration [s]", 20, 10, 30);
        cfgBinaryLoaderIRGDuration = new IntegerConfigEntry("leaderForBinaryLoaderDuration", "Binary loader IRG duration [ms]", 375, 250, 500);
        cfgLeaderAfterInitSegmentDuration = new IntegerConfigEntry("leaderAfterInitDuration", "IRG after INIT segment duration [s]", 12, 8, 30);

        /*Add entries to the class*/
        cfcStandard.addEntry(cfgLeaderDuration);
        cfcStandard.addEntry(cfgBinaryLoaderIRGDuration);
        cfcStandard.addEntry(cfgLeaderAfterInitSegmentDuration);

        /*Register class*/
        Configuration.registerClass(cfcStandard);
    }

    public JPanel getItemPanel() {
        return panel;
    }

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

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

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

    public void fillPanel(AbstractPlayListItem plr) {
        StandardPlaylistItem item = (StandardPlaylistItem) plr;
        panel.fill(item);
    }

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

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

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

    }

    public BinaryConvertor[] getBinaryConvertors() {
        BinaryConvertor[] convertors = new BinaryConvertor[2];
        convertors[0] = new StandardMonolithicBinaryConvertor(this);
        convertors[1] = new StandardBinaryFileConvertor(this);
        return convertors;
    }

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

    public boolean isUsingAlternateFileChooser() {
        return false;
    }

    /**
     * Convert general data block to the standard tape image records
     */
    private void convertData(int[] data, int irgMiliseconds, int leaderMiliseconds, InstructionStream is, boolean noEOFBlock) throws Exception {

        /*If there is no data, we bail out quickly*/
        if (data.length == 0) {
            return;
        }

        boolean firstBlock = true;
        int dataIndex = 0;
        int securityCounter = 0;
        int lastBlockType = -1;

        /*First we write all full blocks if there are any*/
        blockLoop:
        while (securityCounter < 65536) {

            securityCounter++;

            if (noEOFBlock == true && lastBlockType == BLOCK_PARTIAL) {
                break;
            }

            /*Instruction*/
            is.add(SignalGenerator.INSTR_STDDATA);

            /*IRG duration*/
            if (firstBlock == true) {
                firstBlock = false;
                is.add(leaderMiliseconds);
            }
            else {
                is.add(irgMiliseconds);
            }

            /*Data length*/
            is.add(132);

            /*Sync*/
            is.add(0x55);
            is.add(0x55);

            int chsum = 0x55 + 0x55;

            int validBytes = data.length - dataIndex;
            int blockType = BLOCK_COMPLETE;

            /*EOF, partial or full block?*/
            if (validBytes >= 128) {
                blockType = BLOCK_COMPLETE;
            }
            if (validBytes > 0 && validBytes < 128) {
                blockType = BLOCK_PARTIAL;
            }
            if (validBytes == 0) {
                blockType = BLOCK_EOF;
            }

            is.add(blockType);
            chsum += blockType;

            switch (blockType) {
                case BLOCK_EOF: {

                    for (int k = 0; k < 128; k++) {
                        is.add(0);
                    }
                    is.add(getFinalizedCheckSum(chsum));
                    break blockLoop;
                }
                case BLOCK_COMPLETE: {

                    for (int k = 0; k < 128; k++) {
                        is.add(data[dataIndex]);
                        chsum = chsum + data[dataIndex];
                        dataIndex++;
                    }
                    lastBlockType = BLOCK_COMPLETE;

                    break;
                }
                case BLOCK_PARTIAL: {
                    for (int k = 0; k < validBytes; k++) {
                        is.add(data[dataIndex]);
                        chsum = chsum + data[dataIndex];
                        dataIndex++;
                    }
                    for (int k = 0; k < (128 - validBytes - 1); k++) {
                        is.add(0);
                    }
                    is.add(validBytes);
                    chsum += validBytes;
                    lastBlockType = BLOCK_PARTIAL;
                    break;
                }

            }
            is.add(getFinalizedCheckSum(chsum));
        }

    }

    /**
     * Convert plain data
     */
    private void convertPlainData(StandardPlaylistItem pl, InstructionStream is, boolean shortIRG) throws Exception {

        int[] data = readFileData(pl.inFile, 0, 512 * 1024);

        /*Standard, 600 bd*/
        is.add(SignalGenerator.INSTR_BAUD);
        is.add(600);

        int irgMilis = shortIRG == true ? 300 : 3000;
        convertData(data, irgMilis, cfgLeaderDuration.getAsInt() * 1000, is, false);

    }

    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;
    }

    /**
     * Calculates finalized check sum
     *
     * @param checksum Raw check sum
     * @return Finalized check sum - handles carry
     */
    private int getFinalizedCheckSum(int checksum) {

        /*How many times there was a carry. Data block length is 132 bytes,
         so we can safely assume that number of carries is lower than 255.
         */
        int numCarries = (checksum >> 8);
        checksum = (checksum & 0xFF) + numCarries;

        /*There can be one last carry*/
        if (checksum > 255) {
            checksum = (checksum + 1) & 0xFF;
        }
        return checksum;

    }

    /**
     * Check binary file
     *
     * @param Filespec File specifier
     * @param dtb Binary file. If null, then file specifier is used
     * @return Error message or null when everything is OK
     * @throws Exception
     */
    String verifyMonolithicBinary(String Filespec, DOS2Binary dtb) throws Exception {

        DOS2Binary binary;
        Segment dataSegment;

        if (dtb == null) {
            binary = new DOS2Binary(Filespec);
        }
        else {
            binary = dtb;
        }

        if (binary.isIsAnalyzed() == false) {
            binary.analyzeFromFile();
        }

        dataSegment = binary.getFirstDataSegment();

        if (binary.isMonolithic() == false || dataSegment == null) {
            return "Input file is not a monolithic binary file. To convert the binary file, set the input file type to \"Binary file\".";
        }
        if (dataSegment.start < 1536) {
            return "Monolithic binary file occupies addresses lower than 1536. To convert the binary file, set the input file type to \"Binary file\".";
        }
        if (dataSegment.data.length + Math.max(bootHeaderDown.length, bootHeaderUp.length) > 32768) {
            return "Resulting boot file would exceed size of 32 KB. To convert the binary file, set the input file type to \"Binary file\".";
        }

        return null;
    }

    private void convertMonolithicBinary(StandardPlaylistItem pl, InstructionStream is) throws Exception {
        DOS2Binary dtb = new DOS2Binary(pl.inFile);
        dtb.analyzeFromFile();
        convertMonolithicBinaryImpl(dtb, is);
    }

    /**
     * Convert monolithic binary file to a standard cassette boot file This
     * method is intended to be used by other plugins
     *
     * @param data File data
     * @param is Instruction stream
     * @throws Exception
     */
    public void convertMonolithicBinaryUsingData(int[] data, InstructionStream is) throws Exception {
        DOS2Binary dtb = new DOS2Binary("");
        dtb.analyzeFromData(data);
        convertMonolithicBinaryImpl(dtb, is);
    }

    private void convertMonolithicBinaryImpl(DOS2Binary dtb, InstructionStream is) throws Exception {

        Segment dataSegment = dtb.getFirstDataSegment();

        String msg = verifyMonolithicBinary(null, dtb);
        if (msg != null) {
            throw new DOS2BinaryProcessingException(msg);
        }

        /*Calculate total length and boot header length*/
        int[] mainData = dataSegment.data;
        int[] finalData;

        int bootHeaderLength = Math.max(bootHeaderUp.length, bootHeaderDown.length);
        finalData = new int[bootHeaderLength + mainData.length];

        /*Check if program is to be moved up or down*/
        boolean moveUp = true;
        if (dataSegment.start < BASE_ADDRESS + bootHeaderLength) {
            moveUp = false;
        }

        /*Calculate number of blocks*/
        int numBlocks = finalData.length / 128;
        int trailingBytes = finalData.length % 128;
        if (trailingBytes != 0) {
            numBlocks++;
        }

        /*Clear the array*/
        for (int i = 0; i < finalData.length; i++) {
            finalData[i] = 0;
        }

        /*Copy boot header routine to the final array*/
        if (moveUp == true) {
            System.arraycopy(bootHeaderUp, 0, finalData, 0, bootHeaderUp.length);
        }
        else {
            System.arraycopy(bootHeaderDown, 0, finalData, 0, bootHeaderDown.length);
        }

        /*Copy main data to the final array*/
        System.arraycopy(mainData, 0, finalData, bootHeaderLength, mainData.length);

        /*Update the boot header*/
        modifyBootHeader(finalData, numBlocks, dataSegment.start, dtb.getCanonicalRunAddress(), moveUp, bootHeaderLength, dataSegment.data.length);

        /*Standard, 600 bd*/
        is.add(SignalGenerator.INSTR_BAUD);
        is.add(600);

        convertData(finalData, 250, cfgLeaderDuration.getAsInt() * 1000, is, false);

    }

    private void modifyBootHeader(int[] finalData, int numBlocks, int loadAddr, int runAddr, boolean moveUp, int headerLength, int dataLength) {

        /*Place number of blocks*/
        finalData[1] = numBlocks & 0xFF;

        /*Place run address*/
        finalData[0x0B + 1] = runAddr % 256;
        finalData[0x12 + 1] = runAddr / 256;

        /*Modify move routine*/
        int fromAddr = BASE_ADDRESS + headerLength;
        int toAddr = loadAddr;

        /*If moving up, addresses are end addresses increased by 1*/
        if (moveUp == true) {
            fromAddr += dataLength;
            toAddr += dataLength;
        }

        /*Place length*/
        finalData[MOVERTN_OFFSET + 0] = dataLength % 256;
        finalData[MOVERTN_OFFSET + 1] = dataLength / 256;

        /*Modify from and to addresses*/
        finalData[MOVERTN_OFFSET + 0x02 + 1] = fromAddr % 256;
        finalData[MOVERTN_OFFSET + 0x06 + 1] = fromAddr / 256;
        finalData[MOVERTN_OFFSET + 0x0A + 1] = toAddr % 256;
        finalData[MOVERTN_OFFSET + 0x0E + 1] = toAddr / 256;

    }

    private static final int BASE_ADDRESS = 6144;
    private static final int MOVERTN_OFFSET = 0x34;

    private static final int[] bootHeaderUp = {
        0x00, 0x00, 0x00, 0x18, 0x33, 0x18, 0xa9, 0x3c,
        0x8d, 0x02, 0xd3, 0xa9, 0x00, 0x8d, 0xe0, 0x02,
        0x85, 0x0a, 0xa9, 0x00, 0x8d, 0xe1, 0x02, 0x85,
        0x0b, 0xa2, 0xff, 0x9a, 0xa9, 0x00, 0x8d, 0x44,
        0x02, 0xa9, 0x01, 0x85, 0x09, 0xa2, 0x43, 0xbd,
        0x33, 0x18, 0x9d, 0xff, 0x03, 0xca, 0xd0, 0xf7,
        0x4c, 0x02, 0x04, 0x60, 0x00, 0x00, 0xa9, 0x00,
        0x85, 0x32, 0xa9, 0x00, 0x85, 0x33, 0xa9, 0x00,
        0x85, 0x34, 0xa9, 0x00, 0x85, 0x35, 0xa0, 0xff,
        0xae, 0x01, 0x04, 0xf0, 0x13, 0xc6, 0x33, 0xc6,
        0x35, 0xb1, 0x32, 0x91, 0x34, 0x88, 0xd0, 0xf9,
        0xb1, 0x32, 0x91, 0x34, 0x88, 0xca, 0xd0, 0xed,
        0xae, 0x00, 0x04, 0xf0, 0x0c, 0xc6, 0x33, 0xc6,
        0x35, 0xb1, 0x32, 0x91, 0x34, 0x88, 0xca, 0xd0,
        0xf8, 0x6c, 0xe0, 0x02, 0x6c, 0xe0, 0x02

    };

    private static final int[] bootHeaderDown = {
        0x00, 0x00, 0x00, 0x18, 0x33, 0x18, 0xa9, 0x3c,
        0x8d, 0x02, 0xd3, 0xa9, 0x00, 0x8d, 0xe0, 0x02,
        0x85, 0x0a, 0xa9, 0x00, 0x8d, 0xe1, 0x02, 0x85,
        0x0b, 0xa2, 0xff, 0x9a, 0xa9, 0x00, 0x8d, 0x44,
        0x02, 0xa9, 0x01, 0x85, 0x09, 0xa2, 0x3a, 0xbd,
        0x33, 0x18, 0x9d, 0xff, 0x03, 0xca, 0xd0, 0xf7,
        0x4c, 0x02, 0x04, 0x60, 0x00, 0x00, 0xa9, 0x00,
        0x85, 0x32, 0xa9, 0x00, 0x85, 0x33, 0xa9, 0x00,
        0x85, 0x34, 0xa9, 0x00, 0x85, 0x35, 0xa0, 0x00,
        0xae, 0x01, 0x04, 0xf0, 0x0e, 0xb1, 0x32, 0x91,
        0x34, 0xc8, 0xd0, 0xf9, 0xe6, 0x33, 0xe6, 0x35,
        0xca, 0xd0, 0xf2, 0xae, 0x00, 0x04, 0xf0, 0x08,
        0xb1, 0x32, 0x91, 0x34, 0xc8, 0xca, 0xd0, 0xf8,
        0x6c, 0xe0, 0x02, 0x6c, 0xe0, 0x02

    };

    private void convertSegmentedBinary(StandardPlaylistItem pl, InstructionStream is) throws Exception {

        /*Standard, 600 bd*/
        is.add(SignalGenerator.INSTR_BAUD);
        is.add(600);

        int[] silenceList = pl.silenceList;
        int silenceListPos = 0;

        /*Get and analyze the binary file*/
        DOS2Binary dtb = new DOS2Binary(pl.inFile);
        dtb.analyzeFromFile();
        dtb.purify();

        /*Write loader*/
        if (binaryLoaderData==null) {
            binaryLoaderData=Utils.getLoaderFromResource("/turgen/plugin/standard/loaders/stdbload.bot");
        }
        int[] loaderData = new int[binaryLoaderData.length];
        System.arraycopy(binaryLoaderData, 0, loaderData, 0, binaryLoaderData.length);

        loaderData[binaryLoaderSegCountOffset] = dtb.getTotalSegmentCount();
        String fn = pl.fname;
        if (fn.length() > 20) {
            fn = fn.substring(0, 20);
        }

        /*Clear file name displayed by the loader*/
        for (int i = 0; i < 20; i++) {
            loaderData[binaryLoaderTitleOffset + i] = ' ';
        }

        /*Place file name*/
        for (int i = 0; i < fn.length(); i++) {
            loaderData[binaryLoaderTitleOffset + i] = fn.charAt(i);
        }
        convertData(loaderData, 300, cfgLeaderDuration.getAsInt() * 1000, is, false);

        ArrayList segments = dtb.allSegments;
        Iterator it = segments.iterator();
        Segment seg;
        ArrayList<int[]> dataPieces = new ArrayList<int[]>();

        boolean first = true;

        /*Write the blocks*/
        while (it.hasNext()) {
            seg = (Segment) it.next();
            dataPieces.add(seg.getFullData());

            /*If this is an INIT segment or last segment, we flush data as a separate file*/
            if (seg.type == Segment.INIT_SEGMENT || it.hasNext() == false) {
                Iterator<int[]> it2 = dataPieces.iterator();

                /*Calculate total size*/
                int totalBytes = 0;
                if (first == true) {
                    totalBytes = 2;
                }
                while (it2.hasNext()) {
                    totalBytes += it2.next().length;
                }

                /*Merge data*/
                int[] fileData = new int[totalBytes];
                int position = 0;
                if (first == true) {
                    position = 2;
                    fileData[0] = 255;
                    fileData[1] = 255;
                }

                it2 = dataPieces.iterator();
                while (it2.hasNext()) {
                    int[] data = it2.next();
                    System.arraycopy(data, 0, fileData, position, data.length);
                    position += data.length;
                }

                int leaderDuration = (first == true) ? cfgLeaderDuration.getAsInt() : cfgLeaderAfterInitSegmentDuration.getAsInt();
                convertData(fileData, cfgBinaryLoaderIRGDuration.getAsInt(), leaderDuration * 1000, is, false);
                first = false;
                dataPieces.clear();

                /*Write silence if some*/
                if (seg.type == Segment.INIT_SEGMENT && silenceListPos < silenceList.length) {
                    if (silenceList[silenceListPos] != 0) {
                        is.add(SignalGenerator.INSTR_LONGSILENCE);
                        is.add(silenceList[silenceListPos]);
                        silenceListPos++;
                    }
                }
            }
        }

    }

    /**
     * Convert monolithic binary in a dirty way Dirty way - no safe relocation
     *
     * @param data Data of the monolithic binary file
     * @param is Instruction Stream
     * @throws Exception
     */
    public void convertDirtyMonolithicBinaryUsingData(int[] data, InstructionStream is) throws Exception {

        /*Analyze and check the input file*/
        DOS2Binary dtb = new DOS2Binary("");
        dtb.analyzeFromData(data);
        Segment dataSegment = dtb.getFirstDataSegment();

        int[] mainData = dataSegment.data;
        int[] finalData;

        int bootHeaderLength = 6 + bootHeaderSkeleton.length;
        int totalLength = bootHeaderLength + mainData.length;

        int numBlocks = totalLength / 128;
        int trailingBytes = totalLength % 128;
        if (trailingBytes != 0) {
            numBlocks++;
        }

        finalData = new int[totalLength];

        /*Prepare header*/
        int bootStart = dataSegment.start - bootHeaderLength;
        finalData[0] = 0x00;
        finalData[1] = numBlocks & 0xFF;

        /*Boot address*/
        finalData[2] = bootStart % 256;
        finalData[3] = bootStart / 256;

        /*INIT address*/
        finalData[4] = (bootStart + 6 + 5) % 256;
        finalData[5] = (bootStart + 6 + 5) / 256;

        /*Post-boot routine*/
        System.arraycopy(bootHeaderSkeleton, 0, finalData, 6, bootHeaderSkeleton.length);

        /*Update addresses for DOSVEC*/
        finalData[6 + 6] = dtb.getCanonicalRunAddress() % 256;
        finalData[6 + 10] = dtb.getCanonicalRunAddress() / 256;

        /*Main data part*/
        System.arraycopy(mainData, 0, finalData, 6 + bootHeaderSkeleton.length, mainData.length);

        /*Standard, 600 bd*/
        is.add(SignalGenerator.INSTR_BAUD);
        is.add(600);

        convertData(finalData, 250, cfgLeaderDuration.getAsInt() * 1000, is, false);

    }

    private static final int[] bootHeaderSkeleton = {
        0xa9, 0x3c, /*LDA #60 */
        0x8d, 0x02, 0xd3, /*STA 54018 - Motor off*/
        0xa9, 0x10, /*LDA #<?*/
        0x85, 0x0a, /*STA DOSVEC*/
        0xa9, 0x20, /*LDA #>?*/
        0x85, 0x0b, /*STA DOSVEC+1*/
        0xA2, 0xFF, /*LDX 255*/
        0x9A, /*TXS*/
        0x6C, 0x0A, 0x00 /* JMP(DOSVEC) */};

    private static final int binaryLoaderTitleOffset = 0x8A6 - 0x6F2;
    private static final int binaryLoaderSegCountOffset = 0x82F - 0x6F2;
    private int[] binaryLoaderData = null;

}
