// (‑●‑●)> released under the WTFPL v2 license, by Gregory Pakosz (@gpakosz)

package org.mozilla.universalchardet;

import java.io.IOException;
import java.io.InputStream;
import java.io.PushbackInputStream;

/**
 * The <code>UnicodeBOMInputStream</code> class wraps any
 * <code>InputStream</code> and detects the presence of any Unicode BOM
 * (Byte Order Mark) at its beginning, as defined by
 * <a href="http://www.faqs.org/rfcs/rfc3629.html">RFC 3629 - UTF-8, a
 * transformation format of ISO 10646</a>
 *
 * <p>The
 * <a href="http://www.unicode.org/unicode/faq/utf_bom.html">Unicode FAQ</a>
 * defines 5 types of BOMs:</p><ul>
 * <li><pre>00 00 FE FF  = UTF-32, big-endian</pre></li>
 * <li><pre>FF FE 00 00  = UTF-32, little-endian</pre></li>
 * <li><pre>FE FF        = UTF-16, big-endian</pre></li>
 * <li><pre>FF FE        = UTF-16, little-endian</pre></li>
 * <li><pre>EF BB BF     = UTF-8</pre></li>
 * </ul>
 *
 * <p>Use the {@link #getBOM()} method to know whether a BOM has been detected
 * or not.
 * </p>
 * <p>Use the {@link #skipBOM()} method to remove the detected BOM from the
 * wrapped <code>InputStream</code> object.</p>
 *
 * @author Gregory Pakosz
 * @version 1.0
 */
public class UnicodeBOMInputStream extends InputStream {
	/**
	 * Type safe enumeration class that describes the different types of Unicode
	 * BOMs.
	 */
	public static final class BOM {

		final byte bytes[];
		private final String description;
		/**
		 * NONE.
		 */
		public static final BOM NONE = new BOM(new byte[] {}, "NONE");

		/**
		 * UTF-8 BOM (EF BB BF).
		 */
		public static final BOM UTF_8 = new BOM(new byte[] { (byte) 0xEF, (byte) 0xBB, (byte) 0xBF }, "UTF-8");

		/**
		 * UTF-16, little-endian (FF FE).
		 */
		public static final BOM UTF_16_LE = new BOM(new byte[] { (byte) 0xFF, (byte) 0xFE }, "UTF-16 little-endian");

		/**
		 * UTF-16, big-endian (FE FF).
		 */
		public static final BOM UTF_16_BE = new BOM(new byte[] { (byte) 0xFE, (byte) 0xFF }, "UTF-16 big-endian");

		/**
		 * UTF-32, little-endian (FF FE 00 00).
		 */
		public static final BOM UTF_32_LE = new BOM(new byte[] { (byte) 0xFF, (byte) 0xFE, (byte) 0x00, (byte) 0x00 },
				"UTF-32 little-endian");

		/**
		 * UTF-32, big-endian (00 00 FE FF).
		 */
		public static final BOM UTF_32_BE = new BOM(new byte[] { (byte) 0x00, (byte) 0x00, (byte) 0xFE, (byte) 0xFF },
				"UTF-32 big-endian");

		/**
		 * Returns a <code>String</code> representation of this <code>BOM</code>
		 * value.
		 */
		public final String toString() {
			return description;
		}

		/**
		 * Returns the bytes corresponding to this <code>BOM</code> value.
		 * @return the bytes corresponding to this <code>BOM</code> value.
		 */
		public final byte[] getBytes() {
			final int length = bytes.length;
			final byte[] result = new byte[length];

			// make a defensive copy
			System.arraycopy(bytes, 0, result, 0, length);

			return result;
		}

		private BOM(final byte bom[], final String description) {
			assert (bom != null) : "invalid BOM: null is not allowed";
			assert (description != null) : "invalid description: null is not allowed";
			assert (description.length() != 0) : "invalid description: empty string is not allowed";

			this.bytes = bom;
			this.description = description;
		}

	} // BOM

	private final PushbackInputStream in;
	private final BOM bom;
	private boolean skipped = false;

  
  /**
   * Constructs a new <code>UnicodeBOMInputStream</code> that wraps the
   * specified <code>InputStream</code>. By default skip BOM bytes
   *
   * @param inputStream an <code>InputStream</code>.
   *
   * @throws NullPointerException when <code>inputStream</code> is
   * <code>null</code>.
   * @throws IOException on reading from the specified <code>InputStream</code>
   * when trying to detect the Unicode BOM.
   */
  
