/*******************************************************************************
 * Copyright (c) 2019 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
 *     Red Hat Inc. - copied to jdt.core.manipulation core class
 *******************************************************************************/
package org.eclipse.jdt.internal.ui.text.correction;

import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.Comparator;

import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.SubProgressMonitor;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IncrementalProjectBuilder;

import org.eclipse.core.filebuffers.FileBuffers;
import org.eclipse.core.filebuffers.ITextFileBuffer;
import org.eclipse.core.filebuffers.ITextFileBufferManager;
import org.eclipse.core.filebuffers.LocationKind;

import org.eclipse.jdt.core.Flags;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.IRegion;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.ToolFactory;
import org.eclipse.jdt.core.compiler.CharOperation;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.AbstractTypeDeclaration;
import org.eclipse.jdt.core.dom.AnonymousClassDeclaration;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.ParameterizedType;
import org.eclipse.jdt.core.dom.VariableDeclarationFragment;
import org.eclipse.jdt.core.dom.rewrite.ASTRewrite;
import org.eclipse.jdt.core.util.IClassFileReader;
import org.eclipse.jdt.core.util.IFieldInfo;
import org.eclipse.jdt.core.util.IInnerClassesAttribute;
import org.eclipse.jdt.core.util.IInnerClassesAttributeEntry;
import org.eclipse.jdt.core.util.IMethodInfo;

import org.eclipse.jdt.internal.core.manipulation.JavaManipulationPlugin;
import org.eclipse.jdt.internal.corext.fix.AbstractSerialVersionOperationCore;
import org.eclipse.jdt.internal.corext.fix.LinkedProposalModelCore;

/**
 * Proposal for a hashed serial version id.
 *
 * @since 3.1
 */
public final class SerialVersionHashOperationCore extends AbstractSerialVersionOperationCore {

	private static final String STATIC_CLASS_INITIALIZER= "<clinit>"; //$NON-NLS-1$

	public static Long calculateSerialVersionId(ITypeBinding typeBinding, final IProgressMonitor monitor) throws CoreException, IOException {
		try {
			IFile classfileResource= getClassfile(typeBinding);
			if (classfileResource == null)
				return null;

			try (InputStream contents= classfileResource.getContents()) {
				IClassFileReader cfReader= ToolFactory.createDefaultClassFileReader(contents, IClassFileReader.ALL);
				if (cfReader != null) {
					return calculateSerialVersionId(cfReader);
				}
			}
			return null;
		} finally {
			if (monitor != null)
				monitor.done();
		}
	}

	private static String getClassName(char[] name) {
		return new String(name).replace('/', '.');
	}

	private static Long calculateSerialVersionId(IClassFileReader cfReader) throws IOException {
		// implementing algorithm specified on http://download.oracle.com/javase/6/docs/platform/serialization/spec/class.html#4100

		ByteArrayOutputStream os= new ByteArrayOutputStream();
		DataOutputStream doos= new DataOutputStream(os);
		doos.writeUTF(getClassName(cfReader.getClassName())); // class name
		int mod= getClassModifiers(cfReader);
//		System.out.println(Integer.toHexString(mod) + ' ' + Flags.toString(mod));

		int classModifiers= mod & (Flags.AccPublic | Flags.AccFinal | Flags.AccInterface | Flags.AccAbstract);

		doos.writeInt(classModifiers); // class modifiers
		char[][] interfaces= getSortedInterfacesNames(cfReader);
		for (int i= 0; i < interfaces.length; i++) {
			doos.writeUTF(getClassName(interfaces[i]));
		}
		IFieldInfo[] sortedFields= getSortedFields(cfReader);
		for (int i= 0; i < sortedFields.length; i++) {
			IFieldInfo curr= sortedFields[i];
			int flags= curr.getAccessFlags();
			if (!Flags.isPrivate(flags) || (!Flags.isStatic(flags) && !Flags.isTransient(flags))) {
				doos.writeUTF(new String(curr.getName()));
				doos.writeInt(flags & (Flags.AccPublic | Flags.AccPrivate | Flags.AccProtected | Flags.AccStatic | Flags.AccFinal | Flags.AccVolatile | Flags.AccTransient)); // field modifiers
				doos.writeUTF(new String(curr.getDescriptor()));
			}
		}
		if (hasStaticClassInitializer(cfReader)) {
			doos.writeUTF(STATIC_CLASS_INITIALIZER);
			doos.writeInt(Flags.AccStatic);
			doos.writeUTF("()V"); //$NON-NLS-1$
		}
		IMethodInfo[] sortedMethods= getSortedMethods(cfReader);
		for (int i= 0; i < sortedMethods.length; i++) {
			IMethodInfo curr= sortedMethods[i];
			int flags= curr.getAccessFlags();
			if (!Flags.isPrivate(flags) && !curr.isClinit()) {
				doos.writeUTF(new String(curr.getName()));
				doos.writeInt(flags & (Flags.AccPublic | Flags.AccPrivate | Flags.AccProtected | Flags.AccStatic | Flags.AccFinal | Flags.AccSynchronized | Flags.AccNative | Flags.AccAbstract | Flags.AccStrictfp)); // method modifiers
				doos.writeUTF(getClassName(curr.getDescriptor()));
			}
		}
		doos.flush();
		return computeHash(os.toByteArray());
	}

