/*
 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
 *
 * "The contents of this file are subject to the Mozilla Public License
 * Version 1.1 (the "License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the License at
 * http://www.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS"
 * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
 * License for the specific language governing rights and limitations under
 * the License.
 *
 * The Original Code is ICEpdf 3.0 open source software code, released
 * May 1st, 2009. The Initial Developer of the Original Code is ICEsoft
 * Technologies Canada, Corp. Portions created by ICEsoft are Copyright (C)
 * 2004-2009 ICEsoft Technologies Canada, Corp. All Rights Reserved.
 *
 * Contributor(s): _____________________.
 *
 * Alternatively, the contents of this file may be used under the terms of
 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"
 * License), in which case the provisions of the LGPL License are
 * applicable instead of those above. If you wish to allow use of your
 * version of this file only under the terms of the LGPL License and not to
 * allow others to use your version of this file under the MPL, indicate
 * your decision by deleting the provisions above and replace them with
 * the notice and other provisions required by the LGPL License. If you do
 * not delete the provisions above, a recipient may use your version of
 * this file under either the MPL or the LGPL License."
 *
 */
package com.softwareimaging.jpdf.core.pobjects;

import com.softwareimaging.android.graphics.BufferedImage;
import com.softwareimaging.android.graphics.Color;
import com.softwareimaging.data.ILogicalStreamReader;
import com.softwareimaging.data.InputStreamHelper;
import com.softwareimaging.data.LogicalReaderFromByteArray;
import com.softwareimaging.data.SeekOrigin;
import com.softwareimaging.exceptions.FileHandlerNotInitializedException;
import com.softwareimaging.jpdf.core.io.BitStream;
import com.softwareimaging.jpdf.core.io.ConservativeSizingByteArrayOutputStream;
import com.softwareimaging.jpdf.core.io.SeekableInputConstrainedWrapper;
import com.softwareimaging.jpdf.core.pobjects.filters.*;
import com.softwareimaging.jpdf.core.pobjects.graphics.*;
import com.softwareimaging.jpdf.core.util.Library;
import com.softwareimaging.jpdf.core.util.TemporaryFileStreamManager;
import com.softwareimaging.jpdf.util.Hashtable;

import java.io.*;
import java.util.Vector;
import com.softwareimaging.logger.Logger;

/**
 * The Stream class is responsible for decoding stream contents and returning
 * either an images object or a byte array depending on the content. The Stream
 * object worker method is decode which is responsible for decoding the content
 * stream, which is if the first step of the rendering process. Once a Stream is
 * decoded it is either returned as an image object or a byte array that is then
 * processed further by the ContentParser.
 * 
 * @since 1.0
 */
public class Stream extends Dictionary {

	// original byte stream that has not been decoded
	private SeekableInputConstrainedWrapper streamInput;

	public boolean isCCITTFaxDecodeWithoutEncodedByteAlign = false;
	public int CCITTFaxDecodeColumnWidthMismatch = 0;

	// reference of stream, needed for encryption support
	private Reference pObjectReference = null;

	// Log stream reader
	private ILogicalStreamReader logStreamReader = null;

	/**
	 * Save the parameters which would be used to call stream.getImageLine, so
	 * that we can call this from within the BufferedImage class.
	 */
	private Color fill;
	private Resources resources;

	/**
	 * Create a new instance of a Stream.
	 * 
	 * @param l
	 *            library containing a hash of all document objects
	 * @param h
	 *            hashtable of parameters specific to the Stream object.
	 * @param streamInputWrapper
	 *            Accessor to stream byte data
	 */
	public Stream(Library l, Hashtable h, SeekableInputConstrainedWrapper streamInputWrapper) {
		super(l, h);
		streamInput = streamInputWrapper;
	}

	/**
	 * Sets the PObject referece for this stream. The reference number and
	 * generation is need by the encryption algorithm.
	 */
	public void setPObjectReference(Reference reference) {
		pObjectReference = reference;
	}

	/**
	 * Gets the parent PObject reference for this stream.
	 * 
	 * @return Reference number of parent PObject.
	 * @see #setPObjectReference(com.softwareimaging.jpdf.core.pobjects.Reference)
	 */
	public Reference getPObjectReference() {
		return pObjectReference;
	}

	/**
	 * Get a stream reader implementation on the underlying stream
	 * 
	 * @return stream reader on to underlying data
	 */
	public ILogicalStreamReader getStreamReader() {
		if (logStreamReader != null) {
			try {
				// Reset offset to start
				logStreamReader.Seek(SeekOrigin.Begin, 0);
			} catch (FileHandlerNotInitializedException e) {
				Logger.outputStackTrace(e);

				// Try to recreate the stream
				logStreamReader = null;
			}
		}
		if (logStreamReader == null) {
			logStreamReader = new LogicalReaderFromByteArray(getBytes());
		}
		return logStreamReader;
	}

