package turgen.dtb;

import java.io.*;
import java.util.*;
import turgen.Utils;

/** DOS 2 Binary file.*/
public class DOS2Binary {
    
    /**DATA segments*/
    private final ArrayList dataSegments;
    /**JUMP segments*/
    private final ArrayList jumpSegments;
    /**File name*/
    private final String filename;
    /**Length of extra code*/
    private int extraLen;
    /**Length of file*/
    private long fileLength;
    
    /**All segments*/
    public ArrayList allSegments;
    
    /**Indicates that file has been analyzed*/
    private boolean isAnalyzed;

    /**Offsets of the INIT segments. 
     */
    
    private int[] initLocations;
    
    /** Create new instance
     * @param _filename File name
     */
    public DOS2Binary(String _filename) {
        filename=_filename;
        dataSegments = new ArrayList();
        jumpSegments= new ArrayList();
        allSegments=new ArrayList();
        isAnalyzed=false;
    }

    
    /**Delka pridavneho kodu
    * @return Delka specialniho kodu v bajtech*/
    public int getExtraLength() {
        return extraLen;
    }
    
    
    /**Vrati seznam segmentu jako pole retezcu
     * @return Pole retezcu*/
    
    public String[] getListing() {
        
        /*Pole retezcu*/
        String[] s=new String[allSegments.size()];
        
        for(int i=0;i<allSegments.size();i++) {
            s[i]=((Segment)(allSegments.get(i))).toString();
            
        }
        
        return s;
    }
    
    public void analyzeFromFile() throws IOException, DOS2BinaryException {
        
        /*Kontrola velikosti*/
        File f = new File(filename);
        if (f.exists() && f.isFile()) {
            long l = f.length();
            if (l>1024*1024) throw new DOS2BinaryException(filename,"Binary file is too long. File size exceeds 1024 KB.",0);
            fileLength=l;
        }
        
        RandomAccessFile raf;
        raf=new RandomAccessFile(filename,"r");
        
        /*Nacteni vsech dat ze souboru*/
        byte[] filebData=new byte[(int)raf.length()];
        raf.readFully(filebData);
        raf.close();
        
        /*Konverze z pole bajtu na pole intu. Skoda ze Java nema neznamenkove
         *typy,ale neda se nic delat*/
        int[] fileData = Utils.getAsIntArray(filebData);
        
        analyze(fileData);
        
    }

    public void analyzeFromData(int[] fileData) throws IOException, DOS2BinaryException {
        analyze(fileData);
    }


