/*******************************************************************************
 * Copyright (c) 2006, 2010 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Initial Contributors:
 * The following IBM employees contributed to the Remote System Explorer
 * component that contains this file: David McKnight, Kushal Munir,
 * Michael Berger, David Dykstal, Phil Coulthard, Don Yantzi, Eric Simpson,
 * Emily Bruner, Mazen Faraj, Adrian Storisteanu, Li Ding, and Kent Hawley.
 *
 * Contributors:
 * David McKnight   (IBM) [215847]SystemEncodingUtil needs to convert to unsigned when checking xml file
 * David McKnight     (IBM)      - [229610] [api] File transfers should use workspace text file encoding
 * David McKnight (IBM)  - [283033] remoteFileTypes extension point should include "xml" type
 *******************************************************************************/

package org.eclipse.rse.services.clientserver;

import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;

/**
 * A singleton class that contains useful methods related to encodings.
 */
public class SystemEncodingUtil {

	private static SystemEncodingUtil instance;
	public static String ENCODING_UTF_8 = "UTF-8"; //$NON-NLS-1$

	private DefaultEncodingProvider _defaultEncodingProvider = new DefaultEncodingProvider();

	/**
	 * Constructor to create the utility class.
	 */
	private SystemEncodingUtil() {
		super();
	}

	/**
	 * Returns the singleton instance of the utility class.
	 * @return the singleton instance.
	 */
	public static SystemEncodingUtil getInstance() {

		if (instance == null) {
			instance = new SystemEncodingUtil();
		}

		return instance;
	}



	   /**
	    * Provider for the default encodings that RSE uses.
	    * Clients may subclass this class, and override methods.
	    * @since org.eclipse.rse.services 3.0
	    */
	   public static class DefaultEncodingProvider {

	      /**
	       * Return the default encoding for local workspace resources.
	       * Clients may override.
	       * @return String the local default encoding.
	       */
	      public String getLocalDefaultEncoding() {
	          return System.getProperty("file.encoding"); //$NON-NLS-1$
	      }
	      

	    /**
		 * @since 3.2
		 */
	    public boolean isXML(String filePath) {
			int index = filePath.lastIndexOf("."); //$NON-NLS-1$

			// check if there is a "."
			if (index == -1) {
				return false;
			}
			else {

				// check if the name ends with "."
				if (index == filePath.length() - 1) {
					return false;
				}
				else {
					String extension = filePath.substring(index+1);

					if (extension.equalsIgnoreCase("xml") || extension.equalsIgnoreCase("xmi")) { //$NON-NLS-1$ //$NON-NLS-2$
						return true;
					}
					else {
						return false;
					}
				}
			}
	      }
	   }

   	/**
	 * Change the default encoding provider.
	 * 
	 * This is a system-wide change, and clients will not be notified of changed
	 * default encodings due to changing the provider. Therefore, changing the
	 * provider should be done only once during early system startup.
	 * 
	 * @param p the new encoding provider.
	 * @since 3.0
	 */
	public void setDefaultEncodingProvider(DefaultEncodingProvider p) {
	    _defaultEncodingProvider = p;
	}

	/**
	 * Returns the local default encoding as provided by the default encoding
	 * provider. This method should be called after RSE startup is complete in
	 * order to get the proper default workspace encoding.
	 *
	 * @return the local default encoding
	 * @since 3.0
	 */
	public String getLocalDefaultEncoding() {
	   return _defaultEncodingProvider.getLocalDefaultEncoding();
	}


	/**
	 * Gets the encoding of the environment. This is the encoding being used by the JVM,
	 * which by default is the machine encoding, unless changed explicitly.
	 * @return the evironment encoding.
	 */
	public String getEnvironmentEncoding() {
		return System.getProperty("file.encoding"); //$NON-NLS-1$
	}

	/**
	 * Returns whether the file is an XML file.
	 * @param filePath the file path.
	 * @return <code>true</code> if the file is an XML file, <code>false</code> otherwise.
	 */
	public boolean isXML(String filePath) {
		return _defaultEncodingProvider.isXML(filePath);
	}

