/*******************************************************************************
 * Copyright (c) 2000, 2022 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.corext.refactoring.nls;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;

import org.eclipse.core.runtime.CoreException;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IStorage;

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.jface.text.IDocument;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.Region;

import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.IPackageFragment;
import org.eclipse.jdt.core.IPackageFragmentRoot;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.ITypeRoot;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.core.Signature;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.ASTVisitor;
import org.eclipse.jdt.core.dom.Assignment;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.Expression;
import org.eclipse.jdt.core.dom.IBinding;
import org.eclipse.jdt.core.dom.IMethodBinding;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.IVariableBinding;
import org.eclipse.jdt.core.dom.MethodInvocation;
import org.eclipse.jdt.core.dom.Modifier;
import org.eclipse.jdt.core.dom.Name;
import org.eclipse.jdt.core.dom.NodeFinder;
import org.eclipse.jdt.core.dom.QualifiedName;
import org.eclipse.jdt.core.dom.SimpleName;
import org.eclipse.jdt.core.dom.SimpleType;
import org.eclipse.jdt.core.dom.StringLiteral;
import org.eclipse.jdt.core.dom.TypeLiteral;
import org.eclipse.jdt.core.dom.VariableDeclarationFragment;
import org.eclipse.jdt.core.manipulation.SharedASTProviderCore;

import org.eclipse.jdt.internal.corext.dom.Bindings;
import org.eclipse.jdt.internal.corext.util.JavaModelUtil;

import org.eclipse.jdt.internal.ui.JavaPlugin;

public class NLSHintHelper {

	private NLSHintHelper() {
	}

	/**
	 * Returns the accessor binding info or <code>null</code> if this element is not a nls'ed entry
	 *
	 * @param astRoot the ast root
	 * @param nlsElement the nls element
	 * @return the accessor class reference or <code>null</code> if this element is not a nls'ed entry
	 */
	public static AccessorClassReference getAccessorClassReference(CompilationUnit astRoot, NLSElement nlsElement) {
		IRegion region= nlsElement.getPosition();
		return getAccessorClassReference(astRoot, region);
	}

	/**
	 * Returns the accessor binding info or <code>null</code> if this element is not a nls'ed entry
	 *
	 * @param astRoot the ast root
	 * @param region the text region
	 * @return the accessor class reference or <code>null</code> if this element is not a nls'ed entry
	 */
	public static AccessorClassReference getAccessorClassReference(CompilationUnit astRoot, IRegion region) {
		return getAccessorClassReference(astRoot, region, false);
	}

	/**
	 * Returns the accessor binding info or <code>null</code> if this element is not a nls'ed entry
	 *
	 * @param astRoot the ast root
	 * @param region the text region
	 * @param usedFullyQualifiedName boolean flag to indicate that fully qualified name is used to
	 *            refer a NLS key string constant
	 * @return the accessor class reference or <code>null</code> if this element is not a nls'ed
	 *         entry
	 */
	public static AccessorClassReference getAccessorClassReference(CompilationUnit astRoot, IRegion region, boolean usedFullyQualifiedName) {
		ASTNode nlsStringLiteral= NodeFinder.perform(astRoot, region.getOffset(), region.getLength());
		if (nlsStringLiteral == null)
			return null; // not found

		ASTNode parent= nlsStringLiteral.getParent();
		if (usedFullyQualifiedName) {
			parent= parent.getParent();
		}

		ITypeBinding accessorBinding= null;

		if (!usedFullyQualifiedName && nlsStringLiteral instanceof SimpleName && nlsStringLiteral.getLocationInParent() == QualifiedName.NAME_PROPERTY) {
			SimpleName name= (SimpleName)nlsStringLiteral;

			IBinding binding= name.resolveBinding();
			if (binding instanceof IVariableBinding) {
				IVariableBinding variableBinding= (IVariableBinding)binding;
				if (Modifier.isStatic(variableBinding.getModifiers())) {
					accessorBinding= variableBinding.getDeclaringClass();
					if (accessorBinding != null && getResourceBundleName(accessorBinding) == null) {
						parent= parent.getParent();
						accessorBinding= null;
					}
				}
			}
		}

		if (accessorBinding == null) {

			if (parent instanceof MethodInvocation) {
				MethodInvocation methodInvocation= (MethodInvocation) parent;
				List<Expression> args= methodInvocation.arguments();
				if (args.size() != 1 && args.indexOf(nlsStringLiteral) != 0) {
					return null; // must be the only argument in lookup method
				}

				Expression firstArgument= args.get(0);
				ITypeBinding argumentBinding= firstArgument.resolveTypeBinding();
				if (argumentBinding == null || !"java.lang.String".equals(argumentBinding.getQualifiedName())) { //$NON-NLS-1$
					return null;
				}

				ITypeBinding typeBinding= methodInvocation.resolveTypeBinding();
				if (typeBinding == null || !"java.lang.String".equals(typeBinding.getQualifiedName())) { //$NON-NLS-1$
					return null;
				}

				IMethodBinding methodBinding= methodInvocation.resolveMethodBinding();
				if (methodBinding == null || !Modifier.isStatic(methodBinding.getModifiers())) {
					return null; // only static methods qualify
				}

				accessorBinding= methodBinding.getDeclaringClass();
			} else if (parent instanceof VariableDeclarationFragment) {
				VariableDeclarationFragment decl= (VariableDeclarationFragment)parent;
				if (decl.getInitializer() != null)
					return null;

				IVariableBinding variableBinding= decl.resolveBinding();
				if (variableBinding == null)
					return null;

				if (!Modifier.isStatic(variableBinding.getModifiers()))
					return null;

				accessorBinding= variableBinding.getDeclaringClass();
			}
		}

		if (accessorBinding == null)
			return null;

		String resourceBundleName;
		resourceBundleName= getResourceBundleName(accessorBinding);

		if (resourceBundleName != null)
			return new AccessorClassReference(accessorBinding, resourceBundleName, new Region(parent.getStartPosition(), parent.getLength()));

		return null;
	}

	public static IPackageFragment getPackageOfAccessorClass(IJavaProject javaProject, ITypeBinding accessorBinding) throws JavaModelException {
		if (accessorBinding != null) {
			ICompilationUnit unit= Bindings.findCompilationUnit(accessorBinding, javaProject);
			if (unit != null) {
				return (IPackageFragment) unit.getParent();
			}
		}
		return null;
	}

	public static String getResourceBundleName(ITypeBinding accessorClassBinding) {
		IJavaElement je= accessorClassBinding.getJavaElement();
		if (!(je instanceof IType))
			return null;
		ITypeRoot typeRoot= ((IType) je).getTypeRoot();
		CompilationUnit astRoot= SharedASTProviderCore.getAST(typeRoot, SharedASTProviderCore.WAIT_YES, null);

		return getResourceBundleName(astRoot);
	}

	public static String getResourceBundleName(ITypeRoot input) {
		return getResourceBundleName(SharedASTProviderCore.getAST(input, SharedASTProviderCore.WAIT_YES, null));
	}

	public static String getResourceBundleName(CompilationUnit astRoot) {

		if (astRoot == null)
			return null;

		final Map<Object, Object> resultCollector= new HashMap<>(5);
		final Object RESULT_KEY= new Object();
		final Object FIELD_KEY= new Object();

		astRoot.accept(new ASTVisitor() {

			@Override
			public boolean visit(MethodInvocation node) {
				IMethodBinding method= node.resolveMethodBinding();
				if (method == null)
					return true;

				String name= method.getDeclaringClass().getQualifiedName();
				if ((!"java.util.ResourceBundle".equals(name) || !"getBundle".equals(method.getName()) || (node.arguments().size() <= 0)) && //old school //$NON-NLS-1$ //$NON-NLS-2$
						(!"org.eclipse.osgi.util.NLS".equals(name) || !"initializeMessages".equals(method.getName()) || (node.arguments().size() != 2))) //Eclipse style //$NON-NLS-1$ //$NON-NLS-2$
					return true;

				Expression argument= (Expression)node.arguments().get(0);
				String bundleName= getBundleName(argument);
				if (bundleName != null)
					resultCollector.put(RESULT_KEY, bundleName);

				if (argument instanceof Name) {
					Object fieldNameBinding= ((Name)argument).resolveBinding();
					if (fieldNameBinding != null)
						resultCollector.put(FIELD_KEY, fieldNameBinding);
				}

				return false;
			}

			@Override
			public boolean visit(VariableDeclarationFragment node) {
				Expression initializer= node.getInitializer();
				String bundleName= getBundleName(initializer);
				if (bundleName != null) {
					Object fieldNameBinding= node.getName().resolveBinding();
					if (fieldNameBinding != null)
						resultCollector.put(fieldNameBinding, bundleName);
					return false;
				}
				return true;
			}

			@Override
			public boolean visit(Assignment node) {
				if (node.getLeftHandSide() instanceof Name) {
					String bundleName= getBundleName(node.getRightHandSide());
					if (bundleName != null) {
						Object fieldNameBinding= ((Name)node.getLeftHandSide()).resolveBinding();
						if (fieldNameBinding != null) {
							resultCollector.put(fieldNameBinding, bundleName);
							return false;
						}
					}
				}
				return true;
			}

			private String getBundleName(Expression initializer) {
				if (initializer instanceof StringLiteral)
					return ((StringLiteral)initializer).getLiteralValue();

				if (initializer instanceof MethodInvocation) {
					MethodInvocation methInvocation= (MethodInvocation)initializer;
					Expression exp= methInvocation.getExpression();
					if ((exp != null) && (exp instanceof TypeLiteral)) {
						SimpleType simple= (SimpleType)((TypeLiteral) exp).getType();
						ITypeBinding typeBinding= simple.resolveBinding();
						if (typeBinding != null)
							return typeBinding.getQualifiedName();
					}
				}
				return null;
			}

		});


		Object fieldName;
		String result;

		result= (String)resultCollector.get(RESULT_KEY);
		if (result != null)
			return result;

		fieldName= resultCollector.get(FIELD_KEY);
		if (fieldName != null)
			return (String)resultCollector.get(fieldName);

		// Now try hard-coded bundle name String field names from NLS tooling:
		Iterator<Object> iter= resultCollector.keySet().iterator();
		while (iter.hasNext()) {
			Object o= iter.next();
			if (!(o instanceof IBinding))
				continue;
			IBinding binding= (IBinding)o;
			fieldName= binding.getName();
			if ("BUNDLE_NAME".equals(fieldName) || "RESOURCE_BUNDLE".equals(fieldName) || "bundleName".equals(fieldName)) { //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
				result= (String)resultCollector.get(binding);
				if (result != null)
					return result;
			}
		}

		result= (String)resultCollector.get(RESULT_KEY);
		if (result != null)
			return result;

		fieldName= resultCollector.get(FIELD_KEY);
		if (fieldName != null)
			return (String)resultCollector.get(fieldName);

		return null;
	}

	public static IPackageFragment getResourceBundlePackage(IJavaProject javaProject, String packageName, String resourceName) throws JavaModelException {
		for (IPackageFragmentRoot root : javaProject.getAllPackageFragmentRoots()) {
			if (root.getKind() == IPackageFragmentRoot.K_SOURCE) {
				IPackageFragment packageFragment= root.getPackageFragment(packageName);
				if (packageFragment.exists()) {
					for (Object object : packageFragment.isDefaultPackage() ? root.getNonJavaResources() : packageFragment.getNonJavaResources()) {
						if (object instanceof IFile) {
							IFile file= (IFile) object;
							if (file.getName().equals(resourceName)) {
								return packageFragment;
							}
						}
					}
				}
			}
		}
		return null;
	}

	public static IStorage getResourceBundle(ICompilationUnit compilationUnit) throws JavaModelException {
		IJavaProject project= compilationUnit.getJavaProject();
		if (project == null)
			return null;

		String name= getResourceBundleName(compilationUnit);
		if (name == null)
			return null;

		String packName= Signature.getQualifier(name);
		String resourceName= Signature.getSimpleName(name) + NLSRefactoring.PROPERTY_FILE_EXT;

		return getResourceBundle(project, packName, resourceName);
	}

	public static IStorage getResourceBundle(IJavaProject javaProject, String packageName, String resourceName) throws JavaModelException {
		for (IPackageFragmentRoot root : javaProject.getAllPackageFragmentRoots()) {
			if (root.getKind() == IPackageFragmentRoot.K_SOURCE) {
				IStorage storage= getResourceBundle(root, packageName, resourceName);
				if (storage != null)
					return storage;
			}
		}
		return null;
	}

	public static IStorage getResourceBundle(IPackageFragmentRoot root, String packageName, String resourceName) throws JavaModelException {
		IPackageFragment packageFragment= root.getPackageFragment(packageName);
		if (packageFragment.exists()) {
			for (Object object : packageFragment.isDefaultPackage() ? root.getNonJavaResources() : packageFragment.getNonJavaResources()) {
				if (JavaModelUtil.isOpenableStorage(object)) {
					IStorage storage= (IStorage)object;
					if (storage.getName().equals(resourceName)) {
						return storage;
					}
				}
			}
		}
		return null;
	}

	public static IStorage getResourceBundle(IJavaProject javaProject, AccessorClassReference accessorClassReference) throws JavaModelException {
		String resourceBundle= accessorClassReference.getResourceBundleName();
		if (resourceBundle == null)
			return null;

		String resourceName= Signature.getSimpleName(resourceBundle) + NLSRefactoring.PROPERTY_FILE_EXT;
		String packName= Signature.getQualifier(resourceBundle);
		ITypeBinding accessorClass= accessorClassReference.getBinding();

		if (accessorClass.isFromSource())
			return getResourceBundle(javaProject, packName, resourceName);
		else if (accessorClass.getJavaElement() != null)
			return getResourceBundle((IPackageFragmentRoot)accessorClass.getJavaElement().getAncestor(IJavaElement.PACKAGE_FRAGMENT_ROOT), packName, resourceName);

		return null;
	}

	/**
	 * Reads the properties from the given storage and
	 * returns it.
	 *
	 * @param javaProject the Java project
	 * @param accessorClassReference the accessor class reference
	 * @return the properties or <code>null</code> if it was not successfully read
	 */
	public static Properties getProperties(IJavaProject javaProject, AccessorClassReference accessorClassReference) {
		try {
			IStorage storage= NLSHintHelper.getResourceBundle(javaProject, accessorClassReference);
			return getProperties(storage);
		} catch (JavaModelException ex) {
			// sorry no properties
			return null;
		}
	}

	/**
	 * Reads the properties from the given storage and
	 * returns it.
	 *
	 * @param storage the storage
	 * @return the properties or <code>null</code> if it was not successfully read
	 */
	public static Properties getProperties(IStorage storage) {
		if (storage == null)
			return null;

		Properties props= new Properties();
		InputStream is= null;

		ITextFileBufferManager manager= FileBuffers.getTextFileBufferManager();
		try {
			if (manager != null) {
				ITextFileBuffer buffer= manager.getTextFileBuffer(storage.getFullPath(), LocationKind.NORMALIZE);
				if (buffer != null) {
					IDocument document= buffer.getDocument();
					is= new ByteArrayInputStream(document.get().getBytes());
				}
			}

			// Fallback: read from storage
			if (is == null)
				is= storage.getContents();

			props.load(is);

		} catch (IOException | CoreException e) {
			// sorry no properties
			return null;
		} finally {
			if (is != null) try {
				is.close();
			} catch (IOException e) {
				// return properties anyway but log
				JavaPlugin.log(e);
			}
		}
		return props;
	}

}