    /** Analyza souboru. Zjistuje se rozlozeni a typ segmentu
     * @throws IOException,DOS2BinaryException
     */
    private void analyze(int[] fileData) throws IOException,DOS2BinaryException {
        

        Vector v = new Vector();
        
        int pos=0;
        int size=fileData.length;
        int b1,b2;
        int w1,w2,w3,w4;
        
        /*Zahajeni analyzy*/
        /*Prvni dva bajty musi byt 255*/
        if (fileData[0]!=255 || fileData[1]!=255) {
            throw new DOS2BinaryException(filename,"Binary file header not found. First two bytes do not have values of 255 [0xFF]",0);
        }
        
        pos=2;
        
        /*Segment musi mit alespon 3 byty*/
        while(size-pos>3) {
            
            
            int lastSegPos=pos;
            
            try {

                /*Je zde 255 255 ?*/
                b1 = fileData[pos];
                b2 = fileData[pos + 1];

                /*Jestlize ano, tak se inkrementuje pozice, jinak nic*/
                if (b1 == 255 && b2 == 255) {
                    pos += 2;
                }

                /*pos ukazuje na prvni bajt segmentu - zde je jeji velikost*/
                b1 = fileData[pos];
                pos++;
                b2 = fileData[pos];
                pos++;
                w1 = b2 * 256 + b1;   //w1 obsahuje  pocatecni adresu segmentu
                b1 = fileData[pos];
                pos++;
                b2 = fileData[pos];
                pos++;
                w2 = b2 * 256 + b1;   //w2 obsahuje koncovou adresu segmentu

                w3 = w2 - w1 + 1;     //w3 obsahuje velikost segmentu
                
                if (w3<1) {
                    throw new DOS2BinaryException(filename,getNegativeSegmentSizeMessage(w1, w2),lastSegPos);
                }


                /*Je to init ?*/
                if (w3 == 2 && w1 == 738 && w2 == 739) {
                    /*Nacti dva bajty, ukazuji kam se ma skocit na init*/
                    b1 = fileData[pos];
                    pos++;
                    b2 = fileData[pos];
                    pos++;
                    v.add(new Integer(pos - 1));
                    w4 = b2 * 256 + b1;
                    Segment ns = new Segment(w1, w2, Segment.INIT_SEGMENT, w4);
                    this.jumpSegments.add(ns);
                    this.allSegments.add(ns);
                    continue;
                }

                /*Je to run ?*/
                if (w3 == 2 && w1 == 736 && w2 == 737) {
                    /*Nacti dva bajty, ukazuji kam se ma skocit na init*/
                    b1 = fileData[pos];
                    pos++;
                    b2 = fileData[pos];
                    pos++;
                    w4 = b2 * 256 + b1;
                    Segment ns = new Segment(w1, w2, Segment.RUN_SEGMENT, w4);
                    this.jumpSegments.add(ns);
                    this.allSegments.add(ns);
                    continue;
                }

                /*Je to oboji segment- jak init, tak run*/
                if (w3 == 4 && w1 == 736 && w2 == 739) {
                    /*Nacti dva bajty, ukazuji kam se ma skocit na init*/
                    b1 = fileData[pos];
                    pos++;
                    b2 = fileData[pos];
                    pos++;
                    w4 = b2 * 256 + b1;
                    Segment ns = new Segment(w1, w2, Segment.RUN_SEGMENT, w4);
                    int bkp = w4;

                    b1 = fileData[pos];
                    pos++;
                    b2 = fileData[pos];
                    pos++;
                    v.add(new Integer(pos - 1));
                    w4 = b2 * 256 + b1;
                    Segment nx = new Segment(w1, w2, Segment.INIT_SEGMENT, w4);
                    /*Nejprve INIT, potom az RUN*/
                    this.jumpSegments.add(nx);
                    this.jumpSegments.add(ns);
                    Segment ens = new Segment(w1, w2, Segment.RUNINIT_SEGMENT, bkp, w4);
                    this.allSegments.add(ens);

                    continue;
                }

                /*Je to obycejny segment, udela se zaznam do pole a pokracuje se*/
                Segment s;
                s = new Segment(w1, w2, Segment.DATA_SEGMENT);
                this.dataSegments.add(s);
                this.allSegments.add(s);
                for (int i = 0; i < w3; i++) {
                    s.data[i] = fileData[pos + i];
                }
                pos += w3;

            } catch (ArrayIndexOutOfBoundsException ae) {
                throw new DOS2BinaryException(filename,"Segment continues beyond end of binary file",pos);
            }


        }/*Konec while*/

        /*Spocte se delka pridavneho kodu, na kazdy segment JSR (32,lo,hi)*/
        this.extraLen = (3 * jumpSegments.size());
        this.initLocations = new int[v.size()];

        for (int k = 0; k < v.size(); k++) {
            int ps = ((Integer) (v.elementAt(k))).intValue();
            initLocations[k] = ps;
        }
        isAnalyzed=true;
        
    }
    
    /** Vylistuje segmenty souboru na standardni vystup*/
    void listToConsole() {
        for(int i=0;i<allSegments.size();i++) {
            System.out.println(allSegments.get(i).toString());
        }
    }
    
    
    
