package turgen.tapeimageextractor;
import java.io.BufferedOutputStream;
import java.io.DataOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Vector;
import javax.swing.table.AbstractTableModel;
import turgen.FileFormatException;
import turgen.Utils;
import turgen.decoder.FileDataCache;
import turgen.dtb.DOS2Binary;
import turgen.dtb.DOS2BinaryException;
import turgen.tapeimage.FujiChunk;
import turgen.tapeimage.TapeImage;


public class TapeImageModel extends AbstractTableModel {

    private TapeImage tapeImage;
    private Vector<ExtractorChunk> extractorChunks;
    private static final int CI_CHUNKID=0;
    private static final int CI_CHUNKNAME=1;
    private static final int CI_IRG=2;
    private static final int CI_LENGTH=3;
    private static final int CI_HEAD=4;
    private static final int CI_TAIL=5;
    private static final int CI_HINT=6;
    private static final String unknownData = "?";

    
    private static class BootDataExtractionCrate {
        int[] data;
        int[] trailingChunkData;

        public BootDataExtractionCrate() {
            data=null;
            trailingChunkData=null;
        }
        
    }
    
    public TapeImageModel() {
        createEmptyModel();
    }
    
    public void clear() {
        createEmptyModel();
        fireTableDataChanged();
    }
    
    public void openTapeImage(String filespec, boolean append) throws Exception {
        /*Clear extractor chunks*/
        if (append==false) extractorChunks.clear();
        fireTableDataChanged();
        
        /*Open tape image*/
        tapeImage = new TapeImage();
        tapeImage.parse(filespec);
        
        /*Create extractor chunks*/
        int numChunks = tapeImage.getChunkCount();
        for(int i=0;i<numChunks;i++) {
            extractorChunks.add(new ExtractorChunk(tapeImage.getChunkAt(i),i));
        }
        fireTableDataChanged();
        
    }
    
    public int getRowCount() {
        return extractorChunks.size();
    }

    public int getColumnCount() {
        return colNames.length;
    }

    public Object getValueAt(int rowIndex, int columnIndex) {
        ExtractorChunk chunk = extractorChunks.elementAt(rowIndex);
        
        if (chunk==null) return unknownData;
        
        switch(columnIndex) {
            
            case CI_CHUNKID: {
                return chunk.getId();
            }
            case CI_CHUNKNAME: {
                return chunk.getChunkName();
            }
            case CI_IRG: {
                return chunk.getIRGString();
            }
            case CI_LENGTH: {
                return chunk.getLengthString();
            }
            case CI_HEAD: {
                return chunk.getHeadString();
            }
            case CI_TAIL: {
                return chunk.getTailString();
            }
            case CI_HINT: {
                return chunk.getHintString();
            }
            default: {
                return unknownData;
            }
                
        }
    }
    
    private final String[] colNames = {"#","Chunk","IRG","Length","Head","Tail","Hint"};
    
    public String getColumnName(int index) {
        return colNames[index];
    }
    
    public Class getColumnClass(int index) {
         return String.class;
     }
    
    private void createEmptyModel() {
        tapeImage = new TapeImage();
        extractorChunks = new Vector<ExtractorChunk>();
    }

    ExtractorChunk elementAt(int i) {
        return extractorChunks.elementAt(i);
    }

    void updateExtractionHints(int[] indexes, HintsCrate crate,HintsCrate.Map map) {
        
        for(int i=0;i<indexes.length;i++) {
            int index = indexes[i];
            ExtractorChunk c = extractorChunks.elementAt(index);
            c.updateHintsCrate(crate,map);
        }
        
        fireTableDataChanged();
    }
    
    ExtractionReportCrate extractChunks(int[] indexes,String filespec,ExtractionReporter rep) throws Exception {
        
        ExtractionReportCrate reportCrate = new ExtractionReportCrate();
        ExtractorChunk.ExtractionCrate eCrate;
        
        FileOutputStream fos = new FileOutputStream(filespec);
        BufferedOutputStream bos = new BufferedOutputStream(fos);
        
        
        
        for(int i=0;i<indexes.length;i++) {
            ExtractorChunk chunk = elementAt(indexes[i]);
            reportCrate.totalChunks++;
            
            String id = Utils.padZeros(chunk.getId(), 4);
            
            /*We skip chunks that cannot be extracted*/
            if (chunk.isExtractible()==false) {
                reportCrate.skippedChunks++;
                rep.addMessage(id+":Not extractible");
                continue;
            }
            
            /*Extraction of one chunk*/
            eCrate = chunk.extractData();
            if (eCrate.completionCode!=ExtractorChunk.CC_OK) {
                switch (eCrate.completionCode) {
                    case ExtractorChunk.CC_BAD_HINT: {
                        rep.addMessage(id+":Bad hints");
                        reportCrate.badHintsChunks++;
                        break;
                    }
                    case ExtractorChunk.CC_BAD_LENGTH: {
                        rep.addMessage(id+":Bad length");
                        reportCrate.badHintsChunks++;
                        break;
                    }
                }
                
            }
            
            for(int k=0;k<eCrate.data.length;k++) {
                bos.write(eCrate.data[k]);
            }
            
        }
        
        /*Close output file*/
        bos.flush();
        bos.close();
        
        /*Report*/
        return reportCrate;
        
    }

