/* $RCSfile$
 * $Author: egonw $
 * $Date: 2006-03-18 15:59:33 -0600 (Sat, 18 Mar 2006) $
 * $Revision: 4652 $
 *
 * Copyright (C) 2003-2005  Miguel, Jmol Development, www.jmol.org
 *
 * Contact: hansonr@stolaf.edu
 *
 *  This library is free software; you can redistribute it and/or
 *  modify it under the terms of the GNU Lesser General Public
 *  License as published by the Free Software Foundation; either
 *  version 2.1 of the License, or (at your option) any later version.
 *
 *  This library is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 *  Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public
 *  License along with this library; if not, write to the Free Software
 *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 */
package javajs.util;


import java.io.DataInputStream;
import java.io.BufferedInputStream;


import java.util.Map;

import javajs.api.GenericZipTools;



/* a simple compound document reader. 
 * 
 * DIRECTORY STRUCTURE IS NOT REGENERATED
 * 
 * See http://sc.openoffice.org/compdocfileformat.pdf
 * 
 * random access file info: 
 * http://java.sun.com/docs/books/tutorial/essential/io/rafs.html
 * 
 * SHOOT! random access is only for applications, not applets!
 * 
 * With a bit more work, this could be set up to deliver binary files, but
 * right now I've only implemented it for string-based data. 
 * 
 */

public class CompoundDocument extends BinaryDocument{

//  RandomAccessFile file;
  CompoundDocHeader header = new CompoundDocHeader(this);
  Lst<CompoundDocDirEntry> directory = new  Lst<CompoundDocDirEntry>();
  CompoundDocDirEntry rootEntry;

  protected GenericZipTools jzt;

  int[] SAT;
  int[] SSAT;
  int sectorSize;
  int shortSectorSize;
  int nShortSectorsPerStandardSector;
  int nIntPerSector;
  int nDirEntriesperSector;

  // called by reflection
  
  public CompoundDocument(){
    super();
    this.isBigEndian = true;
  }
  
  public void setDocStream(GenericZipTools jzt, BufferedInputStream bis) {
    this.jzt = jzt;
    if (!isRandom) {
      stream = new DataInputStream(bis);
    }
    stream.mark(Integer.MAX_VALUE);
    if (!readHeader())
      return;
    getSectorAllocationTable();
    getShortSectorAllocationTable();
    getDirectoryTable();
  }

  public Lst<CompoundDocDirEntry> getDirectory() {
    return directory;
  }

  public String getDirectoryListing(String separator) {
    SB sb = new SB();
    for (int i = 0; i < directory.size(); i++) {
      CompoundDocDirEntry thisEntry = directory.get(i);
      if (!thisEntry.isEmpty)
        sb.append(separator).append(thisEntry.entryName)
        .append("\tlen=").appendI(thisEntry.lenStream)
        .append("\tSID=").appendI(thisEntry.SIDfirstSector)
        .append(thisEntry.isStandard ? "\tfileOffset="
                + getOffset(thisEntry.SIDfirstSector) : "");
    }
    return sb.toString();
  }

  public SB getAllData() {
    return getAllDataFiles(null, null);
  }

  /**
   * reads a compound document directory and saves all data in a Hashtable
   * so that the files may be organized later in a different order. Also adds
   * a #Directory_Listing entry.
   * 
   * Files are bracketed by BEGIN Directory Entry and END Directory Entry lines, 
   * similar to ZipUtil.getAllData.
   * 
   * @param prefix
   * @param binaryFileList   |-separated list of files that should be saved
   *                         as xx xx xx hex byte strings. The directory listing
   *                         is appended with ":asBinaryString"
   * @param fileData
   */
  @Override
  public void getAllDataMapped(String prefix, 
                         String binaryFileList, Map<String, String> fileData) {
    fileData.put("#Directory_Listing", getDirectoryListing("|"));
    binaryFileList = "|" + binaryFileList + "|";
    for (int i = 0; i < directory.size(); i++) {
      CompoundDocDirEntry thisEntry = directory.get(i);
      if (!thisEntry.isEmpty && thisEntry.entryType != 5) {
      String name = thisEntry.entryName;
        System.out.println("CompoundDocument file " + name);
        boolean isBinary = (binaryFileList.indexOf("|" + name + "|") >= 0);
        if (isBinary)
          name += ":asBinaryString";
        fileData.put(prefix + "/" + name, appendData(new SB(), name, thisEntry, isBinary).toString());
      }
    }
    close();
  }