    /** Merge segments (create monolithic binary file
    * @param outname Output file
    * @param extraAdress Address of the jump segment emulation code
    * @param extra Generate jump segment emulation code
    * @param auto Automatically determine address of the jump segment emulation code
     * @throws turgen.dtb.DOS2BinaryProcessingException
    * @throws IOException,NumberFormatException
     */
    public void merge(String outname,String extraAdress,boolean extra,boolean auto) throws DOS2BinaryProcessingException, IOException,NumberFormatException {
        
        /*Ziskame cs pole datovych segmentu*/
        Object[] o = this.dataSegments.toArray();
        
        if (o.length==0) {
           throw new DOS2BinaryProcessingException("Unable to merge segments. Binary file has no DATA segments.");
        }
        
        Segment[] cs = new Segment[o.length];
        for (int i=0;i<o.length;i++) {
            cs[i]=(Segment)o[i];
        }
        
        
        /*Pole o velikosti 64KB pro data vystupniho souboru*/
        /*Co nebude potreba, to se jednoduse orizne*/
        int[] fileData= new int[65536];
        
        /*Vyresetovat pole*/
        for(int i=0;i<65536;i++) {
            fileData[i]=-1;
        }
        
        int pos=0;
        
        /*Kopirovani dat segmentu do pole*/
        for(int i=0;i<cs.length;i++) {
            /*Kdyz segment je mimo rozsah pameti, tak vyhodit vyjimku*/
            if (cs[i].start<0 || cs[i].start>65535 || cs[i].stop<0 || cs[i].start>65535) {
                throw new DOS2BinaryProcessingException("Internal processing error. Segment does not fit into 0-65535.");
            }
            /*Kopirovat data*/
            int strt=cs[i].start;
            int ln = cs[i].data.length;
            for (int k=0;k<ln;k++) {
                if (fileData[strt+k]!=-1) throw new DOS2BinaryProcessingException("Unable to merge segments. Segments of the binary file overlap.");
                fileData[strt+k]=cs[i].data[k];
            }
            
        }
        
        /*Adresa spusteni*/
        int runAdr=-1;
        /*Adresa pridavneho kodu*/
        int ea=Integer.parseInt(extraAdress);
        
        /*RUTINY JSR*/
        if(extra==true) {
            pos=ea;
            int j;  /*Pomocna*/
            for(int i=0;i<jumpSegments.size();i++) {
                Segment scn = (Segment)(jumpSegments.get(i));
                switch(scn.type) {
                    case(Segment.INIT_SEGMENT): {
                        fileData[pos]=32;pos++; /*JSR*/
                        j=scn.jump;
                        fileData[pos]=(j % 256);pos++;
                        fileData[pos]=(j / 256);pos++;
                        break;
                    }
                    case (Segment.RUN_SEGMENT): { 
                        runAdr=scn.jump;
                        break;
                    }
                    case (Segment.RUNINIT_SEGMENT): {
                        runAdr=scn.jump;
                        j=scn.jump2;
                        fileData[pos]=32;pos++; /*JSR*/
                        fileData[pos]=(j%256);pos++;
                        fileData[pos]=(j/256);pos++;
                        break;
                    }
                    
                }/*switch*/
                
            }
            /*Run at last*/
            if (runAdr>0) { /*JMP*/
                fileData[pos]=76;pos++;
                fileData[pos]=(runAdr%256);pos++;
                fileData[pos]=(runAdr/256);pos++;
            }
            
        }
        /*Only browse for possible run segments*/
        else {
             for(int i=0;i<jumpSegments.size();i++) {
                Segment scn = (Segment)(jumpSegments.get(i));
                switch(scn.type) {
                    case(Segment.RUN_SEGMENT): {
                        runAdr=scn.jump;
                        break;
                    }
                    case (Segment.RUNINIT_SEGMENT): {
                        runAdr=scn.jump;
                        break;
                    }
                }
             }
        }
        
        /*Tady se urci prvni a posledni adresa ziskana z datovych segmentu a
         *pripadne take z extra kodu*/
        
        int fa=-1;
        int la=-1;
        
        for (int i=0;i<65536;i++) {
            if (fileData[i]!=-1) {
                fa=i;
                break;
            }
        }
        
        for (int i=65535;i>-1;i--) {
            if (fileData[i]!=-1) {
                la=i;
                break;
            }
        }
        
        if (fa==-1 || la==-1) throw new DOS2BinaryProcessingException("Unable to merge segments. Internal error when determining lowest and highest address.");
        
        /*Nedotcene adresy v intervalu fa,la se musi vynulovat*/
        for(int i=fa;i<la;i++) {
            if (fileData[i]==-1) fileData[i]=0;
        }
        
        
        /*Zapis do souboru*/
        RandomAccessFile raf;
        
        raf=new RandomAccessFile(outname,"rw");
        raf.setLength(0);
        
        /*Hlavicka*/
        raf.writeByte(255);
        raf.writeByte(255);
        raf.writeByte(fa % 256);
        raf.writeByte(fa / 256);
        raf.writeByte(la % 256);
        raf.writeByte(la /256);
        
        /*Sloucena data*/
        for (int i=fa;i<=la;i++) {
            raf.writeByte(fileData[i]);
        }
        
        
        /*Segment Run, bud skace na extracode nebo na detekovanou run, nebo
          na zacatek prvniho segmentu*/
        raf.writeByte(736 % 256);
        raf.writeByte(736 / 256);
        raf.writeByte(737 % 256);
        raf.writeByte(737 / 256);
        
        int ija=0;
        if (extra==true) ija=ea;
        if (extra==false && runAdr>0) ija=runAdr;
        if (extra==false && runAdr<0) ija=fa;
        
        raf.writeByte(ija % 256);
        raf.writeByte(ija / 256);
        
        raf.close();
        
        
        
    }
    
