/*
 * 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 dvi.font;

import java.io.IOException;

import dvi.DviException;
import dvi.DviRect;
import dvi.api.BinaryDevice;
import dvi.api.DviInput;
import dvi.api.Glyph;

// immutable.

public class PkGlyph
implements Glyph
{
  private static final long serialVersionUID = 3825743583018783073L;

  public static final PkGlyph EMPTY
    = new PkGlyph(
        0, 0,
        null,
        0, false,
        0, 0 
      );

  private int flagByte;
  private int dynF;
  private boolean turnOn;
  private int packetLength;
  private int code;
  private int tfmw;
  private byte [] raster = null;

  private int xAdvance;
  private int yAdvance;
  private int width;
  private int height;
  private int xOffset;
  private int yOffset;
  private DviRect bbox;

  // This constructor is used internally.
  private PkGlyph() {}

  public PkGlyph(
    int width, int height,
    byte [] raster,
    int dynF, boolean turnOn,
    int xOffset, int yOffset)
  {
    this.width = width;
    this.height = height;
    this.raster = raster;
    this.dynF = dynF;
    this.turnOn = turnOn;
    this.xOffset = xOffset;
    this.yOffset = yOffset;

    bbox = new DviRect(-xOffset, -yOffset, width, height);
      
    flagByte = 7;
    packetLength = 0;
    code = 0;
    tfmw = 0; // TODO: write tfmw.
    xAdvance = yAdvance = 0;
  }

  public static Glyph readFromInput(DviInput in)
  throws DviException, IOException
  {
    return readFromInput(in.readU1(), in);
  }

  public static Glyph readFromInput(int firstByte, DviInput in)
  throws DviException, IOException
  {
    PkGlyph g = new PkGlyph();

    // firstByte =
    //               7   6   5   4    3     2   1   0
    //             +---+---+---+---+------+---+---+---+
    //         MSB |      dynF     |turnOn| flagByte  | LSB.
    //             +---+---+---+---+------+---+---+---+

    g.flagByte = firstByte & 7;
    g.turnOn   = (0 != (firstByte & 8));
    g.dynF     = (firstByte >> 4) & 15;

    int rasterSize;
    if (g.flagByte == 7) {
      g.packetLength = in.readS4();
      g.code         = in.readS4();
      g.tfmw         = in.readS4();
      g.xAdvance     = in.readS4();
      g.yAdvance     = in.readS4();
      g.width        = in.readS4();
      g.height       = in.readS4();
      g.xOffset      = in.readS4();
      g.yOffset      = in.readS4();

      g.packetLength += 9;
      rasterSize = g.packetLength - 37;
    } else if (g.flagByte > 3) {
      g.packetLength = (g.flagByte - 4) * 65536
                     + in.readU2();
      g.code         = in.readU1();
      g.tfmw         = in.readS3();
      g.xAdvance     = in.readU2() * 65536;
      g.yAdvance     = 0;
      g.width        = in.readU2();
      g.height       = in.readU2();
      g.xOffset      = in.readS2();
      g.yOffset      = in.readS2();

      g.packetLength += 4;
      rasterSize = g.packetLength - 17;
    } else {
      g.packetLength = g.flagByte * 256
                   + in.readU1();
      g.code         = in.readU1();
      g.tfmw         = in.readS3();
      g.xAdvance     = in.readU1() * 65536;
      g.yAdvance     = 0;
      g.width        = in.readU1();
      g.height       = in.readU1();
      g.xOffset      = in.readS1();
      g.yOffset      = in.readS1();

      g.packetLength += 3;
      rasterSize = g.packetLength - 11;
    }

    if (rasterSize > 0) {
      g.raster = new byte[rasterSize];
      in.readFully(g.raster);
    } else if (rasterSize == 0) {
      // TODO: handle the case where rasterSize == 0 && turnOn==true.
      // This can happen.  It's not an error.
    } else {
      throw new DviException
          ("Negative rasterSize: " + rasterSize);
    }

    g.bbox = new DviRect(-g.xOffset, -g.yOffset, g.width, g.height);

    return g;
  }

  public boolean rasterByBits() { return (dynF == PkConstants.PK_DYNF_RASTER_BY_BITS); }
  public int flagByte()         { return flagByte; }
  public int dynF()             { return dynF; }
  public boolean turnOn()       { return turnOn; }
  public int packetLength()     { return packetLength; }

  public byte [] raster()       { return raster; }

  public int code()             { return code; }

  public int getTfmWidth()      { return tfmw; }

  public int xAdvance()         { return xAdvance; }
  public int yAdvance()         { return yAdvance; }
  public int width()            { return width; }
  public int height()           { return height; }
  public int xOffset()          { return xOffset; }
  public int yOffset()          { return yOffset; }
  public DviRect bounds()    { return bbox; }

  public void rasterizeTo(BinaryDevice out)
  throws DviException
  {
    out.save();
    try {
      out.translate(-xOffset, -yOffset);
      if (rasterByBits()) {
        // TODO: test this code.
        RunLengthEncodedGlyph rlg = RunLengthEncodedGlyph.readRasterByBits(
          raster,
          width, height,
          0, 0 // Offsets are not used when drawing a raster by bits.
        );
        rlg.rasterizeTo(out);
      } else {
        PackedGlyphRasterizer sub = new PackedGlyphRasterizer();
        sub.begin(this);
        sub.rasterizeTo(out);
        sub.end();
      }
    } finally {
      out.restore();
    }
  }

  public String toString()
  {
    return  getClass().getName()
            + "[packetLength=" + packetLength
            + ",flagByte=" + flagByte
            + ",dynF=" + dynF
            + ",code=" + code
            + ",tfmw=" + tfmw
            + ",xAdvance=" + xAdvance
            + ",yAdvance=" + yAdvance
            + ",width=" + width
            + ",height=" + height
            + ",xOffset=" + xOffset
            + ",yOffset=" + yOffset
            + (
                (raster != null) ?
                (",rasterSize=" + raster.length) :
                ",raster=null"
              )
            + "]"
            ;

  }
}