	private static int getClassModifiers(IClassFileReader cfReader) {
		IInnerClassesAttribute innerClassesAttribute= cfReader.getInnerClassesAttribute();
		if (innerClassesAttribute != null) {
			IInnerClassesAttributeEntry[] entries = innerClassesAttribute.getInnerClassAttributesEntries();
			for (int i= 0; i < entries.length; i++) {
				IInnerClassesAttributeEntry entry = entries[i];
				char[] innerClassName = entry.getInnerClassName();
				if (innerClassName != null) {
					if (CharOperation.equals(cfReader.getClassName(), innerClassName)) {
						return entry.getAccessFlags();
					}
				}
			}
		}
		return cfReader.getAccessFlags();
	}

//	private static void print(byte[] bytes) {
//	StringBuffer buf= new StringBuffer();
//	for (int i= 0; i < bytes.length; i++) {
//	char c= (char) bytes[i];
//	if (!Character.isISOControl(c)) {
//	buf.append(c).append(' ');
//	} else {
//	buf.append(Integer.toHexString(c)).append(' ');
//	}
//	}
//	System.out.println(buf.toString());
//	System.out.println();
//	}


	private static Long computeHash(byte[] bytes) {
		try {
			byte[] sha= MessageDigest.getInstance("SHA-1").digest(bytes); //$NON-NLS-1$
			if (sha.length >= 8) {
				long hash= 0;
				for (int i= 7; i >= 0; i--) {
					hash= (hash << 8) | (sha[i] & 0xFF);
				}
				return Long.valueOf(hash);
			}
		} catch (NoSuchAlgorithmException e) {
			JavaManipulationPlugin.log(e);
		}
		return null;
	}

	private static char[][] getSortedInterfacesNames(IClassFileReader cfReader) {
		char[][] interfaceNames= cfReader.getInterfaceNames();
		Arrays.sort(interfaceNames, new Comparator<char[]>() {
			@Override
			public int compare(char[] o1, char[] o2) {
				return CharOperation.compareTo(o1, o2);
			}
		});
		return interfaceNames;
	}

	private static IFieldInfo[] getSortedFields(IClassFileReader cfReader) {
		IFieldInfo[] allFields= cfReader.getFieldInfos();
		Arrays.sort(allFields, new Comparator<IFieldInfo>() {
			@Override
			public int compare(IFieldInfo o1, IFieldInfo o2) {
				return CharOperation.compareTo(o1.getName(), o2.getName());
			}
		});
		return allFields;
	}

	private static boolean hasStaticClassInitializer(IClassFileReader cfReader) {
		IMethodInfo[] methodInfos= cfReader.getMethodInfos();
		for (int i= 0; i < methodInfos.length; i++) {
			if (methodInfos[i].isClinit()) {
				return true;
			}
		}
		return false;
	}

	private static IMethodInfo[] getSortedMethods(IClassFileReader cfReader) {
		IMethodInfo[] allMethods= cfReader.getMethodInfos();
		Arrays.sort(allMethods, new Comparator<IMethodInfo>() {
			@Override
			public int compare(IMethodInfo mi1, IMethodInfo mi2) {
				if (mi1.isConstructor() != mi2.isConstructor()) {
					return mi1.isConstructor() ? -1 : 1;
				} else if (mi1.isConstructor()) {
					return 0;
				}
				int res= CharOperation.compareTo(mi1.getName(), mi2.getName());
				if (res != 0) {
					return res;
				}
				return CharOperation.compareTo(mi1.getDescriptor(), mi2.getDescriptor());
			}
		});
		return allMethods;
	}

	private static IFile getClassfile(ITypeBinding typeBinding) throws CoreException {
		// bug 191943
		IType type= (IType) typeBinding.getJavaElement();
		if (type == null || type.getCompilationUnit() == null) {
			return null;
		}

		IRegion region= JavaCore.newRegion();
		region.add(type.getCompilationUnit());

		String name= typeBinding.getBinaryName();
		if (name != null) {
			int packStart= name.lastIndexOf('.');
			if (packStart != -1) {
				name= name.substring(packStart + 1);
			}
		} else {
			throw new CoreException(new Status(IStatus.ERROR, JavaManipulationPlugin.getPluginId(), CorrectionMessages.SerialVersionHashOperation_error_classnotfound));
		}

		name += ".class"; //$NON-NLS-1$

		IResource[] classFiles= JavaCore.getGeneratedResources(region, false);
		for (int i= 0; i < classFiles.length; i++) {
			IResource resource= classFiles[i];
			if (resource.getType() == IResource.FILE && resource.getName().equals(name)) {
				return (IFile) resource;
			}
		}
		throw new CoreException(new Status(IStatus.ERROR, JavaManipulationPlugin.getPluginId(), CorrectionMessages.SerialVersionHashOperation_error_classnotfound));
	}