    /** Pocet datovych segmentu
     *@return Pocet segmentu
     */
    public int getDataSegmentCount() {
        return dataSegments.size();
    }
    
    /** Pocet RUN/INIT segmentu
     *@return Pocet segmentu
     */
    public int getJumpSegmentCount() {
        return jumpSegments.size();
    }
    
    /** Vrat prvni datovy segment
     *@return Prvni datovy segment
     */
    
    public Segment getFirstDataSegment() {
        if (dataSegments.size()<1) return null;
        return (Segment)dataSegments.get(0);
    }
    
    /** Vrat prvni RUN/INIT segment
     *@return Prvni RUN/INT segment
     */
    public Segment getFirstJumpSegment() {
        if (jumpSegments.size()<1) return null;
        return (Segment)jumpSegments.get(0);
    }
    
    public int[] getInitLocations() {
        return initLocations;
    }
    
    /** Purify the binary file. 
     *  Split RUNINIT segments
     *  Move RUN segments to the end of the binary file
     */
    public void purify() {
        
        /*Nejprve najit rozdelit vsechny RUNINIT segmenty*/
        int l=this.allSegments.size();
        Segment s;
        for(int i=0;i<l;i++) {
            s=(Segment)this.allSegments.get(i);
            if (s.type==Segment.RUNINIT_SEGMENT) {
                s.type=Segment.RUN_SEGMENT;
                Segment si = new Segment(738,739,Segment.INIT_SEGMENT,s.jump2);
                this.allSegments.add(i+1,si);
                s.jump2=0;
            }
        }
        
        /*Presunout vsechny RUN segmenty na konec*/
        ArrayList al = new ArrayList();
        for(int i=0;i<allSegments.size();i++) {
            s=(Segment)this.allSegments.get(i);
            if (s.type==Segment.RUN_SEGMENT) {
                this.allSegments.remove(i);
                al.add(s);
                i--;
            }
        }
        l=al.size();
        for(int i=0;i<l;i++) {
            this.allSegments.add((Segment)al.get(i));
        }
    }

    /**Prepare file for dumb binary loaders that require RUN segment*/
    public void prepareForDumbLoader() {
        /*Look for RUN or RUNINIT segments*/
        int runAdr;
        int l = jumpSegments.size();

        Segment s;
        for(int i=0;i<l;i++) {
            s=(Segment)jumpSegments.get(i);
            if (s.type==Segment.RUN_SEGMENT || s.type==Segment.RUNINIT_SEGMENT) return;
        }
        /*Append new RUN segment if possible*/
        if (dataSegments.size()>0) {
            s=(Segment)dataSegments.get(0);
            runAdr=s.start;
            Segment nrs = new Segment(736,737,Segment.RUN_SEGMENT,runAdr);
            allSegments.add(nrs);
            jumpSegments.add(nrs);
        }
    }
    
    
    
    public int[] getFileStatistics() {
        int[] retVal = new int[7];
        
        retVal[0]=(int)fileLength;      /*Velikost souboru*/
        retVal[1]=allSegments.size();   /*Celkem segmentu*/
        retVal[2]=dataSegments.size();  /*DATA segmentu*/
        retVal[3]=jumpSegments.size();  /*Skokovych segmentu*/
        retVal[4]=0;                    /*V segmentech <1KB*/
        retVal[5]=0;                    /*V segmentech <1 az 3 KB>*/
        retVal[6]=0;                    /*V ostatnich segmentech*/
        int l = this.dataSegments.size();
        int b=0;
        Segment s;
        for(int i=0;i<l;i++) {
            s= (Segment)dataSegments.get(i);
            b=s.data.length;
            if (b<=1024) {retVal[4]+=b;}
            if (b>1024 && b<=3072) {retVal[5]+=b;}
            if (b>3072) {retVal[6]+=b;}
        }
        return retVal;
    }

