/*
 * 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.PrintWriter;
import java.io.StringWriter;
import java.util.ArrayList;

import dvi.DviException;
import dvi.DviPoint;
import dvi.DviRect;
import dvi.DviResolution;
import dvi.api.BinaryDevice;
import dvi.render.AbstractDevice;

// mutable.

public final class RunLengthEncodedGlyph
{
  private static final int DEFAULT_ARRAY_SIZE = 256;

  private int width;
  private int height;
  private int xOffset;
  private int yOffset;
  private ArrayList<RunLengthEncodedLine> lines;

  public RunLengthEncodedGlyph()
  {
    this(0, 0, 0, 0);
  }

  private RunLengthEncodedGlyph(int width, int height, int xOffset, int yOffset)
  {
    this.width = width;
    this.height = height;
    this.xOffset = xOffset;
    this.yOffset = yOffset;
    this.lines = new ArrayList<RunLengthEncodedLine>(DEFAULT_ARRAY_SIZE);
  }

  public static RunLengthEncodedGlyph readByteBinary(
    byte [] buf, int width, int height,
    int xOffset, int yOffset)
  {
    RunLengthEncodedGlyph rlg = new RunLengthEncodedGlyph(width, height, xOffset, yOffset);

    RunLengthEncodedLine prev = null;
    int pitch = ((width + 7) >>> 3) << 3;
    int bitOffset = 0;
    for (int i=0; i<height; i++) {
      RunLengthEncodedLine line = new RunLengthEncodedLine();
      line.append(buf, bitOffset, width);
      if (line.equals(prev)) {
        rlg.lines.add(prev);
      } else {
        rlg.lines.add(line);
        prev = line;
      }
      bitOffset += pitch;
    }

    rlg.compact();

    return rlg;
  }

  public static RunLengthEncodedGlyph readRasterByBits(
    byte [] buf, int width, int height,
    int xOffset, int yOffset)
  {
    RunLengthEncodedGlyph rlg = new RunLengthEncodedGlyph(width, height, xOffset, yOffset);

    int bitOffset = 0;
    RunLengthEncodedLine prev = null;
    for (int i=0; i<height; i++) {
      RunLengthEncodedLine line = new RunLengthEncodedLine();
      line.append(buf, bitOffset, width);
      bitOffset += width;
      if (line.equals(prev)) {
        rlg.lines.add(prev);
      } else {
        rlg.lines.add(line);
        prev = line;
      }
    }
    rlg.compact();

    return rlg;
  }

  public static RunLengthEncodedGlyph readByteGray(
    byte [] buf, int width, int height,
    int xOffset, int yOffset)
  {
    RunLengthEncodedGlyph rlg = new RunLengthEncodedGlyph(width, height, xOffset, yOffset);

    int ptr = 0;
    RunLengthEncodedLine prev = null;
    for (int i=0; i<height; i++) {
      final RunLengthEncodedLine line = new RunLengthEncodedLine();

      int last = 314; // A MAGIC value to denote the begin of line.
      int count = 0;
      for (int j=0; j<width; j++) {
        final int c = buf[ptr++];
        if (c == last) {
          count++;
        } else {
          if (last != 314)
            line.append(count, (0 != last));
          count = 1;
          last = c;
        }
      }
      if (count > 0)
        line.append(count, (0 != last));

      if (line.equals(prev)) {
        rlg.lines.add(prev);
      } else {
        rlg.lines.add(line);
        prev = line;
      }
    }
    rlg.compact();

    return rlg;
  }


  public DviRect getBounds()
  {
    return new DviRect(-xOffset, -yOffset, width, height);
  }
  public int width()   { return width; }
  public int height()  { return height; }
  public int xOffset() { return xOffset; }
  public int yOffset() { return yOffset; }

  public boolean isEmpty()
  {
    return (width <= 0 || height <= 0 || lines.size() == 0);
  }

  public void rasterizeTo(BinaryDevice out)
  throws DviException
  {
    out.save();
    try {
      out.translate(-xOffset, -yOffset);
      if (out.beginRaster(width, height)) {
      // TODO: use repeat.
        for (int i=0; i<height; i++) {
          RunLengthEncodedLine line = lines.get(i);
          out.beginLine();
          line.rasterizeTo(out);
          out.endLine(0);
        }
      }
      out.endRaster();
    } finally {
      out.restore();
    }
  }

  public void compact()
  {
    int lskip = Integer.MAX_VALUE;
    int rskip = Integer.MAX_VALUE;

    RunLengthEncodedLine prev = null;
    for (int i=0; i<height; i++) {
      RunLengthEncodedLine line = lines.get(i);
      if (prev == line) continue;
      if (lskip > 0) {
        if (line.headOn()) {
          lskip = 0;
        } else {
          lskip = Math.min(lskip, line.head());
        }
      }
    }

    prev = null;
    for (int i=0; i<height; i++) {
      RunLengthEncodedLine line = lines.get(i);
      if (prev == line) continue;
      line.cropHead(lskip);
      prev = line;
    }
    width -= lskip;
    xOffset -= lskip;

    if (width <= 0) {
      width = height = 0;
      lines.clear();
      xOffset = yOffset = 0;
      return;
    }

    prev = null;
    for (int i=0; i<height; i++) {
      RunLengthEncodedLine line = lines.get(i);
      if (prev == line) continue;
      if (rskip > 0) {
        if (line.tailOn()) {
          rskip = 0;
        } else {
          rskip = Math.min(rskip, line.tail());
        }
      }
      prev = line;
    }

    prev = null;
    for (int i=0; i<height; i++) {
      RunLengthEncodedLine line = lines.get(i);
      if (prev == line) continue;
      line.cropTail(rskip);
      prev = line;
    }
    width -= rskip;

    if (width <= 0) {
      width = height = 0;
      lines.clear();
      xOffset = yOffset = 0;
      return;
    }

    int tskip = 0;
    int bskip = 0;

    while (height > 0) {
      RunLengthEncodedLine line = lines.get(0);
      if (!line.allOff())
        break;
      lines.remove(0);
      height--;
      yOffset--;
      tskip++;
    }

    while (height > 0) {
      RunLengthEncodedLine line = lines.get(height-1);
      if (!line.allOff())
        break;
      lines.remove(height-1);
      height--;
      bskip++;
    }

    for (int i=0; i<height; i++) {
      RunLengthEncodedLine line = lines.get(i);
      if (line.isEmpty()) {
        throw new IllegalStateException
          ("width=" + width + " height=" + height);
      }
    }
    if (width <= 0 || height <= 0) {
      throw new IllegalStateException
        ("width=" + width + " height=" + height);
    }
  }

  public String dump()
  {
    StringWriter sw = new StringWriter();
    PrintWriter pw = new PrintWriter(sw);
    for (int i=0; i<height; i++) {
      RunLengthEncodedLine line = lines.get(i);
      boolean duplicate = (
        (i > 0) && lines.get(i-1) == line
      );
      pw.println((duplicate ? "+ " : "  " ) + line.dump());
    }
    return sw.toString();
  }

  public String toString()
  {
    return getClass().getName()
      + "[width=" + width
      + " height=" +height
      + "]";
  }

  public PkGlyph toPkGlyph()
  {
    if (isEmpty())
      return PkGlyph.EMPTY;

    ArrayList<Integer> counts = new ArrayList<Integer>(DEFAULT_ARRAY_SIZE);

    int count=0;
    int repeat=0;
    RunLengthEncodedLine line = lines.get(0);
    boolean turnOn = line.headOn();
    int i = 0;
    while (line != null) {
      ArrayList<Integer> data = line.getData();
      final int ds = data.size();
      for (int j=0; j<ds; j++) {
        int c = data.get(j);
        boolean needFlush = true;
        if (j == ds - 1) {
          // at the end of the line.
          int r = 0;
          RunLengthEncodedLine next = null;
          while (++i < height) {
            next = lines.get(i);
            if (next != line)
              break;
            r++;
          }
          needFlush = (
            next == null ||
            next.headOn() != line.tailOn()
          );
          line = next;
          if (count > 0) {
            count += data.get(j) + r * width;
            // repeat is unchanged.
          } else {
            count = data.get(j);
            repeat = r;
          }
        } else {
          count += c;
        }
        if (needFlush) {
          if (repeat > 0) {
            counts.add(-repeat);
            repeat = 0;
          }
          counts.add(count);
          count = 0;
        }
      }
    }

    PackedSequence ps = new SequencePacker(counts).pack();

    return new PkGlyph(
      width, height,
      ps.data(), ps.dynF(),
      turnOn,
      xOffset, yOffset
    );
  }

  public DviRect bounds()
  throws DviException
  {
    if (isEmpty())
      return DviRect.EMPTY;
    else
      return new DviRect(-xOffset, -yOffset, width, height);
  }

  public void unite(RunLengthEncodedGlyph rlg)
  {
    DviRect u = rlg.getBounds().union(getBounds());

    ArrayList<RunLengthEncodedLine> newLines
      = new ArrayList<RunLengthEncodedLine>(DEFAULT_ARRAY_SIZE);
    RunLengthEncodedLine last = null;
    int ey = u.bottom();
    for (int y = u.top(); y<=ey; y++) {
      RunLengthEncodedLine a = new RunLengthEncodedLine();
      RunLengthEncodedLine b = new RunLengthEncodedLine();
      int ia = y + yOffset;
      int ib = y + rlg.yOffset;

      if (0 <= ia && ia < height) {
        int w = u.width();
        int lpad = -xOffset-u.left();
        if (lpad > 0) {
          a.append(lpad, false);
          w -= lpad;
        }
        a.append(lines.get(ia));
        w -= width;
        a.append(w, false);
      } else {
        a.append(u.width(), false);
      }

      if (0 <= ib && ib < rlg.height) {
        int w = u.width();
        int lpad = -rlg.xOffset-u.left();
        if (lpad > 0) {
          b.append(lpad, false);
          w -= lpad;
        }
        b.append(rlg.lines.get(ib));
        w -= rlg.width;
        b.append(w, false);
      } else {
        b.append(u.width(), false);
      }

      a = RunLengthEncodedLine.union(a, b);

      if (a.equals(last)) {
        newLines.add(last);
      } else {
        newLines.add(a);
        last = a;
      }
    }

    this.width   = u.width();
    this.height  = u.height();
    this.xOffset = -u.x();
    this.yOffset = -u.y();
    this.lines = newLines;
    compact();
  }


  public BinaryDevice getBinaryDevice(DviResolution res)
  throws DviException
  {
    return new BinaryDeviceImpl(res);
  }

//  private static String dumpCounts(ArrayList<Integer> counts, boolean flag)
//  {
//    String str = "";
//
//    for (int k=0; k<counts.size(); k++) {
//      int c = counts.get(k);
//      
//      if (c < 0) {
//        str += "[" + (-c) + "]";
//      } else if (flag) {
//        str += String.valueOf(c);
//        flag = !flag;
//      } else {
//        str += "(" + c + ")";
//        flag = !flag;
//      }
//    }
//
//    return str;
//  }

  private class BinaryDeviceImpl
  extends AbstractDevice
  implements BinaryDevice
  {
    private BinaryDeviceImpl(DviResolution res)
    {
      super(res);
    }

    public void begin()
    throws DviException
    {
    }

    public void end()
    throws DviException
    {
    }

    private RunLengthEncodedGlyph rlg = null;
    public boolean beginRaster(int w, int h)
    throws DviException
    {
      rlg = new RunLengthEncodedGlyph();
      rlg.width = w;
      rlg.height = h;
      return true;
    }

    public void endRaster()
    throws DviException
    {
      DviPoint p = getReferencePoint();
      // The reference point of rlg is at the origin of this device.
      rlg.xOffset = -p.x;
      rlg.yOffset = -p.y;
      rlg.compact();

      unite(rlg);
      rlg = null;
    }

    private RunLengthEncodedLine line = null;
    public void beginLine()
    throws DviException
    {
      line = new RunLengthEncodedLine();
    }

    public void endLine(int repeat)
    throws DviException
    {
      for (int i=0; i<=repeat; i++) {
        rlg.lines.add(line);
      }
      line = null;
    }

    public void putBits(int count, boolean paintFlag)
    {
      line.append(count, paintFlag);
    }
  }
}
