/*
 * Copyright (c) 2009, Takeyuki Nagao
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or
 * without modification, are permitted provided that the
 * following conditions are met:
 * 
 *  * Redistributions of source code must retain the above
 *    copyright notice, this list of conditions and the
 *    following disclaimer.
 *  * Redistributions in binary form must reproduce the above
 *    copyright notice, this list of conditions and the
 *    following disclaimer in the documentation and/or other
 *    materials provided with the distribution.
 *    
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
 * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
 * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY
 * OF SUCH DAMAGE.
 */

package jp.sourceforge.dvibrowser.dvicore.doc;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;

import jp.sourceforge.dvibrowser.dvicore.DviConstants;
import jp.sourceforge.dvibrowser.dvicore.DviException;
import jp.sourceforge.dvibrowser.dvicore.DviFontSpec;
import jp.sourceforge.dvibrowser.dvicore.DviFontTable;
import jp.sourceforge.dvibrowser.dvicore.DviObject;
import jp.sourceforge.dvibrowser.dvicore.DviUnit;
import jp.sourceforge.dvibrowser.dvicore.api.DviContextSupport;
import jp.sourceforge.dvibrowser.dvicore.api.DviDocument;
import jp.sourceforge.dvibrowser.dvicore.api.DviInput;
import jp.sourceforge.dvibrowser.dvicore.api.DviPage;
import jp.sourceforge.dvibrowser.dvicore.api.HasURL;
import jp.sourceforge.dvibrowser.dvicore.cmd.DviCommand;
import jp.sourceforge.dvibrowser.dvicore.cmd.DviPostPost;
import jp.sourceforge.dvibrowser.dvicore.cmd.DviPostamble;
import jp.sourceforge.dvibrowser.dvicore.cmd.DviPreamble;
import jp.sourceforge.dvibrowser.dvicore.io.ByteArrayDviData;
import jp.sourceforge.dvibrowser.dvicore.io.DviRandomAccessFileInput;
import jp.sourceforge.dvibrowser.dvicore.render.EmptyDviExecutorHandler;