	/**
	 * Get a file based stream reader implementation on the underlying stream
	 * 
	 * @param raf
	 *            true for use random acscess file stream
	 * @return stream reader on to underlying data
	 */
	public ILogicalStreamReader getFileStreamReader(boolean raf) {
		if (logStreamReader != null) {
			try {
				// Reset offset to start
				logStreamReader.Seek(SeekOrigin.Begin, 0);
			} catch (FileHandlerNotInitializedException e) {
				Logger.outputStackTrace(e);

				// Try to recreate the stream
				logStreamReader = null;
			}
		}
		if (logStreamReader == null) {
			InputStream in = getInputStreamForDecodedStreamBytes();
			if (in != null) {
				if (raf) {
					logStreamReader = InputStreamHelper.getRandomAccessFileBasedReader(in);
				} else {
					logStreamReader = InputStreamHelper.getFileBasedReader(in);
				}
				TemporaryFileStreamManager.addReader(logStreamReader);
				try {
					if (in != null) {
						in.close();
					}
				} catch (IOException e) {
				}
			}
		}
		return logStreamReader;
	}

	boolean isImageSubtype() {
		Object subtype = library.getObject(entries, "Subtype");
		return subtype != null && subtype.equals("Image");
	}

	/**
	 * Utility Method to check if the <code>memoryNeeded</code> can be
	 * allocated, and if not, try and free up the needed amount of memory
	 * 
	 * @param memoryNeeded
	 */
	private boolean checkMemory(int memoryNeeded) {
		return library.memoryManager.checkMemory(memoryNeeded);
	}

	/**
	 * Utility method for decoding the byte stream using the decode algorithem
	 * specified by the filter parameter
	 * <p/>
	 * The memory manger is called every time a stream is being decoded with an
	 * estimated size of the decoded stream. Because many of the Filter
	 * algorithms use compression, further research must be done to try and find
	 * the average amount of memory used by each of the algorithms.
	 */
	public InputStream getInputStreamForDecodedStreamBytes() {
		// Make sure that the stream actually has data to decode, if it doesn't
		// make it null and return.
		if (streamInput == null || streamInput.getLength() < 1) {
			return null;
		}

		long streamLength = streamInput.getLength();
		int memoryNeeded = (int) streamLength;
		streamInput.prepareForCurrentUse();
		InputStream input = streamInput;

		int bufferSize = Math.min(Math.max((int) streamLength, 64), 16 * 1024);
		input = new java.io.BufferedInputStream(input, bufferSize);

		if (library.securityManager != null) {
			input = library.getSecurityManager().getEncryptionInputStream(getPObjectReference(),
					library.getSecurityManager().getDecryptionKey(), input, true);
		}

		// Get the filter name for the encoding type, which can be either
		// a Name or Vector.
		Vector filterNames = getFilterNames();
		if (filterNames == null)
			return input;

		// Decode the stream data based on the filter names.
		// Loop through the filterNames and apply the filters in the order
		// in which they where found.
		for (int i = 0; i < filterNames.size(); i++) {
			// grab the name of the filter
			String filterName = filterNames.elementAt(i).toString();
			// System.out.println("  Decoding: " + filterName);

			if (filterName.equals("FlateDecode") || filterName.equals("/Fl")
					|| filterName.equals("Fl")) {
				input = new FlateDecode(library, entries, input);
				memoryNeeded *= 2;
			} else if (filterName.equals("LZWDecode") || filterName.equals("/LZW")
					|| filterName.equals("LZW")) {
				input = new LZWDecode(new BitStream(input), library, entries);
				memoryNeeded *= 2;
			} else if (filterName.equals("ASCII85Decode") || filterName.equals("/A85")
					|| filterName.equals("A85")) {
				input = new ASCII85Decode(input);
				memoryNeeded *= 2;
			} else if (filterName.equals("ASCIIHexDecode") || filterName.equals("/AHx")
					|| filterName.equals("AHx")) {
				input = new ASCIIHexDecode(input);
				memoryNeeded /= 2;
			} else if (filterName.equals("RunLengthDecode") || filterName.equals("/RL")
					|| filterName.equals("RL")) {
				input = new RunLengthDecode(input);
				memoryNeeded *= 2;
			} else if (filterName.equals("CCITTFaxDecode") || filterName.equals("/CCF")
					|| filterName.equals("CCF")) {
				// Leave empty so our else clause works
			} else if (filterName.equals("DCTDecode") || filterName.equals("/DCT")
					|| filterName.equals("DCT")) {
				// Leave empty so our else clause works
			} else if (filterName.equals("JPXDecode") || filterName.equals("JBIG2Decode")) {
				// We don't currently support these.
				if (input != null) {
					try {
						input.close();
						input = null;
					} catch (IOException e) {
						Logger.outputWarning("Problem closing stream for unsupported JPXDecode");
					}
				}
				return null;
			} else {
				if (Logger.areWarningsSupported()) {
					Logger.outputWarning("UNSUPPORTED:" + filterName + " " + entries);
				}
			}
		}

		checkMemory(memoryNeeded);

		return input;
	}