  @Override
  public SB getAllDataFiles(String binaryFileList, String firstFile) {
// firstFile is now ignored
//    if (firstFile != null) {
//      for (int i = 0; i < directory.size(); i++) {
//        CompoundDocDirEntry thisEntry = directory.get(i);
//        if (thisEntry.entryName.equals(firstFile)) {
//          directory.remove(i);
//          directory.add(1, thisEntry); // after ROOT_ENTRY
//          break;
//        }
//      }
//    }
    SB data = new SB();
    data.append("Compound Document File Directory: ");
    data.append(getDirectoryListing("|"));
    data.append("\n");
    CompoundDocDirEntry thisEntry;
    binaryFileList = "|" + binaryFileList + "|";
    for (int i = 0, n = directory.size(); i < n; i++) {
      thisEntry = directory.get(i);
      //System.out.println("CompoundDocument reading " + thisEntry.entryName);
      String name = thisEntry.entryName;
      switch (thisEntry.entryType) {
      case 5: // root
        break;
      case 1: // user storage (dir)
        data.append("NEW Directory ").append(name).append("\n");            
        break;
      case 2: // user stream (file)
        if (name.endsWith(".gz"))
          name = name.substring(0, name.length() - 3);
        appendData(data, name, thisEntry, binaryFileList.indexOf("|" + thisEntry.entryName + "|") >= 0);
        break;
      }
    }
    close();
    return data;
  }

  private SB appendData(SB data, String name, CompoundDocDirEntry thisEntry,
                          boolean isBinary) {
    data.append("BEGIN Directory Entry ").append(name).append("\n");            
    data.appendSB(getEntryAsString(thisEntry, isBinary));
    data.append("\nEND Directory Entry ").append(name).append("\n");
    return data;
  }

  public SB getFileAsString(String entryName) {
    for (int i = 0; i < directory.size(); i++) {
      CompoundDocDirEntry thisEntry = directory.get(i);
      if (thisEntry.entryName.equals(entryName))
        return getEntryAsString(thisEntry, false);
    }
    return new SB();
  }

  private long getOffset(int SID) {
    return (SID + 1) * sectorSize;
  }

  private void gotoSector(int SID) {
    seek(getOffset(SID));
  }

  private boolean readHeader() {
    if (!header.readData())
      return false;
    sectorSize = 1 << header.sectorPower;
    shortSectorSize = 1 << header.shortSectorPower;
    nShortSectorsPerStandardSector = sectorSize / shortSectorSize; // e.g. 512 / 64 = 8
    nIntPerSector = sectorSize / 4; // e.g. 512 / 4 = 128
    nDirEntriesperSector = sectorSize / 128; // e.g. 512 / 128 = 4
//    System.out.println(
//          "compound document: revNum=" + header.revNumber +
//          " verNum=" + header.verNumber + " isBigEndian=" + isBigEndian +
//          " bytes per standard/short sector=" + sectorSize + "/" + shortSectorSize);
    return true;
  }

  private void getSectorAllocationTable() {
    int nSID = 0;
    int thisSID;
    SAT = new int[header.nSATsectors * nIntPerSector + 109];

    try {
      for (int i = 0; i < 109; i++) {
        thisSID = header.MSAT0[i];
        if (thisSID < 0)
          break;
        gotoSector(thisSID);
        for (int j = 0; j < nIntPerSector; j++) {
          SAT[nSID++] = readInt();
          //Logger.debug(thisSID+"."+j + "/" + (nSID - 1) + " : " + SAT[nSID - 1]);
        }
      }
      int nMaster = header.nAdditionalMATsectors;
      thisSID = header.SID_MSAT_next;
      int[] MSAT = new int[nIntPerSector];
      out: while (nMaster-- > 0 && thisSID >= 0) {
        // read a page of sector identifiers pointing to SAT sectors
        gotoSector(thisSID);
        for (int i = 0; i < nIntPerSector; i++)
          MSAT[i] = readInt();
        // read each page of SAT sector identifiers 
        // last entry is pointer to next master sector allocation table page
        for (int i = 0; i < nIntPerSector - 1; i++) {
          thisSID = MSAT[i];
          if (thisSID < 0)
            break out;
          gotoSector(thisSID);
          for (int j = nIntPerSector; --j >= 0;)
            SAT[nSID++] = readInt();
        }
        thisSID = MSAT[nIntPerSector - 1];
      }
    } catch (Exception e) {
      System.out.println(e.toString());
    }
  }

