/*
 * 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.util.ArrayList;

import dvi.DviException;
import dvi.api.BinaryDevice;

// mutable

public final class RunLengthEncodedLine
implements Cloneable
{
  private int hash = 0;
  private int width = 0;
  private boolean headOn = false;
  private boolean tailOn = false;
  private ArrayList<Integer> data
    = new ArrayList<Integer>();
  
  // TODO: Make the return value unmodifiable.
  public ArrayList<Integer> getData() { return data; }
  public boolean headOn() { return headOn; }
  public boolean tailOn() { return tailOn; }
  public int width() { return width; }
  public boolean allOn() {
    return headOn && data.size() == 1;
  }
  public boolean allOff() {
    return !headOn && data.size() == 1;
  }
  public int head() {
    return isEmpty() ? 0 : data.get(0);
  }
  public int tail() {
    return isEmpty() ? 0 : data.get(data.size()-1);
  }
  ///////////////

  public void rasterizeTo(BinaryDevice out)
  throws DviException
  {
    boolean flag = headOn;
    final int ds = data.size();
    for (int i=0; i<ds; i++) {
      out.putBits(data.get(i), flag);
      flag = !flag;
    }
  }

  ///////////////
  
  public void prepend(int count, boolean isOn)
  {
    if (count <= 0) return;
    if (width == 0) {
      headOn = tailOn = isOn;
      data.add(count);
      width = count;
    } else {
      if (headOn == isOn) {
        data.set(0, data.get(0) + count);
      } else {
        data.add(0, count);
        headOn = isOn;
      }
      width += count;
    }
    hash += isOn ? count : -count;
  }

  public void append(int count, boolean isOn)
  {
    if (count <= 0) return;
    if (width == 0) {
      headOn = tailOn = isOn;
      data.add(count);
      width = count;
    } else {
      int last = data.size() - 1;
      if (tailOn == isOn) {
        data.set(last, data.get(last) + count);
      } else {
        data.add(count);
        tailOn = isOn;
      }
      width += count;
    }
    hash += isOn ? count : -count;
  }

  public void cropHead(int count) {
    if (width == 0) return;
    while (count > 0) {
      int c = data.get(0).intValue();
      if (c > count) {
        data.set(0, c - count);
        width -= count;
        hash -= headOn ? count : -count;
        break;
      } else {
        data.remove(0);
        hash -= headOn ? c : -c;
        headOn = !headOn;
        count -= c;
        width -= c;
        if (width <= 0) {
          clear();
          break;
        }
      }
    }
  }

  public void cropTail(int count)
  {
    if (width == 0) return;
    while (count > 0) {
      int last = data.size() - 1;
      int c = data.get(last);
      if (c > count) {
        data.set(last, c - count);
        hash -= tailOn ? count : -count;
        width -= count;
        break;
      } else {
        data.remove(last);
        hash -= tailOn ? c : -c;
        tailOn = !tailOn;
        count -= c;
        width -= c;
        if (width <= 0) {
          clear();
          break;
        }
      }
    }
  }

  ///////////////

  public void prepend(RunLengthEncodedLine rll)
  {
    if (rll.isEmpty()) return;
    if (rll == this)
      rll = (RunLengthEncodedLine) clone();
    boolean flag = rll.tailOn;
    for (int i=rll.data.size()-1; i>=0; i--) {
      prepend(rll.data.get(i), flag);
      flag = !flag;
    }
  }

  public void append(RunLengthEncodedLine rll)
  {
    if (rll.isEmpty()) return;
    if (rll == this)
      rll = (RunLengthEncodedLine) clone();
    boolean flag = rll.headOn;
    final int ds = rll.data.size();
    for (int i=0; i<ds; i++) {
      append(rll.data.get(i), flag);
      flag = !flag;
    }
  }

  ///////////////
  
  public void prepend(byte [] buf) {
    prepend(buf, 0, buf.length * 8, false);
  }
  public void append(byte [] buf) {
    append(buf, 0, buf.length * 8, false);
  }
  public void prepend(byte [] buf, int bitOffset, int bitLen) {
    prepend(buf, bitOffset, bitLen, false);
  }
  public void append(byte [] buf, int bitOffset, int bitLen) {
    append(buf, bitOffset, bitLen, false);
  }
  public void prepend(byte [] buf, int bitOffset, int bitLen, boolean revert)
  {
    if (bitOffset < 0)
      throw new IllegalArgumentException
        ("bit offset can't be negative");
    if (bitLen <= 0) return;
    // TODO: Optimize this code using a look-up table.
    for (int i=bitOffset+bitLen-1; i>=bitOffset; i--) {
      boolean flag = (0 != (buf[i >>> 3] & (1 << (7 - (i & 7)))));
      prepend(1, revert ? !flag : flag);
    }
  }
  
  public void append(byte [] buf,
    int bitOffset, int bitLen, boolean revert)
  {
    if (bitOffset < 0)
      throw new IllegalArgumentException
        ("bit offset can't be negative");
    if (bitLen <= 0) return;
    // TODO: Optimize this code using a look-up table.
    for (int i=0; i<bitLen; i++) {
      final int p = i + bitOffset;
      boolean flag = (0 != (buf[p >>> 3] & (1 << (7 - (p & 7)))));
      append(1, revert ? !flag : flag);
    }
  }

  ///////////////

  public static RunLengthEncodedLine union(RunLengthEncodedLine a, RunLengthEncodedLine b)
  {
    if (a == null || b == null)
      throw new NullPointerException();
    if (a.isEmpty()) return (RunLengthEncodedLine) b.clone();
    if (b.isEmpty()) return (RunLengthEncodedLine) a.clone();

    RunLengthEncodedLine rll = new RunLengthEncodedLine();
    boolean a_flag = a.headOn;
    boolean b_flag = b.headOn;
    int a_ptr = 0;
    int b_ptr = 0;
    int a_size = a.data.size();
    int b_size = b.data.size();
    int a_count = a.data.get(0);
    int b_count = b.data.get(0);
    while (a_count != Integer.MAX_VALUE ||
           b_count != Integer.MAX_VALUE)
    {
      final int count;
      boolean flag = a_flag || b_flag;
      if (a_flag && b_flag) {
        count = Math.max(a_count, b_count);
      } else if (a_flag) {
        count = a_count;
      } else if (b_flag) {
        count = b_count;
      } else {
        count = Math.min(a_count, b_count);
      }

      rll.append(count, flag);

      {
        int c = count;
        while (a_count <= c) {
          c -= a_count;
          if (++a_ptr >= a_size) {
            a_count = Integer.MAX_VALUE;
            a_flag = false;
          } else {
            a_count = a.data.get(a_ptr);
            a_flag = !a_flag;
          }
        }
        if (a_count < Integer.MAX_VALUE)
          a_count -= c;
      }

      {
        int c = count;
        while (b_count <= c) {
          c -= b_count;
          if (++b_ptr >= b_size) {
            b_count = Integer.MAX_VALUE;
            b_flag = false;
          } else {
            b_count = b.data.get(b_ptr);
            b_flag = !b_flag;
          }
        }
        if (b_count < Integer.MAX_VALUE)
          b_count -= c;
      }
    }

    return rll;
  }
  
  ///////////////

  public void clear()
  {
    hash = 0;
    width = 0;
    headOn = tailOn = false;
    data.clear();
  }

  public boolean isEmpty()
  {
    return (width == 0);
  }

  public boolean equals(Object obj)
  {
    if (this == obj) return true;
    if (obj instanceof RunLengthEncodedLine) {
      RunLengthEncodedLine rll = (RunLengthEncodedLine) obj;
      return rll.hash   == hash
          && rll.width  == width
          && rll.headOn == headOn
          && rll.tailOn == tailOn
          && rll.data.equals(data)
          ;
    }

    return false;
  }

  public int hashCode()
  {
    return hash;
  }

  @SuppressWarnings({"unchecked"})
  public Object clone()
  {
    try {
      RunLengthEncodedLine rll = (RunLengthEncodedLine) super.clone();
      rll.data = (ArrayList<Integer>) rll.data.clone();
      return rll;
    } catch (CloneNotSupportedException ex) {
      throw new InternalError(); // this shouldn't happen.
    }
  }

  public String toString()
  {
    StringBuilder sb = new StringBuilder();
    boolean flag = headOn;
    int s = data.size();
    for (int i=0; i<s; i++) {
      int count = data.get(i);
      if (flag)
        sb.append(String.valueOf(count));
      else
        sb.append("(" + String.valueOf(count) + ")");
      flag = !flag;
    }
    return getClass().getName()
      + "[width=" + width
      + " hash=" + hash
      + " headOn=" + headOn
      + " tailOn=" + tailOn
      + " dataSize=" + data.size()
      + " data=[" + sb.toString() + "]"
      ;
  }

  public String dump()
  {
    StringBuilder sb = new StringBuilder();
    boolean flag = headOn;
    int s = data.size();
    for (int i=0; i<s; i++) {
      int count = data.get(i);
      if (flag)
        while (count-- > 0)
          sb.append("*");
      else
        while (count-- > 0)
          sb.append("-");
      flag = !flag;
    }
    return sb.toString();
  }
}
