/*
 * :tabSize=4:indentSize=4:noTabs=false:
 * :folding=explicit:collapseFolds=1:
 *
 * Copyright (C) 2006 Kazutoshi Satoda
 *
 * This program 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 any later version.
 *
 * This program 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.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 */

package org.gjt.sp.jedit.io;

//{{{ Imports
import java.io.InputStream;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.util.List;
import java.util.ArrayList;
import java.util.zip.GZIPInputStream;

import org.gjt.sp.jedit.jEdit;
import org.gjt.sp.jedit.ServiceManager;
import org.gjt.sp.jedit.bufferio.BufferIORequest;
import org.gjt.sp.util.Log;

import javax.annotation.Nullable;
//}}}

/**
 * Some functions for auto detection of I/O stream properties.
 */
public class AutoDetection
{
	//{{{ getMarkedStream() method
	/**
	 * Returns a marked, rewindable stream.
	 * Calling reset() method rewinds the stream to its beginning.
	 * But reset() can fail if too long bytes were read.
	 */
	public static BufferedInputStream getMarkedStream(InputStream in)
	{
		int bufferSize = BufferIORequest.getByteIOBufferSize();
		BufferedInputStream markable
			= new BufferedInputStream(in, bufferSize);
		assert(markable.markSupported());
		markable.mark(bufferSize);
		return markable;
	} //}}}

	//{{{ isGzipped() method
	/**
	 * Returns wheather the stream is gzipped.
	 * This method reads a few bytes from the sample. So a caller
	 * must take care of mark() to reuse the contents. Wraping the
	 * stream by getMarkedStream() is suitable.
	 */
	public static boolean isGzipped(InputStream sample)
		throws IOException
	{
		int magic1 = GZIPInputStream.GZIP_MAGIC & 0xff;
		int magic2 = (GZIPInputStream.GZIP_MAGIC >> 8) & 0xff;
		return sample.read() == magic1
			&& sample.read() == magic2;
	} //}}}

	//{{{ getEncodingDetectors() method
	/**
	 * Returns the user configured ordered list of encoding detectors.
	 * This method reads property "encodingDetectors".
	 */
	public static List<EncodingDetector> getEncodingDetectors()
	{
		List<EncodingDetector> detectors = new ArrayList<>();
		String propName = "encodingDetectors";
		String selectedDetectors
			= jEdit.getProperty(propName);
		if (selectedDetectors != null && !selectedDetectors.isEmpty())
		{
			for (String name: selectedDetectors.split("\\s+"))
			{
				EncodingDetector service
					= getEncodingDetectorService(name);
				if (service != null)
				{
					detectors.add(service);
				}
				else
				{
					Log.log(Log.ERROR, AutoDetection.class
						, "getEncodingDetectors():"
							+ " No EncodingDetector for the name"
							+ " \"" + name + "\"");
				}
			}
		}
		return detectors;
	} //}}}

	//{{{ getDetectedEncoding() method
	/**
	 * Returns an auto detected encoding from content of markedStream.
	 * This method assumes that markedStream is wrapped by
	 * getMarkedStream() method.
	 */
	public static String getDetectedEncoding(BufferedInputStream markedStream)
		throws IOException
	{
		List<EncodingDetector> detectors = getEncodingDetectors();
		for (EncodingDetector detector: detectors)
		{
			// FIXME: Here the method reset() can fail if the
			// previous detector read more than buffer size of
			// markedStream.
			markedStream.reset();
			// Wrap once more so that calling mark()
			// or reset() in detectEncoding() don't
			// alter the mark position of markedStream.
			String detected = detector.detectEncoding(
				new BufferedInputStream(markedStream));
			if (detected != null)
			{
				return detected;
			}
		}
		return null;
	} //}}}

	//{{{ class Result
	/**
	 * An utility class to hold the result of some auto detections.
	 */
	public static class Result
	{
		//{{{ Constructor
		/**
		 * Do some auto detection for a stream and hold the
		 * result in this instance.
		 * @param in the stream
		 */
		public Result(InputStream in) throws IOException
		{
			BufferedInputStream marked = getMarkedStream(in);

			gzipped = isGzipped(marked);
			if (gzipped)
			{
				marked.reset();
				marked = getMarkedStream(
					new GZIPInputStream(marked));
			}

			marked.reset();
			encoding = AutoDetection.getDetectedEncoding(marked);

			markedStream = marked;
		} //}}}

		//{{{ getRewindedStream()
		/**
		 * Returns the stream which can be read the contents of
		 * the original stream.
		 * Some bytes ware read from original stream for auto
		 * detections. But they are rewinded at this method.
		 */
		public BufferedInputStream getRewindedStream()
			throws IOException
		{
			markedStream.reset();
			return markedStream;
		} //}}}

		//{{{ streamIsGzipped()
		/**
		 * Returns true if the stream is gzipped.
		 */
		public boolean streamIsGzipped()
		{
			return gzipped;
		} //}}}

		//{{{ getDetectedEncoding()
		/**
		 * Returns the auto detected encoding.
		 * Returns null if no encoding was detected.
		 */
		public String getDetectedEncoding()
		{
			return encoding;
		} //}}}

		//{{{ Private members
		private final BufferedInputStream markedStream;
		private final boolean gzipped;
		private final String encoding;
		//}}}
	} //}}}

	//{{{ Private members
	/**
	 * Returns a service of EncodingDetector for name.
	 */
	@Nullable
	private static EncodingDetector getEncodingDetectorService(String name)
	{
		String serviceClass = "org.gjt.sp.jedit.io.EncodingDetector";
		Object service = ServiceManager.getService(serviceClass, name);
		if (service instanceof EncodingDetector)
		{
			return (EncodingDetector)service;
		}
		else
		{
			return null;
		}
	}
	//}}}
}
