/*******************************************************************************
 * Copyright (c) 2006, 2015 IBM Corporation and others.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.jdt.internal.compiler.tool;

import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.CoderResult;
import java.nio.charset.CodingErrorAction;
import java.nio.charset.IllegalCharsetNameException;
import java.nio.charset.UnsupportedCharsetException;
import java.util.ArrayList;

import javax.tools.FileObject;

/**
 * Util class that defines helper methods to read class contents with handling of wrong encoding
 */
public final class Util {
	public static String LINE_SEPARATOR = System.getProperty("line.separator"); //$NON-NLS-1$

	public static class EncodingError {
		int position;
		int length;
		public EncodingError(int position, int length) {
			this.position = position;
			this.length = length;
		}

		public String getSource(char[] unitSource) {
			//extra from the source the innacurate     token
			//and "highlight" it using some underneath ^^^^^
			//put some context around too.

			//this code assumes that the font used in the console is fixed size

			//sanity .....
			int startPosition = this.position;
			int endPosition = this.position + this.length - 1;

			if ((startPosition > endPosition)
				|| ((startPosition < 0) && (endPosition < 0))
				|| unitSource.length == 0)
				return "No source available"; //$NON-NLS-1$

			StringBuffer errorBuffer = new StringBuffer();
			errorBuffer.append('\t');

			char c;
			final char SPACE = ' ';
			final char MARK = '^';
			final char TAB = '\t';
			//the next code tries to underline the token.....
			//it assumes (for a good display) that token source does not
			//contain any \r \n. This is false on statements !
			//(the code still works but the display is not optimal !)

			// expand to line limits
			int sourceLength = unitSource.length, begin, end;
			for (begin = startPosition >= sourceLength ? sourceLength - 1 : startPosition; begin > 0; begin--) {
				if ((c = unitSource[begin - 1]) == '\n' || c == '\r') break;
			}
			for (end = endPosition >= sourceLength ? sourceLength - 1 : endPosition ; end+1 < sourceLength; end++) {
				if ((c = unitSource[end + 1]) == '\r' || c == '\n') break;
			}

			// trim left and right spaces/tabs
			while ((c = unitSource[begin]) == ' ' || c == '\t') begin++;
			//while ((c = unitSource[end]) == ' ' || c == '\t') end--; TODO (philippe) should also trim right, but all tests are to be updated

			// copy source
			errorBuffer.append(unitSource, begin, end-begin+1);
			errorBuffer.append(Util.LINE_SEPARATOR).append("\t"); //$NON-NLS-1$

			// compute underline
			for (int i = begin; i <startPosition; i++) {
				errorBuffer.append((unitSource[i] == TAB) ? TAB : SPACE);
			}
			for (int i = startPosition; i <= (endPosition >= sourceLength ? sourceLength - 1 : endPosition); i++) {
				errorBuffer.append(MARK);
			}
			return errorBuffer.toString();
		}
	}
	public static class EncodingErrorCollector {
		ArrayList<EncodingError> encodingErrors = new ArrayList<>();
		FileObject fileObject;
		String encoding;

		public EncodingErrorCollector(FileObject fileObject, String encoding) {
			this.fileObject = fileObject;
			this.encoding = encoding;
		}
		public void collect(int position, int length) {
			this.encodingErrors.add(new EncodingError(position, length));
		}
		public void reportAllEncodingErrors(String string) {
			// this is where the encoding errors should be reported
			char[] unitSource = string.toCharArray();
			for (EncodingError error : this.encodingErrors) {
				System.err.println(this.fileObject.getName() + " Unmappable character for encoding " + this.encoding);//$NON-NLS-1$
				System.err.println(error.getSource(unitSource));
			}
		}
	}

	public static char[] getInputStreamAsCharArray(InputStream stream, int length, String encoding) throws IOException {
		Charset charset = null;
		try {
			charset = Charset.forName(encoding);
		} catch (IllegalCharsetNameException e) {
			System.err.println("Illegal charset name : " + encoding); //$NON-NLS-1$
			return null;
		} catch(UnsupportedCharsetException e) {
			System.err.println("Unsupported charset : " + encoding); //$NON-NLS-1$
			return null;
		}
		CharsetDecoder charsetDecoder = charset.newDecoder();
		charsetDecoder.onMalformedInput(CodingErrorAction.REPLACE).onUnmappableCharacter(CodingErrorAction.REPLACE);
		byte[] contents = org.eclipse.jdt.internal.compiler.util.Util.getInputStreamAsByteArray(stream);
		ByteBuffer byteBuffer = ByteBuffer.allocate(contents.length);
		byteBuffer.put(contents);
		byteBuffer.flip();
		return charsetDecoder.decode(byteBuffer).array();
	}

	public static CharSequence getCharContents(FileObject fileObject, boolean ignoreEncodingErrors, byte[] contents, String encoding) throws IOException {
		if (contents == null) return null;
		Charset charset = null;
		try {
			charset = Charset.forName(encoding);
		} catch (IllegalCharsetNameException e) {
			System.err.println("Illegal charset name : " + encoding); //$NON-NLS-1$
			return null;
		} catch(UnsupportedCharsetException e) {
			System.err.println("Unsupported charset : " + encoding); //$NON-NLS-1$
			return null;
		}
		CharsetDecoder charsetDecoder = charset.newDecoder();
		ByteBuffer byteBuffer = ByteBuffer.allocate(contents.length);
		byteBuffer.put(contents);
		byteBuffer.flip();
		if (ignoreEncodingErrors) {
			charsetDecoder.onMalformedInput(CodingErrorAction.REPLACE).onUnmappableCharacter(CodingErrorAction.REPLACE);
			return charsetDecoder.decode(byteBuffer);
		} else {
			charsetDecoder.onMalformedInput(CodingErrorAction.REPORT).onUnmappableCharacter(CodingErrorAction.REPORT);
			CharBuffer out = CharBuffer.allocate(contents.length);
			CoderResult result = null;
			String replacement = charsetDecoder.replacement();
			int replacementLength = replacement.length();
			EncodingErrorCollector collector = null;
			while (true) {
				result = charsetDecoder.decode(byteBuffer, out, true);
				if (result.isMalformed() || result.isUnmappable()) {
					/* treat the error
					 * The wrong input character is at out.position
					 */
					if (collector == null) {
						collector = new EncodingErrorCollector(fileObject, encoding);
					}
					reportEncodingError(collector, out.position(), result.length());
					if ((out.position() + replacementLength) >= out.capacity()) {
						// resize
						CharBuffer temp = CharBuffer.allocate(out.capacity() * 2);
						out.flip();
						temp.put(out);
						out = temp;
					}
					out.append(replacement);
					byteBuffer.position(byteBuffer.position() + result.length());
					continue;
				}
				if (result.isOverflow()) {
					CharBuffer temp = CharBuffer.allocate(out.capacity() * 2);
					out.flip();
					temp.put(out);
					out = temp;
				} else {
					break;
				}
			}
			out.flip();
			if (collector != null) {
				collector.reportAllEncodingErrors(out.toString());
			}
			return out;
		}
	}

	private static void reportEncodingError(EncodingErrorCollector collector, int position, int length) {
		collector.collect(position, -length);
	}
}