  private void getShortSectorAllocationTable() {
    int nSSID = 0;
    int thisSID = header.SID_SSAT_start;
    int nMax = header.nSSATsectors * nIntPerSector;
    SSAT = new int[nMax];
    try {
      while (thisSID > 0 && nSSID < nMax) {
        gotoSector(thisSID);
        for (int j = 0; j < nIntPerSector; j++) {
          SSAT[nSSID++] = readInt();
          //System.out.println("short: " + thisSID+"."+j+" SSID=" +(nSSID-1)+" "+SSAT[nSSID-1]);
        }
        thisSID = SAT[thisSID];
      }
    } catch (Exception e) {
      System.out.println(e.toString());
    }
  }

  private void getDirectoryTable() {
    int thisSID = header.SID_DIR_start;
    CompoundDocDirEntry thisEntry;
    rootEntry = null;
    try {
      while (thisSID > 0) {
        gotoSector(thisSID);
        for (int j = nDirEntriesperSector; --j >= 0;) {
          thisEntry = new CompoundDocDirEntry(this);
          thisEntry.readData();
          directory.addLast(thisEntry);
          if (thisEntry.entryType == 5)
            rootEntry = thisEntry;
        }
        thisSID = SAT[thisSID];
      }
    } catch (Exception e) {
      System.out.println(e.toString());
    }
//    System.out.println("CompoundDocument directory entry: \n"
//        + getDirectoryListing("\n"));
  }

  private SB getEntryAsString(CompoundDocDirEntry thisEntry, boolean asBinaryString) {
    if(thisEntry.isEmpty)
      return new SB();
    //System.out.println(thisEntry.entryName + " " + thisEntry.entryType + " " + thisEntry.lenStream + " " + thisEntry.isStandard + " " + thisEntry.SIDfirstSector);
    return (thisEntry.isStandard ? getStandardStringData(
            thisEntry.SIDfirstSector, thisEntry.lenStream, asBinaryString)
            : getShortStringData(thisEntry.SIDfirstSector, thisEntry.lenStream, asBinaryString));
  }
  private SB getStandardStringData(int thisSID, int nBytes,
                                             boolean asBinaryString) {
    SB data = new SB();
    byte[] byteBuf = new byte[sectorSize];
    ZipData gzipData = new ZipData(nBytes);
    try {
      while (thisSID > 0 && nBytes > 0) {
        gotoSector(thisSID);
        nBytes = getSectorData(data, byteBuf, sectorSize, nBytes, asBinaryString, gzipData);
        thisSID = SAT[thisSID];
      }
      if (nBytes == -9999)
        return new SB();
    } catch (Exception e) {
      System.out.println(e.toString());
    }
    if (gzipData.isEnabled)
      gzipData.addTo(jzt, data);
    return data;
  }

  private int getSectorData(SB data, byte[] byteBuf,
                            int nSectorBytes, int nBytes, 
                            boolean asBinaryString, ZipData gzipData)
      throws Exception {
    readByteArray(byteBuf, 0, byteBuf.length);
    int n = gzipData.addBytes(byteBuf, nSectorBytes, nBytes);
    if (n >= 0)
      return n;
    if (asBinaryString) {
      for (int i = 0; i < nSectorBytes; i++) {
        data.append(Integer.toHexString(byteBuf[i] & 0xFF)).appendC(' ');
        if (--nBytes < 1)
          break;
      }
    } else {
      for (int i = 0; i < nSectorBytes; i++) {
        if (byteBuf[i] == 0)
          return -9999; // don't allow binary data
        data.appendC((char) byteBuf[i]);
        if (--nBytes < 1)
          break;
      }
    }
    return nBytes;
  }

  private SB getShortStringData(int shortSID, int nBytes, boolean asBinaryString) {
    SB data = new SB();
    if (rootEntry == null)
      return data;
    int thisSID = rootEntry.SIDfirstSector;
    int ptShort = 0;
    byte[] byteBuf = new byte[shortSectorSize];
    ZipData gzipData = new ZipData(nBytes);
    try {
      //System.out.println("CD shortSID=" + shortSID);
      // point to correct short data sector, 512/64 = 4 per page
      while (thisSID >= 0 && shortSID >= 0 && nBytes > 0) {
        while (shortSID - ptShort >= nShortSectorsPerStandardSector) {
          ptShort += nShortSectorsPerStandardSector;
          thisSID = SAT[thisSID];
        }
        seek(getOffset(thisSID) + (shortSID - ptShort) * shortSectorSize);
        nBytes = getSectorData(data, byteBuf, shortSectorSize, nBytes, asBinaryString, gzipData);
        shortSID = SSAT[shortSID];
        //System.out.println("CD shortSID=" + shortSID);
      }
    } catch (Exception e) {
      System.out.println(data.toString());
      System.out.println("reader error in CompoundDocument " + e.toString());
    }
    if (gzipData.isEnabled)
      gzipData.addTo(jzt, data);
    return data;
  }  
}
