/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (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.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.sanselan.formats.jpeg.xmp;

import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;

import org.apache.sanselan.ImageReadException;
import org.apache.sanselan.ImageWriteException;
import org.apache.sanselan.common.BinaryFileParser;
import org.apache.sanselan.common.byteSources.ByteSource;
import org.apache.sanselan.formats.jpeg.JpegConstants;
import org.apache.sanselan.formats.jpeg.JpegUtils;
import org.apache.sanselan.formats.jpeg.iptc.IPTCParser;

/**
 * Interface for Exif write/update/remove functionality for Jpeg/JFIF images.
 * <p>
 * <p>
 * See the source of the XmpXmlUpdateExample class for example usage.
 * 
 * @see org.apache.sanselan.sampleUsage.WriteXmpXmlExample
 */
public class JpegRewriter extends BinaryFileParser implements JpegConstants
{
	private static final int JPEG_BYTE_ORDER = BYTE_ORDER_NETWORK;

	/**
	 * Constructor. to guess whether a file contains an image based on its file
	 * extension.
	 */
	public JpegRewriter()
	{
		setByteOrder(JPEG_BYTE_ORDER);
	}

	protected static class JFIFPieces
	{
		public final List pieces;
		public final List segmentPieces;

		public JFIFPieces(final List pieces, final List segmentPieces)
		{
			this.pieces = pieces;
			this.segmentPieces = segmentPieces;
		}

	}

	protected abstract static class JFIFPiece
	{
		protected abstract void write(OutputStream os) throws IOException;
		
		public String toString()
		{
			return "[" + this.getClass().getName() + "]";
		}
	}

	protected static class JFIFPieceSegment extends JFIFPiece
	{
		public final int marker;
		public final byte markerBytes[];
		public final byte segmentLengthBytes[];
		public final byte segmentData[];

		public JFIFPieceSegment(final int marker, final byte[] segmentData)
		{
			this(marker, int2ToByteArray(marker, JPEG_BYTE_ORDER),
					int2ToByteArray(segmentData.length + 2, JPEG_BYTE_ORDER),
					segmentData);
		}

		public JFIFPieceSegment(final int marker, final byte[] markerBytes,
				final byte[] segmentLengthBytes, final byte[] segmentData)
		{
			this.marker = marker;
			this.markerBytes = markerBytes;
			this.segmentLengthBytes = segmentLengthBytes;
			this.segmentData = segmentData;
		}

		public String toString()
		{
			return "[" + this.getClass().getName() + " (0x" + Integer.toHexString(marker) + ")]";
		}

		protected void write(OutputStream os) throws IOException
		{
			os.write(markerBytes);
			os.write(segmentLengthBytes);
			os.write(segmentData);
		}

		public boolean isApp1Segment()
		{
			return marker == JPEG_APP1_Marker;
		}

		public boolean isAppSegment()
		{
			return marker >= JPEG_APP0_Marker && marker <= JPEG_APP15_Marker;
		}

		public boolean isExifSegment()
		{
			if (marker != JPEG_APP1_Marker)
				return false;
			if (!byteArrayHasPrefix(segmentData, EXIF_IDENTIFIER_CODE))
				return false;
			return true;
		}

		public boolean isPhotoshopApp13Segment()
		{
			if (marker != JPEG_APP13_Marker)
				return false;
			if (!new IPTCParser().isPhotoshopJpegSegment(segmentData))
				return false;
			return true;
		}

		public boolean isXmpSegment()
		{
			if (marker != JPEG_APP1_Marker)
				return false;
			if (!byteArrayHasPrefix(segmentData, XMP_IDENTIFIER))
				return false;
			return true;
		}

	}

	protected static class JFIFPieceImageData extends JFIFPiece
	{
		public final byte markerBytes[];
		public final byte imageData[];

		public JFIFPieceImageData(final byte[] markerBytes,
				final byte[] imageData)
		{
			super();
			this.markerBytes = markerBytes;
			this.imageData = imageData;
		}

		protected void write(OutputStream os) throws IOException
		{
			os.write(markerBytes);
			os.write(imageData);
		}
	}

	protected JFIFPieces analyzeJFIF(ByteSource byteSource)
			throws ImageReadException, IOException
	// , ImageWriteException
	{
		final ArrayList pieces = new ArrayList();
		final List segmentPieces = new ArrayList();

		JpegUtils.Visitor visitor = new JpegUtils.Visitor() {
			// return false to exit before reading image data.
			public boolean beginSOS()
			{
				return true;
			}

			public void visitSOS(int marker, byte markerBytes[],
					byte imageData[])
			{
				pieces.add(new JFIFPieceImageData(markerBytes, imageData));
			}

			// return false to exit traversal.
			public boolean visitSegment(int marker, byte markerBytes[],
					int segmentLength, byte segmentLengthBytes[],
					byte segmentData[]) throws ImageReadException, IOException
			{
				JFIFPiece piece = new JFIFPieceSegment(marker, markerBytes,
						segmentLengthBytes, segmentData);
				pieces.add(piece);
				segmentPieces.add(piece);

				return true;
			}
		};

		new JpegUtils().traverseJFIF(byteSource, visitor);

		return new JFIFPieces(pieces, segmentPieces);
	}

	private static interface SegmentFilter
	{
		public boolean filter(JFIFPieceSegment segment);
	}

	private static final SegmentFilter EXIF_SEGMENT_FILTER = new SegmentFilter() {
		public boolean filter(JFIFPieceSegment segment)
		{
			return segment.isExifSegment();
		}
	};

	private static final SegmentFilter XMP_SEGMENT_FILTER = new SegmentFilter() {
		public boolean filter(JFIFPieceSegment segment)
		{
			return segment.isXmpSegment();
		}
	};

	private static final SegmentFilter PHOTOSHOP_APP13_SEGMENT_FILTER = new SegmentFilter() {
		public boolean filter(JFIFPieceSegment segment)
		{
			return segment.isPhotoshopApp13Segment();
		}
	};

	protected List removeXmpSegments(List segments)
	{
		return filterSegments(segments, XMP_SEGMENT_FILTER);
	}

	protected List removePhotoshopApp13Segments(List segments)
	{
		return filterSegments(segments, PHOTOSHOP_APP13_SEGMENT_FILTER);
	}

	protected List findPhotoshopApp13Segments(List segments)
	{
		return filterSegments(segments, PHOTOSHOP_APP13_SEGMENT_FILTER, true);
	}

	protected List removeExifSegments(List segments)
	{
		return filterSegments(segments, EXIF_SEGMENT_FILTER);
	}

	protected List filterSegments(List segments, SegmentFilter filter)
	{
		return filterSegments(segments, filter, false);
	}

	protected List filterSegments(List segments, SegmentFilter filter,
			boolean reverse)
	{
		List result = new ArrayList();

		for (int i = 0; i < segments.size(); i++)
		{
			JFIFPiece piece = (JFIFPiece) segments.get(i);
			if (piece instanceof JFIFPieceSegment)
			{
				if (filter.filter((JFIFPieceSegment) piece) ^ !reverse)
					result.add(piece);
			} else if(!reverse)
				result.add(piece);
		}

		return result;
	}

	protected List insertBeforeFirstAppSegments(List segments, List newSegments)
			throws ImageWriteException
	{
		int firstAppIndex = -1;
		for (int i = 0; i < segments.size(); i++)
		{
			JFIFPiece piece = (JFIFPiece) segments.get(i);
			if (!(piece instanceof JFIFPieceSegment))
				continue;

			JFIFPieceSegment segment = (JFIFPieceSegment) piece;
			if (segment.isAppSegment())
			{
				if (firstAppIndex == -1)
					firstAppIndex = i;
			}
		}

		List result = new ArrayList(segments);
		if (firstAppIndex == -1)
			throw new ImageWriteException("JPEG file has no APP segments.");
		result.addAll(firstAppIndex, newSegments);
		return result;
	}

	protected List insertAfterLastAppSegments(List segments, List newSegments)
			throws ImageWriteException
	{
		int lastAppIndex = -1;
		for (int i = 0; i < segments.size(); i++)
		{
			JFIFPiece piece = (JFIFPiece) segments.get(i);
			if (!(piece instanceof JFIFPieceSegment))
				continue;

			JFIFPieceSegment segment = (JFIFPieceSegment) piece;
			if (segment.isAppSegment())
				lastAppIndex = i;
		}

		List result = new ArrayList(segments);
		if (lastAppIndex == -1)
		{
			if(segments.size()<1)
				throw new ImageWriteException("JPEG file has no APP segments.");
			result.addAll(1, newSegments);
		}
		else
		result.addAll(lastAppIndex + 1, newSegments);
		
		return result;
	}

	protected void writeSegments(OutputStream os, List segments)
			throws ImageWriteException, IOException
	{
		try
		{
			os.write(SOI);

			for (int i = 0; i < segments.size(); i++)
			{
				JFIFPiece piece = (JFIFPiece) segments.get(i);
				piece.write(os);
			}
			os.close();
			os = null;
		} finally
		{
			try
			{
				if (os != null)
					os.close();
			} catch (Exception e)
			{
				// swallow exception; already in the context of an exception.
			}
		}
	}

	// private void writeSegment(OutputStream os, JFIFPieceSegment piece)
	// throws ImageWriteException, IOException
	// {
	// byte markerBytes[] = convertShortToByteArray(JPEG_APP1_Marker,
	// JPEG_BYTE_ORDER);
	// if (piece.segmentData.length > 0xffff)
	// throw new JpegSegmentOverflowException("Jpeg segment is too long: "
	// + piece.segmentData.length);
	// int segmentLength = piece.segmentData.length + 2;
	// byte segmentLengthBytes[] = convertShortToByteArray(segmentLength,
	// JPEG_BYTE_ORDER);
	//
	// os.write(markerBytes);
	// os.write(segmentLengthBytes);
	// os.write(piece.segmentData);
	// }

	public static class JpegSegmentOverflowException extends
			ImageWriteException
	{
		public JpegSegmentOverflowException(String s)
		{
			super(s);
		}
	}

}