	private byte[] getDecodedStreamBytes() {
		InputStream input = getInputStreamForDecodedStreamBytes();
		ConservativeSizingByteArrayOutputStream out = null;
		byte[] ret = null;
		if (input == null)
			return null;
		try {
			int outLength = Math.max(1024, (int) streamInput.getLength());
			out = new ConservativeSizingByteArrayOutputStream(outLength, library.memoryManager);
			InputStreamHelper.copyStream(input, out);
			ret = out.toByteArray();
		} catch (IOException e) {
			Logger.outputError("Problem decoding stream bytes: ", e);
		} finally {
			try {
				if (out != null) {
					out.close();
				}
				if (input != null) {
					input.close();
				}
			} catch (IOException e) {
				Logger.outputStackTrace(e);
			}
		}

		return ret;
	}

	public boolean shouldUseCCITTFaxDecode() {
		Vector filterNames = getFilterNames();
		if (filterNames == null)
			return false;
		for (int i = 0; i < filterNames.size(); i++) {
			String filterName = filterNames.elementAt(i).toString();
			if (filterName.equals("CCITTFaxDecode") || filterName.equals("/CCF")
					|| filterName.equals("CCF")) {
				return true;
			}
		}
		return false;
	}

	private Vector getFilterNames() {
		Vector filterNames = null;
		Object o = library.getObject(entries, "Filter");
		if (o instanceof Name) {
			filterNames = new Vector();
			filterNames.addElement(o);
		} else if (o instanceof Vector) {
			filterNames = (Vector) o;
		}
		return filterNames;
	}

	/**
	 * Despose of references to images and decoded byte streams. Memory
	 * optimization
	 */
	public void dispose(boolean cache) {
		if (streamInput != null) {
			if (!cache) {
				try {
					streamInput.dispose();
				} catch (IOException e) {
					Logger.outputError("Error disposing stream.", e);
				}
				streamInput = null;
			}
		}
	}

	/**
	 * CCITT fax decode algorithm.
	 * 
	 * @param in
	 *            stream to decode
	 * @return decoded stream
	 */
	public byte[] ccittfaxDecode(InputStream in) {
		// get decode parameters from stream properties
		Hashtable decodeparms = library.getDictionary(entries, "DecodeParms");
		isCCITTFaxDecodeWithoutEncodedByteAlign = !isCCITFaxByteAligned(decodeparms);

		// default value is always false
		float k = library.getFloat(decodeparms, "K");
		boolean blackIs1 = getBlackIs1(library, decodeparms);
		int columns = library.getInt(decodeparms, "Columns");
		int width = library.getInt(entries, "Width");
		int height = library.getInt(entries, "Height");

		// setup streams based on stream properties
		int memoryNeeded = width * height / 8;
		ByteArrayOutputStream out = new ByteArrayOutputStream(memoryNeeded);

		if (k < 0) {
			CCITTFax.Group4Decode(in, out, columns, blackIs1);
		}
		try {
			out.close();
			out.flush();
		} catch (IOException e) {
			Logger.outputStackTrace(e);
		}
		if (columns > width) {
			CCITTFaxDecodeColumnWidthMismatch = columns - width;
		} else {
			CCITTFaxDecodeColumnWidthMismatch = 0;
		}
		return out.toByteArray();
	}

	/**
	 * Check library for byte aligned entry
	 * 
	 * @param decodeparms
	 *            Decode parameters
	 * @return true if the CCITFax data is byte aligned
	 */
	public boolean isCCITFaxByteAligned(Hashtable decodeparms) {
		boolean encodedByteAlign = false;
		Object encodedByteAlignObject = library.getObject(decodeparms, "EncodedByteAlign");
		if (encodedByteAlignObject instanceof Boolean) {
			encodedByteAlign = (Boolean) encodedByteAlignObject;
		}
		return encodedByteAlign;
	}

	/**
	 * Gets the decoded Byte stream of the Stream object.
	 * 
	 * @return decoded Byte stream
	 */
	public byte[] getBytes() {
		byte[] data = getDecodedStreamBytes();
		if (data == null)
			data = new byte[0];
		return data;
	}

