/*
 * Copyright (c) 1998-2012 Caucho Technology -- all rights reserved
 *
 * This file is part of Resin(R) Open Source
 *
 * Each copy or derived work must preserve the copyright notice and this
 * notice unmodified.
 *
 * Resin Open Source is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * Resin Open Source 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, or any warranty
 * of NON-INFRINGEMENT.  See the GNU General Public License for more
 * details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Resin Open Source; if not, write to the
 *   Free SoftwareFoundation, Inc.
 *   59 Temple Place, Suite 330
 *   Boston, MA 02111-1307  USA
 *
 * @author Scott Ferguson
 */

package com.caucho.util;

import com.caucho.vfs.*;

import java.io.*;
import java.util.*;

public class BinaryDiff {
  int _oldLength;
  ArrayList<TempBuffer> _oldBuffers;
  byte[][] _oldBytes;

  int _chunkSize = 1;
  int _chunkCount;

  int []_suffixArray;

  int []_charMin;
  int []_charMax;
    
  public void delta(OutputStream out, InputStream oldFile, InputStream newFile)
    throws IOException
  {
    readBuffers(oldFile);

    processOrigFile();

    processNewFile(out, newFile);
  }

  protected void add(OutputStream out, byte []buffer, int offset, int length)
    throws IOException
  {
    System.out.println("ADD(" + length + "): '" + new String(buffer, offset, length) + "'");
  }

  protected void copy(OutputStream out, int offset, int length)
    throws IOException
  {
    System.out.println("COPY(" + offset + "," + length + ")");
  }

  private void readBuffers(InputStream oldFile)
    throws IOException
  {
    _oldBuffers = new ArrayList<TempBuffer>();
    _oldLength = 0;

    while (true) {
      TempBuffer buf = TempBuffer.allocate();
      byte []buffer = buf.getBuffer();

      int sublen = readAll(oldFile, buffer);

      if (sublen < 0) {
        TempBuffer.free(buf);
        break;
      }

      _oldLength += sublen;
      _oldBuffers.add(buf);

      if (sublen < buffer.length)
        break;
    }

    _oldBytes = new byte[_oldBuffers.size()][];
    for (int i = 0; i < _oldBytes.length; i++) {
      _oldBytes[i] = _oldBuffers.get(i).getBuffer();
    }
  }

  private void processOrigFile()
  {
    _chunkCount = _oldLength / _chunkSize;

    _suffixArray = new int[_chunkCount];

    int chunkSize = _chunkSize;
    int chunkCount = _chunkCount;
    int []suffixArray = _suffixArray;

    for (int i = 0; i < chunkCount; i++) {
      suffixArray[i] = i * chunkSize;
    }

    byte [][]data = _oldBytes;
    
    sort(suffixArray, _oldBytes, TempBuffer.SIZE, _chunkSize);

    int []charMin = _charMin = new int[256];
    int []charMax = _charMax = new int[256];

    for (int i = 0; i < suffixArray.length; i++) {
      int offset = suffixArray[i];
      
      int ch = data[offset / TempBuffer.SIZE][offset % TempBuffer.SIZE] & 0xff;

      if (charMin[ch] == 0)
        charMin[ch] = i;

      charMax[ch] = i + 1;
    }
  }

  private static void sort(int []suffixArray, byte [][]data,
                           int bufLength, int chunkSize)
  {
    int length = suffixArray.length;
    int dataLength = length * chunkSize;

    for (int i = 0; i < suffixArray.length - 1; i++) {
      for (int j = suffixArray.length - 2; i <= j; j--) {
        int a = suffixArray[j];
        int b = suffixArray[j + 1];

        if (suffixCompareTo(a, b, data, dataLength, bufLength) > 0) {
          int temp = suffixArray[j];
          suffixArray[j] = suffixArray[j + 1];
          suffixArray[j + 1] = temp;
        }
      }
    }
  }