// TODO: Optimize this class.  It's really slow for a large file.
public class DirectFileDviDocument
extends DviObject
implements DviDocument, HasURL
{
  public static final long MAX_BUFFER_LENGTH = 10000000;
    /* Buffer size for the data after postamble. 10MB */

  private final File file;

  private DviPreamble   preamble = null;
  private DviPostamble  postamble = null;
  private DviPostPost   postPost = null;

  final DviFontTable fontTable = new DviFontTable();

  private ArrayList<DviPage> pages = new ArrayList<DviPage>();

  public DirectFileDviDocument(DviContextSupport dcs, File file)
  throws DviException
  {
    super(dcs);
    this.file = file;
    try {
      RandomAccessFile in = new RandomAccessFile(file, "r");
      parseRandomAccessFile(in);
      in.close();
    } catch (IOException ex) {
      throw new DviException(ex);
    }
  }
  
  private void parseRandomAccessFile(RandomAccessFile in)
  throws IOException, DviException
  {
    // Parse the preamble.
    {
      int idByte, num, den, mag, k;
      byte [] comment;

      if (DviCommand.DVI_PRE != in.readUnsignedByte())
        throw new DviException
          ("Format error in dvi file: file doesn't start with pre.");

      idByte = in.readUnsignedByte();
      num = in.readInt();
      den = in.readInt();
      mag = in.readInt();

      k = in.readUnsignedByte();
      comment = new byte[k];
      in.readFully(comment);

      preamble = new DviPreamble(
        idByte, DviUnit.getInstance(num, den, mag), comment);
    }

    // Determine the location of the postamble.
    final long postPostPointer;
    {
      long pos = in.length() - 1;
      int postamblePointer;
      int idByte;

      long paddingSize = 0;

      while (true) {
        if (pos < 0)
          throw new DviException(
            "Dvi file ended while looking for the postamble.");
        in.seek(pos);
        if (DviConstants.DVI_TRAILER != in.readUnsignedByte()) break;
        paddingSize++;
        pos--;
      }
      pos -= 5;
      if (pos < 0)
        throw new DviException(
          "Dvi file ended while looking for the postamble.");

      postPostPointer = pos;

      /* pos -> +0 DVI_POST_POST (U1)
       *        +1 post_offset   (U4)      
       *        +5 id_byte       (U1)      
       *        +6 padding       paddingSize copies of ((byte)223).
       */
      in.seek(pos);

      if (DviCommand.DVI_POST_POST != in.readUnsignedByte())
        throw new DviException(
          "Format error in dvi file: unable to find post_post.");

      postamblePointer = in.readInt();
      if (postamblePointer < 0 ||
        (long) postamblePointer > in.length() - 33)
          throw new DviException(
            "Format error in dvi file: file size too short.");

      /* TODO: check id_byte */
      idByte = in.readUnsignedByte();

      postPost = new DviPostPost(postamblePointer, idByte);
    }

    in.seek(postPost.postamblePointer());

    /* pos -> + 0 DVI_POST        (U1)
     *        + 1 bp              (S4)
     *        + 5 num             (S4)
     *        + 9 den             (S4)
     *        +13 mag             (S4)
     *        +17 maxV            (S4)
     *        +21 maxH            (S4)
     *        +25 max_stack_depth (U2)
     *        +27 total_pages     (U2)
     *   size = 29 bytes.
     */

    if (DviCommand.DVI_POST != in.readUnsignedByte())
      throw new DviException(
        "Format error in dvi file: unable to find post.");

    {
      int bp;
      int num, den, mag;
      int maxV, maxH, maxStackDepth, totalPages;

      bp   = in.readInt();
      num  = in.readInt();
      den  = in.readInt();
      mag  = in.readInt();
      maxV = in.readInt();
      maxH = in.readInt();
      maxStackDepth = in.readUnsignedShort();
      totalPages    = in.readUnsignedShort();

      postamble = new DviPostamble(
        bp, DviUnit.getInstance(num, den, mag),
        maxV, maxH, maxStackDepth, totalPages);
    }

    // parse font definitions stored right after the postamble.

    {
      long buflen = postPostPointer - in.getFilePointer();
      if (0 < buflen) {
        if (buflen > MAX_BUFFER_LENGTH)
          throw new DviException
            ("Too long data after postamble.");

        final byte [] buf = new byte [(int) buflen];
        in.readFully(buf);

        getDviContext().execute(
          new ByteArrayDviData(buf),
          new EmptyDviExecutorHandler() {
            public void doDefineFont(int fn, DviFontSpec fs) {
              fontTable.put(fn, fs);
            }
          }
        );
      }
    }

    // TODO: handle embedded data.

    {
      long bop = postamble.firstBackPointer();
      long eop = postPost.postamblePointer() - 1;
      int pageNum = postamble.totalPages();

      while (bop != -1 && pageNum > 0) {
        in.seek(bop);

        if (DviCommand.DVI_BOP != in.readUnsignedByte())
          throw new DviException(
            "Format error in dvi file: broken bop link.");

        pageNum--;
        // REMARK: pageNum==0 corresponds to the first page.
        pages.add(0, new DefaultDviPage(DirectFileDviDocument.this, pageNum, bop, eop));

        eop = bop - 1;
        in.seek(bop + 1 + 4 * 10);
        bop = in.readInt();
      }

      if (pageNum != 0)
        throw new DviException(
          "Format error in dvi file: wrong number of pages.");
    }
  }
  
  public int getTotalPages() throws DviException {
    return postamble.totalPages();
  }

  public DviUnit getDviUnit() {
    return postamble.dviUnit();
  }
  public DviPreamble getPreamble() {
    return preamble;
  }
  public DviPostamble getPostamble() {
    return postamble;
  }
  public DviPostPost getPostPost() {
    return postPost;
  }
  public DviFontTable getFontTable() {
    return fontTable;
  }

  public DviInput getInput()
  throws DviException
  {
    try {
      RandomAccessFile raf = new RandomAccessFile(getFile(), "r");
      DviRandomAccessFileInput in = new DviRandomAccessFileInput(raf);
      return in;
    } catch (FileNotFoundException e) {
      throw new DviException(e);
    }
  }

  public DviInput getInput(long start, long end) throws DviException
  {
    try {
      RandomAccessFile raf = new RandomAccessFile(getFile(), "r");
      raf.seek(start);
      DviRandomAccessFileInput in = new DviRandomAccessFileInput(raf);
      in.setOffset(start);
      in.setEnd(end);
      return in;
    } catch (FileNotFoundException e) {
      throw new DviException(e);
    } catch (IOException e) {
      throw new DviException(e);
    }
  }

//  private DviInput getInputNIO(long start, long end)
//  throws DviException
//  {
//    try {
//      FileInputStream fis = new FileInputStream(file);
//      FileChannel fc = fis.getChannel();
//      MappedByteBuffer bb = fc.map(
//        FileChannel.MapMode.READ_ONLY,
//        start, end - start + 1
//      );
//      DviByteBufferInput in = new DviByteBufferInput(bb);
//      in.setOffset(start);
//      return in;
//    } catch (Throwable ex) {
//      Logger.trace(ex);
//      return getInput(start, end);
//    }
//  }

  public DviPage getPage(int p)
  throws DviException
  {
    if (p < 0 || getTotalPages() <= p)
      throw new IllegalArgumentException
        ("page number out of bounds.");

    return pages.get(p);
  }

  public DviPage [] getPages()
  {
    return pages.toArray(new DviPage [0]);
  }

  public long getDataSize() throws DviException
  {
    return getFile().length();
  }

  public File getFile()
  {
    return file;
  }

  public URL getURL() throws DviException
  {
    try {
      return file.toURL();
    } catch (MalformedURLException e) {
      throw new DviException(e);
    }
  }
}
