package org.basex.io.out;

import java.io.DataOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;

import org.basex.io.IO;
import org.basex.util.Num;
import org.basex.util.Token;
import org.basex.util.list.IntList;

/**
 * This is an output stream for project specific data types.
 * It bears resemblance to Java's {@link DataOutputStream}.
 *
 * @author BaseX Team 2005-11, BSD License
 * @author Christian Gruen
 */
public final class DataOutput extends OutputStream {
  /** The underlying output stream. */
  private final OutputStream os;
  /** Number of written bytes. */
  private long size;

  /**
   * Constructor, specifying an output stream.
   * @param out output stream to operate on
   */
  public DataOutput(final OutputStream out) {
    os = out;
  }

  /**
   * Constructor, specifying a file.
   * @param db name of the database
   * @throws IOException I/O exception
   */
  public DataOutput(final File db) throws IOException {
    this(db, IO.BLOCKSIZE);
  }

  /**
   * Constructor, specifying a file and a buffer size.
   * The specified buffer size is used.
   * @param db name of the database
   * @param bufs size of the buffer to use
   * @throws IOException I/O exception
   */
  public DataOutput(final File db, final int bufs) throws IOException {
    os = new BufferOutput(new FileOutputStream(db), bufs);
  }

  @Override
  public void write(final int b) throws IOException {
    os.write(b);
    ++size;
  }

  /**
   * Writes a byte array.
   * @param bytes array to be written
   * @throws IOException I/O exception
   */
  public void writeBytes(final byte[] bytes) throws IOException {
    for(final byte b : bytes) write(b);
  }

  /**
   * Writes a boolean value.
   * @param b boolean value
   * @throws IOException I/O exception
   */
  public void writeBool(final boolean b) throws IOException {
    write(b ? 1 : 0);
  }

  /**
   * Writes a string.
   * @param s string
   * @throws IOException I/O exception
   */
  public void writeString(final String s) throws IOException {
    writeToken(Token.token(s));
  }

  /**
   * Writes a token.
   * @param tok array to be written
   * @return number of written bytes
   * @throws IOException I/O exception
   */
  public int writeToken(final byte[] tok) throws IOException {
    final int s = writeNum(tok.length);
    writeBytes(tok);
    return s + tok.length;
  }

  /**
   * Writes a double value.
   * @param num array to be written
   * @return number of written bytes
   * @throws IOException I/O exception
   */
  public int writeDouble(final double num) throws IOException {
    return writeToken(Token.token(num));
  }

  /**
   * Writes tokens. {@code null} references are replaced by an empty array.
   * @param array array to be written
   * @throws IOException I/O exception
   */
  public void writeTokens(final byte[][] array) throws IOException {
    writeNum(array.length);
    for(final byte[] a : array) writeToken(a != null ? a : Token.EMPTY);
  }

  /**
   * Writes distances between integers.
   * @param array array to be written
   * @throws IOException I/O exception
   */
  public void writeDiffs(final IntList array) throws IOException {
    final int al = array.size();
    writeNum(al);
    int c = 0;
    for(int a = 0; a < al; a++) {
      final int t = array.get(a);
      writeNum(t - c);
      c = t;
    }
  }

  /**
   * Writes compressed numbers; see {@link Num} for more.
   * @param array array to be written
   * @throws IOException I/O exception
   */
  public void writeNums(final int[] array) throws IOException {
    writeNum(array.length);
    for(final int a : array) writeNum(a);
  }

  /**
   * Writes a compressed integer value; see {@link Num} for more.
   * By compressing, the size of the database files is reduced.
   * @param v value to be written
   * @return number of written values
   * @throws IOException I/O exception
   */
  public int writeNum(final int v) throws IOException {
    if(v < 0 || v > 0x3FFFFFFF) {
      write(0xC0); write(v >>> 24); write(v >>> 16); write(v >>> 8); write(v);
      return 5;
    }
    if(v > 0x3FFF) {
      write(v >>> 24 | 0x80); write(v >>> 16); write(v >>> 8); write(v);
      return 4;
    }
    if(v > 0x3F) {
      write(v >>> 8 | 0x40); write(v);
      return 2;
    }
    write(v);
    return 1;
  }

  /**
   * Writes long values.
   * NOTE: the long values are not compressed!
   * @param array array to be written
   * @throws IOException I/O exception
   */
  public void writeLongs(final long[] array) throws IOException {
    writeNum(array.length);
    for(final long a : array) write8(a);
  }

  /**
   * Writes a long value.
   * @param v value to be written
   * @throws IOException I/O exception
   */
  public void write8(final long v) throws IOException {
    write((byte) (v >>> 56));
    write((byte) (v >>> 48));
    write((byte) (v >>> 40));
    write((byte) (v >>> 32));
    write((byte) (v >>> 24));
    write((byte) (v >>> 16));
    write((byte) (v >>>  8));
    write((byte)  v);
  }

  /**
   * Writes an integer value.
   * @param v value to be written
   * @throws IOException I/O exception
   */
  public void write4(final int v) throws IOException {
    write(v >>> 24);
    write(v >>> 16);
    write(v >>>  8);
    write(v);
  }

  /**
   * Writes a byte value.
   * @param v value to be written
   * @throws IOException I/O exception
   */
  public void write1(final int v) throws IOException {
    write(v);
  }

  /**
   * Writes a short value.
   * @param v value to be written
   * @throws IOException I/O exception
   */
  public void write2(final int v) throws IOException {
    write(v >>> 8);
    write(v);
  }

  /**
   * Writes 5 bytes of a long value.
   * @param v value to be written
   * @throws IOException I/O exception
   */
  public void write5(final long v) throws IOException {
    write((byte) (v >>> 32));
    write((byte) (v >>> 24));
    write((byte) (v >>> 16));
    write((byte) (v >>>  8));
    write((byte) v);
  }

  /**
   * Returns the number of written bytes.
   * This is not necessarily e.g. the file size.
   * @return number of written bytes
   */
  public long size() {
    return size;
  }

  @Override
  public void flush() throws IOException {
    os.flush();
  }

  @Override
  public void close() throws IOException {
    os.close();
  }
}