    void resetHints(int[] indexes) {
        for(int i=0;i<indexes.length;i++) {
            int index = indexes[i];
            ExtractorChunk c = extractorChunks.elementAt(index);
            c.resetHints();
        }
        fireTableDataChanged();
    }
    
    /** Check if selected chunks can represent a boot file
     * 
     * @param indexes Indexes of chunks selected
     * @return true When selected chunks appear to contain a boot file
     */
    boolean isSelectionBootFile(int[] indexes) {
        
        try {
            getSelectionBootData(indexes,false);
        }
        catch (FileFormatException ffe) {
            return false;
        }
        return true;
        
    }
    
    boolean isSelectionBootFileWithTrailingRecord(int[] selectedIndexes) {
        try {
            getSelectionBootData(selectedIndexes,true);
        }
        catch (FileFormatException ffe) {
            return false;
        }
        return true;
    }
    
    private BootDataExtractionCrate getSelectionBootData(int[] indexes,boolean withTrailingRecord) throws FileFormatException {
        
        BootDataExtractionCrate returnCrate = new BootDataExtractionCrate();
        
        
        final String EXCP_PFX="Not a boot file. ";
                
        /*At least one chunk selected?*/
        if (indexes.length<1) {
            throw new FileFormatException(EXCP_PFX+"No chunks.");
        }

        /*Get first chunk*/
        ExtractorChunk firstChunk = elementAt(indexes[0]);
        if (firstChunk.getRawLength()!=132) {
            throw new FileFormatException(EXCP_PFX+"First record is not a standard record.");
        }
        
        FileDataCache cache = new FileDataCache();
        
        /*Prepare crate and get data*/
        ExtractorChunk.ExtractionCrate eCrate;
        eCrate = firstChunk.extractData();
        
        /*If not extracted OK, bail out*/
        if (eCrate.completionCode!=ExtractorChunk.CC_OK) {
            throw new FileFormatException(EXCP_PFX+"Data of first chunk extracted with errors.");
        }
        
        /*Is there enough data*/
        if (eCrate.data.length<1) {
            throw new FileFormatException(EXCP_PFX+"First record is not a full or a partial record.");
        }
        
        /*Determine total boot records*/
        int totalBootRecords = eCrate.data[1];
        if (totalBootRecords==0) totalBootRecords=256;
        int numRecords=1;
        cache.add(eCrate.data);
        
        /*Data of the last chunk*/
        int[] trailingChunkData=null;
        
        for(int i=1;i<indexes.length;i++) {
            
            ExtractorChunk chunk = elementAt(indexes[i]);
            if (chunk.getRawLength()!=132) {
                throw new FileFormatException(EXCP_PFX+"Record "+chunk.getId()+" is not a standard record.");
            }
            if (chunk.isExtractible()==false) continue;
            
            eCrate = chunk.extractData();
            if (eCrate.completionCode!=ExtractorChunk.CC_OK) {
                throw new FileFormatException(EXCP_PFX+"Record "+chunk.getId()+" extracted with errors.");
            }
            numRecords++;
            
            if (withTrailingRecord==true) {
                trailingChunkData=eCrate.data;
            }
            
            cache.add(eCrate.data);
            
        }
        
        /*Check if there is appropriate number of chunks*/
        int totalRecordsCorrection = withTrailingRecord ? +1 : 0;
        
        if (numRecords==(totalBootRecords+totalRecordsCorrection)) {
            returnCrate.data=cache.getBytes();
            returnCrate.trailingChunkData=trailingChunkData;
            return returnCrate;
        }
        throw new FileFormatException(EXCP_PFX+"Total number of boot records does not match boot header. (t "+Integer.toString(numRecords)+",h "+Integer.toString(totalBootRecords)+")");
    }

    boolean isSelectionBinaryFile(int[] selectedIndexes) {
        
        int[] data = getSelectionData(selectedIndexes);
        
        DOS2Binary dtb = new DOS2Binary("");
        try {
           dtb.analyzeFromData(data);
           return true;
        }
        catch(IOException e) {
            return false;
        }
        catch(DOS2BinaryException dtbe) {
            return false;
        }
        catch(Exception genE) {
            return false;
        }
    }
    
    private int[] getSelectionData(int[] selectedIndexes) {
        
        FileDataCache cache = new FileDataCache();
        ExtractorChunk chunk;
        ExtractorChunk.ExtractionCrate eCrate;
        
        for (int i=0;i<selectedIndexes.length;i++) {
            chunk = elementAt(selectedIndexes[i]);
            if (chunk.isExtractible()==false) continue;
            eCrate = chunk.extractData();
            cache.add(eCrate.data);
        }
        
        return cache.getBytes();
    }
    