	/**
	 * Displays an appropriate error message for a specific problem.
	 *
	 * @param message
	 *            The message to display
	 */
	private void displayErrorMessage(final String message) {
		fDisplay.displayErrorMessage(message);
	}

	/**
	 * Displays an appropriate error message for a specific problem.
	 *
	 * @param throwable
	 *            the throwable object to display
	 */
	private void displayErrorMessage(final Throwable throwable) {
		displayErrorMessage(throwable.getLocalizedMessage());
	}

	/**
	 * Displays a dialog with a question as message.
	 *
	 * @param title
	 *            The title to display
	 * @param message
	 *            The message to display
	 * @return returns the result of the dialog
	 */
	private boolean displayYesNoMessage(final String title, final String message) {
		return fDisplay.displayYesNoMessage(title, message);
	}

	private final ICompilationUnit fCompilationUnit;
	private final SerialVersionHashOperationDisplayCore fDisplay;

	public SerialVersionHashOperationCore(ICompilationUnit unit, ASTNode[] nodes) {
		this(unit, nodes, new SerialVersionHashOperationDisplayCore());
	}

	public SerialVersionHashOperationCore(ICompilationUnit unit, ASTNode[] nodes, SerialVersionHashOperationDisplayCore display) {
		super(unit, nodes);
		fCompilationUnit= unit;
		fDisplay= display;
	}

	@Override
	protected boolean addInitializer(final VariableDeclarationFragment fragment, final ASTNode declarationNode) {
		Assert.isNotNull(fragment);
		try {
			String id= computeId(declarationNode, new NullProgressMonitor());
			fragment.setInitializer(fragment.getAST().newNumberLiteral(id));
		} catch (InterruptedException exception) {
			// Do nothing
		}
		return true;
	}

	@Override
	protected void addLinkedPositions(ASTRewrite rewrite, VariableDeclarationFragment fragment, LinkedProposalModelCore positionGroups) {
		//Do nothing
	}

	private String computeId(final ASTNode declarationNode, final IProgressMonitor monitor) throws InterruptedException {
		Assert.isNotNull(monitor);
		long serialVersionID= SERIAL_VALUE;
		try {
			monitor.beginTask(CorrectionMessages.SerialVersionHashOperation_computing_id, 200);
			final IJavaProject project= fCompilationUnit.getJavaProject();
			final IPath path= fCompilationUnit.getResource().getFullPath();
			ITextFileBufferManager bufferManager= FileBuffers.getTextFileBufferManager();
			try {
				bufferManager.connect(path, LocationKind.IFILE, new SubProgressMonitor(monitor, 10));
				if (monitor.isCanceled())
					throw new InterruptedException();

				final ITextFileBuffer buffer= bufferManager.getTextFileBuffer(path, LocationKind.IFILE);
				if (buffer.isDirty() && buffer.isStateValidated() && buffer.isCommitable() && displayYesNoMessage(CorrectionMessages.SerialVersionHashOperation_save_caption, CorrectionMessages.SerialVersionHashOperation_save_message))
					buffer.commit(new SubProgressMonitor(monitor, 20), true);
				else
					monitor.worked(20);

				if (monitor.isCanceled())
					throw new InterruptedException();
			} finally {
				bufferManager.disconnect(path, LocationKind.IFILE, new SubProgressMonitor(monitor, 10));
			}
			project.getProject().build(IncrementalProjectBuilder.INCREMENTAL_BUILD, new SubProgressMonitor(monitor, 60));
			if (monitor.isCanceled())
				throw new InterruptedException();

			ITypeBinding typeBinding= getTypeBinding(declarationNode);
			if (typeBinding != null) {
				Long id= calculateSerialVersionId(typeBinding, new SubProgressMonitor(monitor, 100));
				if (id != null)
					serialVersionID= id.longValue();
			}
		} catch (CoreException exception) {
			displayErrorMessage(exception);
		} catch (IOException exception) {
			displayErrorMessage(exception);
		} finally {
			monitor.done();
		}
		return serialVersionID + LONG_SUFFIX;
	}

	private static ITypeBinding getTypeBinding(final ASTNode parent) {
		if (parent instanceof AbstractTypeDeclaration) {
			final AbstractTypeDeclaration declaration= (AbstractTypeDeclaration) parent;
			return declaration.resolveBinding();
		} else if (parent instanceof AnonymousClassDeclaration) {
			final AnonymousClassDeclaration declaration= (AnonymousClassDeclaration) parent;
			return declaration.resolveBinding();
		} else if (parent instanceof ParameterizedType) {
			final ParameterizedType type= (ParameterizedType) parent;
			return type.resolveBinding();
		}
		return null;
	}
}