	/**
	 * Gets the encoding of an XML file.
	 * @param filePath the file path.
	 * @return the encoding, or <code>null</code> if the encoding could not be determined.
	 */
	public String getXMLFileEncoding(String filePath) throws IOException {

		String encoding = null;

		// this is an implementation of the encoding detection algorithm
		// as specified in Appendix F of the XML specification

		FileInputStream stream = new FileInputStream(filePath);

		// try to get the encoding if the file starts with a BOM
		String encodingGuess = getEncodingFromBOM(stream);

		stream.close();

		// if no BOM, read in bytes corresponding to the first four chars in the header, i.e. "<?xm"
		// and try to determine the encoding from that
		if (encodingGuess == null) {

			stream = new FileInputStream(filePath);

			byte[] tempSigned = new byte[4];

			stream.read(tempSigned);

			stream.close();


			// convert to unsigned
			int[] temp = new int[4];
			for (int i = 0; i < 4; i++){
				temp[i] = 0xFF & tempSigned[i];
			}


			// UTF-8, ISO 646, ASCII, some part of ISO 8859, Shift-JIS, EUC, or any other 7-bit,
			// 8-bit, or mixed-width encoding which ensures that the characters of ASCII have their
			// normal positions, width, and values; the actual encoding declaration must be read to
			// detect which of these applies, but since all of these encodings use the same bit patterns
			// for the relevant ASCII characters, the encoding declaration itself may be read reliably
			if (temp[0] == 0x3C && temp[1] == 0x3F && temp[2] == 0x78 && temp[3] == 0x6D) {
				encodingGuess = SystemEncodingUtil.ENCODING_UTF_8;
			}

			// UCS-4 or other encoding with a 32-bit code unit and ASCII characters encoded as
			// ASCII values, in respectively big-endian (1234), little-endian (4321) and two
			// unusual byte orders (2143 and 3412). The encoding declaration must be read to
			// determine which of UCS-4 or other supported 32-bit encodings applies.

			// UCS-4, big-endian order (1234 order)
			/* DKM - encodingGuess can only be null
			else if (temp[0] == 0x00 && temp[1] == 0x00 && temp[2] == 0x00 && temp[3] == 0x3C) {
				encodingGuess = null;
			}
			// UCS-4, little-endian order (4321 order)
			else if (temp[0] == 0x3C && temp[1] == 0x00 && temp[2] == 0x00 && temp[3] == 0x00) {
				encodingGuess = null;
			}
			// UCS-4, unusual octet order (2143)
			else if (temp[0] == 0x00 && temp[1] == 0x00 && temp[2] == 0x3C && temp[3] == 0x00) {
				encodingGuess = null;
			}
			// UCS-4, unusual octet order (3412)
			else if (temp[0] == 0x00 && temp[1] == 0x3C && temp[2] == 0x00 && temp[3] == 0x00) {
				encodingGuess = null;
			}
			*/

			// UTF-16BE or big-endian ISO-10646-UCS-2 or other encoding with a 16-bit code unit
			// in big-endian order and ASCII characters encoded as ASCII values (the encoding
			// declaration must be read to determine which)
			else if (temp[0] == 0x00 && temp[1] == 0x3C && temp[2] == 0x00 && temp[3] == 0x3F) {
				encodingGuess = "UnicodeBigUnmarked"; //$NON-NLS-1$
			}


			// UTF-16LE or little-endian ISO-10646-UCS-2 or other encoding with a 16-bit code unit
			// in little-endian order and ASCII characters encoded as ASCII values (the encoding
			// declaration must be read to determine which)
			else if (temp[0] == 0x3C && temp[1] == 0x00 && temp[2] == 0x3F && temp[3] == 0x00) {
				encodingGuess = "UnicodeLittleUnmarked"; //$NON-NLS-1$
			}


			// EBCDIC (in some flavor; the full encoding declaration must be read to tell which
			// code page is in use)
			else if (temp[0] == 0x4C && temp[1] == 0x6F && temp[2] == 0xA7 && temp[3] == 0x94) {
				encodingGuess = "Cp037"; //$NON-NLS-1$
			}


			// UTF-8 without an encoding declaration, or else the data stream is mislabeled
			// (lacking a required encoding declaration), corrupt, fragmentary, or enclosed in a
			// wrapper of some kind
			else {

				// From section 4.3.3 of the XML specification:
				// In the absence of information provided by an external transport protocol
				// (e.g. HTTP or MIME), it is an error for an entity including an encoding declaration
				// to be presented to the XML processor in an encoding other than that named in the
				// declaration, or for an entity which begins with neither a Byte Order Mark nor an
				// encoding declaration to use an encoding other than UTF-8. Note that since ASCII is
				// a subset of UTF-8, ordinary ASCII entities do not strictly need an encoding declaration.

				// We'll assume that this is UTF-8 or ASCII encoding without an encoding declaration.
				// Of course, it could also be another encoding that doesn't have an encoding declaration
				// in which case it has violated the XML specification (any encoding beside UTF-8 or UTF-16
				// must specify the character encoding). From section 4.3.3 of the XML specification:
				// In the absence of external character encoding information (such as MIME headers),
				// parsed entities which are stored in an encoding other than UTF-8 or UTF-16 must begin
				// with a text declaration (see 4.3.1 The Text Declaration) containing an encoding declaration.
				encodingGuess = SystemEncodingUtil.ENCODING_UTF_8;
			}
		}

		// if we have a guess, we need to read in the encoding declaration to get the actual encoding
		// the guess tells us the encoding of the family
		if (encodingGuess != null) {

			boolean encodingFound = false;

			FileInputStream inputStream = new FileInputStream(filePath);
			InputStreamReader reader = new InputStreamReader(inputStream, encodingGuess);

			// note that buffer capacity must be 1, otherwise we run into a problem
			// if the XML file has a non UTF-8 encoding and accented characters in the file
			BufferedReader bufReader = new BufferedReader(reader, 1);

			String line = bufReader.readLine();

			while (line != null) {

				int encodingIndex = line.indexOf("encoding"); //$NON-NLS-1$

				// look for the encoding attribute
				if (encodingIndex != -1) {

					// we look in the same line first
					boolean sameLine = true;

					boolean doubleQuoteFound = false;
					boolean singleQuoteFound = false;

					while (line != null) {

						// now look for the begin quote, which does not have to be
						// on the same line as the encoding attribute declaration
						int beginQuote = -1;

						// search in same line first
						if (sameLine) {

							// look for double quote
							beginQuote = line.indexOf('\"', encodingIndex+9);

							// if double quote not found, then try single quote
							if (beginQuote == -1) {
								beginQuote = line.indexOf('\'', encodingIndex+9);

								// single quote found, so flag it
								if (beginQuote != -1) {
									singleQuoteFound = true;
								}
							}
							// double quote found, so flag it
							else {
								doubleQuoteFound = true;
							}
						}
						// search in another line now
						else {

							// look for double quote
							beginQuote = line.indexOf('\"');

							// if single quote not found, then try single quote
							if (beginQuote == -1) {
								beginQuote = line.indexOf('\'');

								// single quote found, so flag it
								if (beginQuote != -1) {
									singleQuoteFound = true;
								}
							}
							// double quote found, so flag it
							else {
								doubleQuoteFound = true;
							}
						}

						// if begin quote found, look for endquote which should be on the same line
						if (beginQuote != -1) {
							int endQuote = -1;

							if (doubleQuoteFound) {
								endQuote = line.indexOf('\"', beginQuote+1);
							}
							else if (singleQuoteFound){
								endQuote = line.indexOf('\'', beginQuote+1);
							}

							// if end quote found, encoding is in between begin quote and end quote
							if (endQuote != -1) {
								encoding = line.substring(beginQuote+1, endQuote);
								encodingFound = true;
							}

							break;
						}

						if (sameLine) {
							sameLine = false;
						}

						line = bufReader.readLine();
					}
				}

				if (encodingFound) {
					break;
				}

				line = bufReader.readLine();
			}

			bufReader.close();

			// if the encoding declaration was not found
			if (encoding == null) {

				// check if our initial guess was UTF-8 or UTF-16
				// those do not have to have an encoding declaration
				if (encodingGuess.equals(SystemEncodingUtil.ENCODING_UTF_8) || encodingGuess.startsWith("UnicodeBig") || //$NON-NLS-1$
					encodingGuess.equals("UnicodeLittle")) { //$NON-NLS-1$
					encoding = encodingGuess;
				}
			}
		}

		return encoding;
	}