    /** Test whether some data segment covers defined memory area. Useful
     *  to test compatibility with some binary loaders
     * @param firstAdr First address of tested memory area
     * @param lastAdr Last address of tested memory area
     * @return true if some segment covers defined memory area
     */
    public boolean coversMemory(int firstAdr,int lastAdr) {
        
        int l = dataSegments.size();
        Segment s;
        boolean b;
        for(int i=0;i<l;i++) {
            b=true;
            s=(Segment)dataSegments.get(i);
            if ( (s.start < firstAdr) && (s.stop<firstAdr) ) b=false;
            if ( (s.start > lastAdr)  && (s.stop>lastAdr) ) b=false;
            if (b==true) return b;
        }

        return false;
    }


    /** Test whether file contain at least one INIT segment
     *
     * @return true When file contains at least one INIT segment
     */
    public boolean hasInitSegment() {
        int l = jumpSegments.size();
        Segment s;
        for (int i=0;i<l;i++) {
            s=(Segment)jumpSegments.get(i);
            if (s.type==Segment.INIT_SEGMENT || s.type==Segment.RUNINIT_SEGMENT) return true;
        }
        return false;
    }

    /** Test whether file contain at least one RUN segment
     *
     * @return true When file contains at least one RUN segment
     */
    public boolean hasRunSegment() {
        int l = jumpSegments.size();
        Segment s;
        for (int i=0;i<l;i++) {
            s=(Segment)jumpSegments.get(i);
            if (s.type==Segment.RUN_SEGMENT || s.type==Segment.RUNINIT_SEGMENT) return true;
        }
        return false;
    }



    public String getFileName() {
        return filename;
    }

    public boolean isMonolithic() {

        if (getDataSegmentCount()==1 && getJumpSegmentCount()==1 && getFirstJumpSegment().type==Segment.RUN_SEGMENT) return true;
        if (getDataSegmentCount()==1 && getJumpSegmentCount()==0) return true;

        return false;
    }

    public boolean isPlTurboROMCompatible() {
        
        /*One data segment and nothing*/
        if (getDataSegmentCount()==1 && getJumpSegmentCount()==0) return true;

        /*One data segment + one jump segment of any type*/
        if (getDataSegmentCount()==1 && getJumpSegmentCount()==1) return true;

        /*One data segment + one INIT and one RUN segment*/
        if (getDataSegmentCount()==1 && getSegmentCount(Segment.RUN_SEGMENT)==1 && getSegmentCount(Segment.INIT_SEGMENT)==1) return true;


        return false;
    }

    public int getTotalSegmentCount() {
        return allSegments.size();
    }

    /**Return number of segments of specific type
     *
     * @param type Type of segment
     * @return Number of segments
     */
    public int getSegmentCount(int type) {

        int count=0;
        Segment s;
        Iterator it = jumpSegments.iterator();

        while(it.hasNext()) {
            s=(Segment)it.next();
            if (s.type==type) count++;
        }

        return count;

    }
    
    /** Get canonical RUN address 
     *  If the binary file has a RUN segments, it is the address specified 
     *  by the RUN segment. Otherwise it is the address of first data segment.
     * @return 
     */
    public int getCanonicalRunAddress() {

        int runAddr=-1;
        Segment s;

        Iterator it = allSegments.iterator();
        while(it.hasNext()) {

            s=(Segment)it.next();
            if (s.type==Segment.RUN_SEGMENT) {
                return s.jump;
            }
            if (s.type==Segment.DATA_SEGMENT && runAddr==-1) {
                runAddr=s.start;
            }
        }

        return runAddr;
    }
    
    public boolean isIsAnalyzed() {
        return isAnalyzed;
    }
    
    private String getNegativeSegmentSizeMessage(int w1,int w2) {
     
        StringBuilder sb = new StringBuilder();
        sb.append("Segment with negative size found ");
        sb.append('(');
        sb.append(Utils.padZeros(Integer.toString(w1),5));
        sb.append('-');
        sb.append(Utils.padZeros(Integer.toString(w2),5));
        sb.append(" [");
        sb.append(Utils.padZeros(Integer.toHexString(w1),4).toUpperCase());
        sb.append('-');
        sb.append(Utils.padZeros(Integer.toHexString(w2),4).toUpperCase());
        sb.append("])");
        
        return sb.toString();
        
    }

}