  private static int suffixCompareTo(int a, int b, byte [][]data,
                                     int length, int bufLength)
  {
    int sublen = length - a;

    if (length - b < sublen)
      sublen = length - b;

    for (int i = 0; i < sublen; i++) {
      int a1 = data[a / bufLength][a % bufLength];
      int b1 = data[b / bufLength][b % bufLength];

      if (a1 < b1)
        return -1;
      else if (b1 < a1)
        return 1;

      a++;
      b++;
    }

    return 0;
  }

  private void processNewFile(OutputStream out, InputStream newIn)
    throws IOException
  {
    TempBuffer tempBuf = TempBuffer.allocate();
    byte []buffer = tempBuf.getBuffer();;

    int length = readAll(newIn, buffer);

    int prevOffset = 0;
    int offset = 0;

    int []charMin = _charMin;
    int []charMax = _charMax;
    int minLength = 8;

    byte [][]data = _oldBytes;
    int []suffixArray = _suffixArray;

    loop:
    for (; offset + minLength < length; offset += 1) {
      int ch = buffer[offset] & 0xff;

      int min = charMin[ch];
      int max = charMax[ch];

      byte ch2 = buffer[offset + 1];

      for (int i = min; i < max; i++) {
        int suffixOffset = suffixArray[i];
        int dataOffset = suffixOffset + 1;

        byte d1 = data[suffixOffset / TempBuffer.SIZE][suffixOffset % TempBuffer.SIZE];
        byte d2 = data[dataOffset / TempBuffer.SIZE][dataOffset % TempBuffer.SIZE];
        if (d2 == ch2) {
          dataOffset++;
          int bufOffset = offset + 2;

          match_loop:
          while (bufOffset < length && dataOffset < _oldLength) {
            byte bufCh = buffer[bufOffset];
            byte dataCh = data[dataOffset / TempBuffer.SIZE][dataOffset % TempBuffer.SIZE];

            if (bufCh == dataCh) {
              bufOffset += 1;
              dataOffset += 1;
            }
            else if (bufCh < dataCh) {
              if (dataOffset - suffixOffset >= minLength) {
                if (prevOffset < offset)
                  add(out, buffer, prevOffset, offset - prevOffset);

                offset = bufOffset;
                copy(out, suffixOffset, dataOffset - suffixOffset);
                prevOffset = offset;
                continue loop;
              }
              else
                break;
            }
            else {
              for (i++; i < max; i++) {
                int newSuffix = suffixArray[i];
                int newOffset = newSuffix + (dataOffset - suffixOffset);

                boolean isMatch = true;
                for (int j = newOffset; newSuffix <= j; j--) {
                  dataCh = data[j / TempBuffer.SIZE][j % TempBuffer.SIZE];
                  bufCh = buffer[offset + (j - newSuffix)];

                  if (bufCh != dataCh) {
                    isMatch = false;
                    break;
                  }
                }

                if (isMatch) {
                  dataOffset = newOffset;
                  suffixOffset = newSuffix;
                  continue match_loop;
                }
              }

              if (dataOffset - suffixOffset >= minLength) {
                if (prevOffset < offset)
                  add(out, buffer, prevOffset, offset - prevOffset);

                offset = bufOffset;
                prevOffset = offset;
                copy(out, suffixOffset, dataOffset - suffixOffset);
                continue loop;
              }
              else
                break;
            }
          }

          if (dataOffset - suffixOffset >= minLength) {
            if (prevOffset < offset)
              add(out, buffer, prevOffset, offset - prevOffset);

            offset = bufOffset;
            prevOffset = offset;
            copy(out, suffixOffset, dataOffset - suffixOffset);
          }

          break;
        }
        else if (ch2 < d2)
          break;
      }
    }

    if (prevOffset < length)
      add(out, buffer, prevOffset, length - prevOffset);

    TempBuffer.free(tempBuf);
  }

  private int readAll(InputStream is, byte []buffer)
    throws IOException
  {
    int offset = 0;

    while (offset < buffer.length) {
      int sublen = buffer.length - offset;

      sublen = is.read(buffer, offset, sublen);

      if (sublen < 0)
        return offset > 0 ? offset : -1;

      offset += sublen;
    }

    return buffer.length;
  }
}