	/**
	 * Gets the encoding from the Byte Order Mark (BOM).
	 * @param filePath the file path.
	 * @return the encoding, or <code>null</code> if there is no BOM.
	 */
	public String getEncodingFromBOM(String filePath) throws IOException {
		FileInputStream stream = new FileInputStream(filePath);

		String encoding = getEncodingFromBOM(stream);

		stream.close();

		return encoding;
	}

	/**
	 * Gets the encoding from the Byte Order Mark (BOM).
	 * @param filePath the file path.
	 * @return the encoding, or <code>null</code> if there is no BOM.
	 */
	private String getEncodingFromBOM(InputStream stream) throws IOException {

		byte[] bomBytes = new byte[4];

		// read the first three bytes
		stream.read(bomBytes, 0, 3);

		// check if UTF-8 BOM
		if (bomBytes[0] == 0xEF && bomBytes[1] == 0xBB && bomBytes[2] == 0xBF) {
			return SystemEncodingUtil.ENCODING_UTF_8;
		}

		// now read the fourth byte
		stream.read(bomBytes, 3, 1);

		// check if it matches some other encoding BOM

		// UCS-4, big-endian order (1234 order)
		if (bomBytes[0] == 0x00 && bomBytes[1] == 0x00 && bomBytes[2] == 0xFE && bomBytes[3] == 0xFF) {
			return null;
		}
		// UCS-4, little-endian order (4321 order)
		else if (bomBytes[0] == 0xFF && bomBytes[1] == 0xFE && bomBytes[2] == 0x00 && bomBytes[3] == 0x00) {
			return null;
		}
		// UCS-4, unusual octet order (2143)
		else if (bomBytes[0] == 0x00 && bomBytes[1] == 0x00 && bomBytes[2] == 0xFF && bomBytes[3] == 0xFE) {
			return null;
		}
		// UCS-4, unusual octet order (3412)
		else if (bomBytes[0] == 0xFE && bomBytes[1] == 0xFF && bomBytes[2] == 0x00 && bomBytes[3] == 0x00) {
			return null;
		}
		// UTF-16, big-endian order
		else if (bomBytes[0] == 0xFE && bomBytes[1] == 0xFF && !(bomBytes[2] == 0x00 && bomBytes[3] == 0x00)) {
			return "UnicodeBig"; //$NON-NLS-1$
		}
		// UTF-16, little-endian order
		else if (bomBytes[0] == 0xFF && bomBytes[1] == 0xFE && !(bomBytes[2] == 0x00 && bomBytes[3] == 0x00)) {
			return "UnicodeLittle"; //$NON-NLS-1$
		}
		// not a BOM
		else {
			return null;
		}
	}
}