    void extractBinaryFile(int[] selectedIndexes,String filespec) throws Exception {
        
        /*Get data from selected chunks*/
        int[] data = getSelectionData(selectedIndexes);
        
        /*Check if really binary file. If not, an exception is thrown*/
        DOS2Binary dtb = new DOS2Binary("");
        dtb.analyzeFromData(data);
        
        /*Store data to a real file*/
        FileOutputStream fos = new FileOutputStream(filespec);
        BufferedOutputStream bos = new BufferedOutputStream(fos);
        
        for(int i=0;i<data.length;i++) {
            bos.write(data[i]);
        }
        
        /*Close output stream*/
        bos.flush();
        bos.close();
    }
    
    void extractBootFile(int[] selectedIndexes,String filespec,boolean withTrailingRecord) throws Exception {
        
        /* Let us extract boot data*/
        BootDataExtractionCrate bootCrate = getSelectionBootData(selectedIndexes,withTrailingRecord);
        int[] rawBootData = bootCrate.data;
        int[] bootData;
        int[] trailingData = bootCrate.trailingChunkData;
        boolean useTrailingData = (withTrailingRecord && trailingData!=null && trailingData.length!=0);
        
        if (useTrailingData) {
            bootData = new int[rawBootData.length-trailingData.length];
            System.arraycopy(rawBootData, 0, bootData, 0, bootData.length);
        }
        else {
            bootData = rawBootData;
        }
        
        if (bootData==null || bootData.length<6) {
            throw new FileFormatException("Selected chunks do not contain a boot file");
        }
        
        if (bootData.length<6) {
            throw new FileFormatException("Selected chunks do not contain a boot file");
        }
        
        /*Store data to a real file*/
        FileOutputStream fos = new FileOutputStream(filespec);
        BufferedOutputStream bos = new BufferedOutputStream(fos);
        
        /*Write binary file header*/
        bos.write(255);
        bos.write(255);
        
        /*First segment - header*/
        bos.write(bootData[2]);
        bos.write(bootData[3]);
        int firstAddr=bootData[2]+256*bootData[3];
        int lastAddr=(firstAddr)+bootData.length-1;
        bos.write(lastAddr % 256);
        bos.write(lastAddr / 256);
        
        /*First segment - data*/
        for(int i=0;i<bootData.length;i++) {
            bos.write(bootData[i]);
        }
        
        /*Second segment with trailing data*/
        if (useTrailingData==true) {
            /* Second segment - header*/
            bos.write(0);
            bos.write(4);
            bos.write((1024+trailingData.length-1)%256);
            bos.write((1024+trailingData.length-1)/256);
            /*Second segment - data*/
            for(int i=0;i<trailingData.length;i++) {
                bos.write(trailingData[i]);
            }
        }
        
        /*INIT segment - header*/
        bos.write(0xE2);
        bos.write(0x02);
        bos.write(0xE3);
        bos.write(0x02);
        /*INIT segment - data*/
        bos.write((firstAddr+6) % 256);
        bos.write((firstAddr+6) / 256);
        
        /*RUN segment - header*/
        bos.write(0xE0);
        bos.write(0x02);
        bos.write(0xE1);
        bos.write(0x02);
        /*RUN segment - data */
        bos.write(bootData[4]);
        bos.write(bootData[5]);
        
        bos.flush();
        bos.close();
    }
    
    void extractPlainDataFile(int[] selectedIndexes,String filespec,int loadAddr,int runAddr) throws Exception {
        
        int[] data = getSelectionData(selectedIndexes);
        
        /*Store data to a real file*/
        FileOutputStream fos = new FileOutputStream(filespec);
        BufferedOutputStream bos = new BufferedOutputStream(fos);
        
        /*Write binary file header*/
        bos.write(255);
        bos.write(255);
        
        /*Data segment - header*/
        bos.write(loadAddr % 256);
        bos.write(loadAddr / 256);
        int lastAddr = loadAddr+data.length-1;
        bos.write(lastAddr % 256);
        bos.write(lastAddr / 256);
        
        /*Data segment - data*/
        for(int i=0;i<data.length;i++) {
            bos.write(data[i]);
        }
        if (runAddr!=-1) {
            /*RUN segment - header*/
            bos.write(0xE0);
            bos.write(0x02);
            bos.write(0xE1);
            bos.write(0x02);
            /*RUN segment - data */
            bos.write(runAddr % 256);
            bos.write(runAddr / 256);
        }
        
        bos.flush();
        bos.close();
        
    }
    
    void saveSelectedChunks(String absolutePath, int[] indexes) throws Exception {
        
        /*Don't do anything if there are no chunks*/
        if (indexes.length==0) return;
        
        FileOutputStream fos = new FileOutputStream(absolutePath);
        DataOutputStream dos = new DataOutputStream(fos);
        
        /*Probe the first chunk.If not FUJI, add artificial one*/
        ExtractorChunk eChunk = extractorChunks.elementAt(indexes[0]);
        if (!(eChunk.getTapeImageChunk() instanceof FujiChunk)) {
            FujiChunk.getDefaultFujiChunk().writeToStream(dos);
        }
        for (int i=0;i<indexes.length;i++) {
            eChunk = extractorChunks.elementAt(indexes[i]);
            eChunk.getTapeImageChunk().writeToStream(dos);
        }
        
        dos.flush();
        dos.close();
    }

    
    
   


}