  public UnicodeBOMInputStream(final InputStream inputStream) throws IOException {
	  this(inputStream, true);
  }
  
  
  /**
   * Constructs a new <code>UnicodeBOMInputStream</code> that wraps the
   * specified <code>InputStream</code>.
   *
   * @param inputStream an <code>InputStream</code>.
   * @param skipIfFound to automatically skip BOM bytes if found
   *
   * @throws NullPointerException when <code>inputStream</code> is
   * <code>null</code>.
   * @throws IOException on reading from the specified <code>InputStream</code>
   * when trying to detect the Unicode BOM.
   */
	public UnicodeBOMInputStream(final InputStream inputStream,
			boolean skipIfFound) throws IOException {
		if (inputStream == null) {
			throw new NullPointerException(
					"invalid input stream: null is not allowed");
		}
		in = new PushbackInputStream(inputStream, 4);

		final byte bom[] = new byte[4];
		final int read = in.read(bom);

		switch (read) {
		case 4:
			if ((bom[0] == (byte) 0xFF) && (bom[1] == (byte) 0xFE)
					&& (bom[2] == (byte) 0x00) && (bom[3] == (byte) 0x00)) {
				this.bom = BOM.UTF_32_LE;
				break;
			} else if ((bom[0] == (byte) 0x00) && (bom[1] == (byte) 0x00)
					&& (bom[2] == (byte) 0xFE) && (bom[3] == (byte) 0xFF)) {
				this.bom = BOM.UTF_32_BE;
				break;
			}

		case 3:
			if ((bom[0] == (byte) 0xEF) && (bom[1] == (byte) 0xBB)
					&& (bom[2] == (byte) 0xBF)) {
				this.bom = BOM.UTF_8;
				break;
			}

		case 2:
			if ((bom[0] == (byte) 0xFF) && (bom[1] == (byte) 0xFE)) {
				this.bom = BOM.UTF_16_LE;
				break;
			} else if ((bom[0] == (byte) 0xFE) && (bom[1] == (byte) 0xFF)) {
				this.bom = BOM.UTF_16_BE;
				break;
			}

		default:
			this.bom = BOM.NONE;
			break;
		}

		if (read > 0) {
			in.unread(bom, 0, read);
		}
		if (skipIfFound) {
			this.skipBOM();
		}

	}

  /**
   * Returns the <code>BOM</code> that was detected in the wrapped
   * <code>InputStream</code> object.
   *
   * @return a <code>BOM</code> value.
   */
	public final BOM getBOM() {
		// BOM type is immutable.
		return bom;
	}

  /**
   * Skips the <code>BOM</code> that was found in the wrapped
   * <code>InputStream</code> object.
   *
   * @return this <code>UnicodeBOMInputStream</code>.
   *
   * @throws IOException when trying to skip the BOM from the wrapped
   * <code>InputStream</code> object.
   */
	public final synchronized UnicodeBOMInputStream skipBOM()
			throws IOException {
		if (!skipped) {
			long bytesToSkip = bom.bytes.length;
			long bytesSkipped = in.skip(bytesToSkip);
			for (long i = bytesSkipped; i < bytesToSkip; i++) {
				in.read();
			}
			skipped = true;
		}
		return this;
	}

  /**
   * {@inheritDoc}
   */
	public int read() throws IOException {
		this.skipped = true;
		return in.read();
	}

  /**
   * {@inheritDoc}
   */
	public int read(final byte b[]) throws IOException {
		this.skipped = true;
		return in.read(b, 0, b.length);
	}

  /**
   * {@inheritDoc}
   */
	public int read(final byte b[], final int off, final int len) throws IOException {
		this.skipped = true;
		return in.read(b, off, len);
	}

  /**
   * {@inheritDoc}
   */
	public long skip(final long n) throws IOException {
		this.skipped = true;
		return in.skip(n);
	}

  /**
   * {@inheritDoc}
   */
	public int available() throws IOException {
		return in.available();
	}

  /**
   * {@inheritDoc}
   */
	public void close() throws IOException {
		in.close();
	}

  /**
   * {@inheritDoc}
   */
	public synchronized void mark(final int readlimit) {
		in.mark(readlimit);
	}

  /**
   * {@inheritDoc}
   */
	public synchronized void reset() throws IOException {
		in.reset();
	}

  /**
   * {@inheritDoc}
   */
	public boolean markSupported() {
		return in.markSupported();
	}


}