	/**
	 * Gets the image object for the given resource.
	 * 
	 * @param fill
	 *            color value of image
	 * @param resources
	 *            resouces containing image reference
	 * 
	 * @return new image object containing the dimensions of the image only, or
	 *         null if we don't handle this stream coding
	 */
	public BufferedImage getImage(Color fill, Resources resources) {
		// get dimension of image stream
		int width = library.getInt(entries, "Width");
		int height = library.getInt(entries, "Height");
		this.fill = fill;
		this.resources = resources;
		return new BufferedImage(width, height, library.getInt(entries, "BitsPerComponent"),
				isImageMask(), fill, this);
	}

	/**
	 * Get associated colour space
	 * 
	 * @return Colour space
	 */
	public PColorSpace getColorSpace() {
		PColorSpace cs = null;
		Object o = library.getObject(entries, "ColorSpace");
		if (resources != null) {
			cs = resources.getColorSpace(o);
		}
		// colourSpace = PColorSpace.getColorSpace(library, o);
		// assume b&w image is no colour space
		if (cs == null) {
			cs = new DeviceGray(library, null);
		}

		return cs;
	}

	/**
	 * Get decode entries
	 * 
	 * @return Decode entries
	 */
	public Vector getDecode() {
		Vector decode = (Vector) library.getObject(entries, "Decode");
		if (decode == null) {
			decode = new Vector();
			decode.addElement(new Float(0));
			decode.addElement(new Float(1));
		}
		return decode;
	}

	/**
	 * Get associated SMask or Mask if present
	 * 
	 * @return BufferedImage object encapsulating SMask or Mask
	 */
	public BufferedImage getMaskImage() {
		Object maskObj = library.getObject(entries, "SMask");
		BufferedImage maskImage = null;
		if (maskObj instanceof Stream) {
			Stream smaskStream = (Stream) maskObj;
			if (smaskStream.isImageSubtype()) {
				maskImage = smaskStream.getImage(fill, resources);
			}
		} else {
			maskObj = library.getObject(entries, "Mask");
			if (maskObj instanceof Stream) {
				Stream imaskStream = (Stream) maskObj;
				if (imaskStream.isImageSubtype()) {
					maskImage = imaskStream.getImage(fill, resources);
				}
			}
		}

		return maskImage;
	}

	/**
	 * Get Mask Colour values if present
	 * 
	 * @return Colour values to be masked out
	 */
	public Vector getMaskColourKeys() {
		Object maskObj = library.getObject(entries, "Mask");
		if (maskObj instanceof Vector) {
			return (Vector) maskObj;
		}
		return null;
	}

	/**
	 * Does the image have an ImageMask.
	 */
	public boolean isImageMask() {
		Object o = library.getObject(entries, "ImageMask");
		return (o != null) ? (o.toString().equals("true") ? true : false) : false;
	}

	public boolean getBlackIs1(Library library, Hashtable decodeParmsDictionary) {
		Boolean blackIs1 = getBlackIs1OrNull(library, decodeParmsDictionary);
		if (blackIs1 != null)
			return blackIs1.booleanValue();
		return false;
	}

	/**
	 * If BlackIs1 was not specified, then return null, instead of the default
	 * value of false, so we can tell if it was given or not
	 */
	public Boolean getBlackIs1OrNull(Library library, Hashtable decodeParmsDictionary) {
		Object blackIs1Obj = library.getObject(decodeParmsDictionary, "BlackIs1");
		if (blackIs1Obj != null) {
			if (blackIs1Obj instanceof Boolean) {
				return (Boolean) blackIs1Obj;
			} else if (blackIs1Obj instanceof String) {
				String blackIs1String = (String) blackIs1Obj;
				if (blackIs1String.equalsIgnoreCase("true"))
					return Boolean.TRUE;
				else if (blackIs1String.equalsIgnoreCase("t"))
					return Boolean.TRUE;
				else if (blackIs1String.equals("1"))
					return Boolean.TRUE;
				else if (blackIs1String.equalsIgnoreCase("false"))
					return Boolean.FALSE;
				else if (blackIs1String.equalsIgnoreCase("f"))
					return Boolean.FALSE;
				else if (blackIs1String.equals("0"))
					return Boolean.FALSE;
			}
		}
		return null;
	}

	/**
	 * Return a string description of the object. Primarily used for debugging.
	 */
	public String toString() {
		StringBuffer sb = new StringBuffer(64);
		sb.append("STREAM= ");
		sb.append(entries);
		if (getPObjectReference() != null) {
			sb.append("  ");
			sb.append(getPObjectReference());
		}
		return sb.toString();
	}
}
