/*******************************************************************************
 * Copyright (c) 2021, 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:
 *     Carsten Hammer
 *******************************************************************************/
package org.eclipse.jdt.internal.corext.fix;

import static org.eclipse.jdt.internal.corext.fix.LibStandardNames.FIELD_PATH_SEPARATOR;
import static org.eclipse.jdt.internal.corext.fix.LibStandardNames.FIELD_SEPARATOR;
import static org.eclipse.jdt.internal.corext.fix.LibStandardNames.METHOD_BOOLEAN;
import static org.eclipse.jdt.internal.corext.fix.LibStandardNames.METHOD_DEFAULT_CHARSET;
import static org.eclipse.jdt.internal.corext.fix.LibStandardNames.METHOD_DISPLAY_NAME;
import static org.eclipse.jdt.internal.corext.fix.LibStandardNames.METHOD_GET_DEFAULT;
import static org.eclipse.jdt.internal.corext.fix.LibStandardNames.METHOD_GET_PROPERTY;
import static org.eclipse.jdt.internal.corext.fix.LibStandardNames.METHOD_GET_SEPARATOR;
import static org.eclipse.jdt.internal.corext.fix.LibStandardNames.METHOD_INTEGER;
import static org.eclipse.jdt.internal.corext.fix.LibStandardNames.METHOD_LINE_SEPARATOR;
import static org.eclipse.jdt.internal.corext.fix.LibStandardNames.METHOD_LONG;
import static org.eclipse.jdt.internal.corext.fix.LibStandardNames.METHOD_PARSEBOOLEAN;
import static org.eclipse.jdt.internal.corext.fix.LibStandardNames.METHOD_PARSEINTEGER;
import static org.eclipse.jdt.internal.corext.fix.LibStandardNames.METHOD_PARSELONG;
import static org.eclipse.jdt.internal.ui.fix.MultiFixMessages.ConstantsCleanUp_description;

import java.io.File;
import java.nio.charset.Charset;
import java.nio.file.FileSystems;
import java.util.List;
import java.util.Set;

import org.eclipse.core.runtime.CoreException;

import org.eclipse.text.edits.TextEditGroup;

import org.eclipse.jdt.core.dom.AST;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.ASTVisitor;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.Expression;
import org.eclipse.jdt.core.dom.FieldAccess;
import org.eclipse.jdt.core.dom.MethodInvocation;
import org.eclipse.jdt.core.dom.NumberLiteral;
import org.eclipse.jdt.core.dom.StringLiteral;
import org.eclipse.jdt.core.dom.rewrite.ASTRewrite;
import org.eclipse.jdt.core.dom.rewrite.ImportRewrite;
import org.eclipse.jdt.core.dom.rewrite.TargetSourceRangeComputer;

import org.eclipse.jdt.internal.corext.dom.ASTNodeFactory;
import org.eclipse.jdt.internal.corext.dom.ASTNodes;
import org.eclipse.jdt.internal.corext.fix.CompilationUnitRewriteOperationsFixCore.CompilationUnitRewriteOperation;
import org.eclipse.jdt.internal.corext.refactoring.structure.CompilationUnitRewrite;
import org.eclipse.jdt.internal.corext.util.JavaModelUtil;
import org.eclipse.jdt.internal.corext.util.Messages;

public enum UpdateProperty {
	/**
	 * After change -Dfile.encoding=xxx is no longer taken into account
	 * setting encoding -Dfile.encoding="UTF-8" is not official supported anyway as it is
	 * a "read only" property similar as -Dsun.jnu.encoding="UTF-8" is not supported officially
	 *
	 * Change
	 * <code>System.getProperty("file.encoding");</code>
	 * to
	 * <code>Charset.defaultCharset().displayName();</code> since Java 1.5
	 *
	 */
	FILE_ENCODING("file.encoding", //$NON-NLS-1$
			Charset.class,
			METHOD_DEFAULT_CHARSET,
			METHOD_DISPLAY_NAME,
			null,
			null,
			UpdateProperty::defaultvisitor,
			UpdateProperty::defaultRewrite),
	/**
	 * After change -Dpath.separator=xxx is no longer taken into account
	 *
	 * Change
	 * <code>System.getProperty("path.separator");</code>
	 * to
	 * <code>File.pathSeparator;</code>
	 *
	 */
	PATH_SEPARATOR("path.separator", //$NON-NLS-1$
			null,
			null,
			null,
			File.class,
			FIELD_PATH_SEPARATOR,
			UpdateProperty::defaultvisitor,
			UpdateProperty::pathRewrite),
	/**
	 * After change -Dfile.separator=xxx is no longer taken into account
	 *
	 * Change
	 * <code>System.getProperty("file.separator");</code>
	 * to
	 * <code>FileSystems.getDefault().getSeparator();</code> introduced in Java 1.7
	 * fallback
	 * <code>File.separator;</code>
	 *
	 * see system property {@code java.nio.file.spi.DefaultFileSystemProvider} to set default file system
	 */
	FILE_SEPARATOR("file.separator", //$NON-NLS-1$
			FileSystems.class,
			METHOD_GET_DEFAULT,
			METHOD_GET_SEPARATOR,
			File.class,
			FIELD_SEPARATOR,
			UpdateProperty::defaultvisitor,
			UpdateProperty::defaultRewrite),
	/**
	 * Change
	 * <code>System.getProperty("line.separator");</code>
	 * to
	 * <code>System.lineSeparator();</code>
	 */
	LINE_SEPARATOR("line.separator", //$NON-NLS-1$
			System.class,
			METHOD_LINE_SEPARATOR,
			null,
			null,
			null,
			UpdateProperty::defaultvisitor,
			UpdateProperty::defaultRewrite),
	/**
	 * Change
	 * <code>Boolean.parseBoolean(System.getProperty("arbitrarykey"));</code>
	 * to
	 * <code>Boolean.getBoolean("arbitrarykey");</code>
	 *
	 * Currently <code>Boolean.parseBoolean(System.getProperty("arbitrarykey", "false"))</code> is not supported
	 */
	BOOLEAN_PROPERTY(null,
			null,
			METHOD_BOOLEAN,
			null,
			Boolean.class,
			null,
			UpdateProperty::parseboolean_visitor,
			UpdateProperty::boolintlongRewrite),
	/**
	 * Change
	 * <code>Integer.parseInt(System.getProperty("arbitrarykey"));</code>
	 * to
	 * <code>Integer.getInteger("arbitrarykey");</code>
	 *
	 * Currently <code>Integer.parseInt(System.getProperty("arbitrarykey", "false"))</code> is not supported
	 */
	INTEGER_PROPERTY(null,
			null,
			METHOD_INTEGER,
			null,
			Integer.class,
			null,
			UpdateProperty::parseinteger_visitor,
			UpdateProperty::boolintlongRewrite),
	/**
	 * Change
	 * <code>Long.parseLong(System.getProperty("arbitrarykey"));</code>
	 * to
	 * <code>Long.getLong("arbitrarykey");</code>
	 *
	 * Currently <code>Long.parseLong(System.getProperty("arbitrarykey", "false"))</code> is not supported
	 */
	LONG_PROPERTY(null,
			null,
			METHOD_LONG,
			null,
			Long.class,
			null,
			UpdateProperty::parselong_visitor,
			UpdateProperty::boolintlongRewrite);

	public static Object UNUSED= new Object();

	String key;
	Class<?> cl;
	String simplename;
	String simplename2;
	Class<?> alternativecl;
	String constant;
	IFinder myfinder;
	IRewriter myrewriter;

	UpdateProperty(String key, Class<?> cl, String simplename, String simplename2, Class<?> alternativecl, String constant,
			IFinder myfinder, IRewriter myrewriter) {
		this.key= key;
		this.cl= cl;
		this.simplename= simplename;
		this.simplename2= simplename2;
		this.alternativecl= alternativecl;
		this.constant= constant;
		this.myfinder= myfinder;
		this.myrewriter= myrewriter;
	}

	static void defaultvisitor(final UpdateProperty upp, final CompilationUnit compilationUnit,final Set<CompilationUnitRewriteOperation> operations,final Set<ASTNode> nodesprocessed) {
		compilationUnit.accept(new ASTVisitor() {
			@Override
			public boolean visit(final MethodInvocation visited) {
				if(nodesprocessed.contains(visited)) {
					return false;
				}
				/**
				 * look for
				 * <code>System.getProperty("file.encoding");</code>
				 * <code>System.getProperty("path.separator");</code>
				 * <code>System.getProperty("file.separator");</code>
				 * <code>System.getProperty("line.separator");</code>
				 *
				 */
				if (ASTNodes.usesGivenSignature(visited, System.class.getCanonicalName(), METHOD_GET_PROPERTY, String.class.getCanonicalName())) {
					Expression expression= (Expression) visited.arguments().get(0);
					Object propertykey= expression.resolveConstantExpressionValue();
					if (propertykey instanceof String && upp.key.equals(propertykey)) {
						operations.add(upp.rewrite(visited, null, expression,null));
						nodesprocessed.add(visited);
						return false;
					}
				}
				return true;
			}
		});
	}

	static void parseboolean_visitor(final UpdateProperty upp, final CompilationUnit compilationUnit, final Set<CompilationUnitRewriteOperation> operations, final Set<ASTNode> nodesprocessed) {
		compilationUnit.accept(new ASTVisitor() {
			@Override
			public boolean visit(final MethodInvocation visited) {
				if(nodesprocessed.contains(visited)) {
					return false;
				}
				/**
				 * look for
				 * <code>Boolean.parseBoolean(System.getProperty("arbitrarykey"));</code>
				 * (has to be done after completing the specific search)
				 */
				if (ASTNodes.usesGivenSignature(visited, System.class.getCanonicalName(), METHOD_GET_PROPERTY, String.class.getCanonicalName())) {
					Expression expression= (Expression) visited.arguments().get(0);
					Object propertykey= expression.resolveConstantExpressionValue();
					if (propertykey instanceof String && visited.getParent() instanceof MethodInvocation) {
						MethodInvocation parent=(MethodInvocation) visited.getParent();
						if (ASTNodes.usesGivenSignature(parent, Boolean.class.getCanonicalName(), METHOD_PARSEBOOLEAN, String.class.getCanonicalName())) {
							operations.add(upp.rewrite(parent, (String) propertykey,expression, null));
							nodesprocessed.add(visited);
							return false;
						}
					}
				} else if (ASTNodes.usesGivenSignature(visited, System.class.getCanonicalName(), METHOD_GET_PROPERTY, String.class.getCanonicalName(), String.class.getCanonicalName())) {
					Expression expression= (Expression) visited.arguments().get(0);
					Expression expression2= (Expression) visited.arguments().get(1);
					Object propertykey= expression.resolveConstantExpressionValue();
					Object propertykey2= expression2.resolveConstantExpressionValue();
					if (propertykey instanceof String && propertykey2 instanceof String && visited.getParent() instanceof MethodInvocation) {
						MethodInvocation parent=(MethodInvocation) visited.getParent();
						if (ASTNodes.usesGivenSignature(parent, Boolean.class.getCanonicalName(), METHOD_PARSEBOOLEAN, String.class.getCanonicalName()) && ((String)propertykey2).toLowerCase().equals("false")) { //$NON-NLS-1$
							operations.add(upp.rewrite(parent, (String) propertykey,expression, UNUSED));
							nodesprocessed.add(visited);
							return false;
						}
					}
				}
				return true;
			}
		});
	}

	static void parseinteger_visitor(final UpdateProperty upp, final CompilationUnit compilationUnit, final Set<CompilationUnitRewriteOperation> operations, final Set<ASTNode> nodesprocessed) {
		compilationUnit.accept(new ASTVisitor() {
			@Override
			public boolean visit(final MethodInvocation visited) {
				if(nodesprocessed.contains(visited)) {
					return false;
				}
				/**
				 * look for
				 * <code>Integer.parseInteger(System.getProperty("arbitrarykey"));</code>
				 * (has to be done after completing the specific search)
				 */
				if (ASTNodes.usesGivenSignature(visited, System.class.getCanonicalName(), METHOD_GET_PROPERTY, String.class.getCanonicalName())) {
					Expression expression= (Expression) visited.arguments().get(0);
					Object propertykey= expression.resolveConstantExpressionValue();
					if (propertykey instanceof String && visited.getParent() instanceof MethodInvocation) {
						MethodInvocation parent=(MethodInvocation) visited.getParent();
						if (ASTNodes.usesGivenSignature(parent, Integer.class.getCanonicalName(), METHOD_PARSEINTEGER, String.class.getCanonicalName())) {
							operations.add(upp.rewrite(parent, (String) propertykey,expression, null));
							nodesprocessed.add(visited);
							return false;
						}
					}
				} else if (ASTNodes.usesGivenSignature(visited, System.class.getCanonicalName(), METHOD_GET_PROPERTY, String.class.getCanonicalName(), String.class.getCanonicalName())) {
					Expression expression= (Expression) visited.arguments().get(0);
					Expression expression2= (Expression) visited.arguments().get(1);
					Object propertykey= expression.resolveConstantExpressionValue();
					Object propertykey2= expression2.resolveConstantExpressionValue();
					if (propertykey instanceof String && propertykey2 instanceof String && visited.getParent() instanceof MethodInvocation) {
						MethodInvocation parent=(MethodInvocation) visited.getParent();
						if (ASTNodes.usesGivenSignature(parent, Integer.class.getCanonicalName(), METHOD_PARSEINTEGER, String.class.getCanonicalName())&& ((String)propertykey2).toLowerCase().equals("0")) { //$NON-NLS-1$
							operations.add(upp.rewrite(parent, (String) propertykey,expression, UNUSED));
							nodesprocessed.add(visited);
							return false;
						} else if (ASTNodes.usesGivenSignature(parent, Integer.class.getCanonicalName(), METHOD_PARSEINTEGER, String.class.getCanonicalName())) {
							operations.add(upp.rewrite(parent, (String) propertykey,expression,Integer.valueOf((String)propertykey2)));
							nodesprocessed.add(visited);
							return false;
						}
					}
				}
				return true;
			}
		});
	}

	static void parselong_visitor(final UpdateProperty upp, final CompilationUnit compilationUnit, final Set<CompilationUnitRewriteOperation> operations, final Set<ASTNode> nodesprocessed) {
		compilationUnit.accept(new ASTVisitor() {
			@Override
			public boolean visit(final MethodInvocation visited) {
				if(nodesprocessed.contains(visited)) {
					return false;
				}
				/**
				 * look for
				 * <code>Integer.parseInteger(System.getProperty("arbitrarykey"));</code>
				 * (has to be done after completing the specific search)
				 */
				if (ASTNodes.usesGivenSignature(visited, System.class.getCanonicalName(), METHOD_GET_PROPERTY, String.class.getCanonicalName())) {
					Expression expression= (Expression) visited.arguments().get(0);
					Object propertykey= expression.resolveConstantExpressionValue();
					if (propertykey instanceof String && visited.getParent() instanceof MethodInvocation) {
						MethodInvocation parent=(MethodInvocation) visited.getParent();
						if (ASTNodes.usesGivenSignature(parent, Long.class.getCanonicalName(), METHOD_PARSELONG, String.class.getCanonicalName())) {
							operations.add(upp.rewrite(parent, (String) propertykey,expression, null));
							nodesprocessed.add(visited);
							return false;
						}
					}
				} else if (ASTNodes.usesGivenSignature(visited, System.class.getCanonicalName(), METHOD_GET_PROPERTY, String.class.getCanonicalName(), String.class.getCanonicalName())) {
					Expression expression= (Expression) visited.arguments().get(0);
					Expression expression2= (Expression) visited.arguments().get(1);
					Object propertykey= expression.resolveConstantExpressionValue();
					Object propertykey2= expression2.resolveConstantExpressionValue();
					if (propertykey instanceof String && propertykey2 instanceof String && visited.getParent() instanceof MethodInvocation) {
						MethodInvocation parent=(MethodInvocation) visited.getParent();
						if (ASTNodes.usesGivenSignature(parent, Long.class.getCanonicalName(), METHOD_PARSELONG, String.class.getCanonicalName()) && ((String)propertykey2).toLowerCase().equals("0")) { //$NON-NLS-1$
							operations.add(upp.rewrite(parent, (String) propertykey,expression, UNUSED));
							nodesprocessed.add(visited);
							return false;
						} else if (ASTNodes.usesGivenSignature(parent, Long.class.getCanonicalName(), METHOD_PARSELONG, String.class.getCanonicalName())) {
							operations.add(upp.rewrite(parent, (String) propertykey,expression,Long.valueOf((String)propertykey2)));
							nodesprocessed.add(visited);
							return false;
						}
					}
				}
				return true;
			}
		});
	}
	interface IRewriter {
		void computeRewriter(final UpdateProperty upp,final MethodInvocation visited, final String propertykey,
				final Expression expression, final CompilationUnitRewrite cuRewrite, final TextEditGroup group, Object object) throws CoreException;
	}

	CompilationUnitRewriteOperation rewrite(final MethodInvocation visited, final String propertykey, final Expression expression, final Object object) {
		return new CompilationUnitRewriteOperation() {
			@Override
			public void rewriteAST(final CompilationUnitRewrite cuRewrite, final LinkedProposalModelCore linkedModel) throws CoreException {
				TextEditGroup group= createTextEditGroup(Messages.format(ConstantsCleanUp_description,UpdateProperty.this.toString()), cuRewrite);
				cuRewrite.getASTRewrite().setTargetSourceRangeComputer(computer);
				myrewriter.computeRewriter(UpdateProperty.this, visited, propertykey, expression, cuRewrite, group, object);
			}
		};
	}

	private static void boolintlongRewrite(UpdateProperty upp,final MethodInvocation visited, final String propertykey, Expression expression, final CompilationUnitRewrite cuRewrite,
			TextEditGroup group, Object object) throws CoreException {
		ASTRewrite rewrite= cuRewrite.getASTRewrite();
		AST ast= cuRewrite.getRoot().getAST();
		/**
		 * Add import
		 */
		ImportRewrite importRewrite= cuRewrite.getImportRewrite();
		importRewrite.addImport(upp.alternativecl.getCanonicalName());
		/**
		 * Add field access
		 */
		MethodInvocation newMethodInvocation= ast.newMethodInvocation();
		newMethodInvocation.setExpression(ASTNodeFactory.newName(ast, upp.alternativecl.getSimpleName()));
		newMethodInvocation.setName(ast.newSimpleName(upp.simplename));
		List<Expression> arguments= newMethodInvocation.arguments();
		if (object != null) {
			StringLiteral sl= ast.newStringLiteral();
			sl.setLiteralValue(propertykey);
			arguments.add(sl);
			if (object != UNUSED) {
				NumberLiteral newNumberLiteral= ast.newNumberLiteral();
				newNumberLiteral.setToken(object.toString());
				arguments.add(newNumberLiteral);
			}
			ASTNode replace_with_Call= newMethodInvocation;
			ASTNodes.replaceAndRemoveNLS(rewrite, visited, replace_with_Call, group, cuRewrite);
		} else {
			arguments.add(ASTNodes.createMoveTarget(cuRewrite.getASTRewrite(), ASTNodes.getUnparenthesedExpression(expression)));
			ASTNode replace_with_Call= newMethodInvocation;
			ASTNodes.replaceButKeepComment(rewrite, visited, replace_with_Call, group);
		}
	}

	private static void defaultRewrite(UpdateProperty upp, final MethodInvocation visited, final String propertykey, Expression expression,
			final CompilationUnitRewrite cuRewrite,final TextEditGroup group, Object object) throws CoreException {
		ASTRewrite rewrite= cuRewrite.getASTRewrite();
		AST ast= cuRewrite.getRoot().getAST();
		ASTNode replace_with_Call;
		if (JavaModelUtil.is1d7OrHigher(cuRewrite.getCu().getJavaProject())) {

			/**
			 * Add import
			 */
			ImportRewrite importRewrite= cuRewrite.getImportRewrite();
			importRewrite.addImport(upp.cl.getCanonicalName());
			/**
			 * Add first method call
			 */
			MethodInvocation firstCall= ast.newMethodInvocation();
			firstCall.setExpression(ASTNodeFactory.newName(ast, upp.cl.getSimpleName()));
			firstCall.setName(ast.newSimpleName(upp.simplename));

			if(upp.simplename2==null) {
				replace_with_Call= firstCall;
			} else {
				/**
				 * Add second method call
				 */
				MethodInvocation secondCall= ast.newMethodInvocation();
				secondCall.setExpression(firstCall);
				secondCall.setName(ast.newSimpleName(upp.simplename2));
				replace_with_Call= secondCall;
			}
		} else {
			if(upp.alternativecl==null) {
				/**
				 * can be null for System.getProperty("file.encoding") on Java < 7
				 */
				return;
			} else {
				/**
				 * fallback to File.pathSeparator for java < 7.0
				 */

				/**
				 * Add import
				 */
				ImportRewrite importRewrite= cuRewrite.getImportRewrite();
				importRewrite.addImport(upp.alternativecl.getCanonicalName());
				/**
				 * Add field access
				 */
				FieldAccess fieldaccess= ast.newFieldAccess();
				fieldaccess.setExpression(ASTNodeFactory.newName(ast, upp.alternativecl.getSimpleName()));
				fieldaccess.setName(ast.newSimpleName(upp.constant));
				replace_with_Call= fieldaccess;
			}
		}
		ASTNodes.replaceAndRemoveNLS(rewrite, visited, replace_with_Call, group, cuRewrite);
	}

	private static void pathRewrite(UpdateProperty upp, final MethodInvocation visited, final String propertykey, Expression expression,
			final CompilationUnitRewrite cuRewrite,final TextEditGroup group, Object object) throws CoreException {
		ASTRewrite rewrite= cuRewrite.getASTRewrite();
		AST ast= cuRewrite.getRoot().getAST();
		ASTNode replace_with_Call;
		/**
		 * Add import
		 */
		ImportRewrite importRewrite= cuRewrite.getImportRewrite();
		importRewrite.addImport(upp.alternativecl.getCanonicalName());
		/**
		 * Add field access
		 */
		FieldAccess fieldaccess= ast.newFieldAccess();
		fieldaccess.setExpression(ASTNodeFactory.newName(ast, upp.alternativecl.getSimpleName()));
		fieldaccess.setName(ast.newSimpleName(upp.constant));
		replace_with_Call= fieldaccess;
		ASTNodes.replaceAndRemoveNLS(rewrite, visited, replace_with_Call, group, cuRewrite);
	}

	interface IFinder {
		void finder(final UpdateProperty upp, final CompilationUnit compilationUnit,final  Set<CompilationUnitRewriteOperation> operations,final  Set<ASTNode> nodesprocessed);
	}

	/**
	 * Compute set of CompilationUnitRewriteOperation to refactor supported system property to method calls
	 *
	 * @param compilationUnit unit to search for System.getProperty(xxx) calls
	 * @param operations set of all CompilationUnitRewriteOperations created already
	 * @param nodesprocessed list to remember nodes already processed
	 */
	public void findOperations(final CompilationUnit compilationUnit,final Set<CompilationUnitRewriteOperation> operations,final Set<ASTNode> nodesprocessed) {
		myfinder.finder(this, compilationUnit, operations, nodesprocessed);
	}

	final static TargetSourceRangeComputer computer= new TargetSourceRangeComputer() {
		@Override
		public SourceRange computeSourceRange(final ASTNode nodeWithComment) {
			if (Boolean.TRUE.equals(nodeWithComment.getProperty(ASTNodes.UNTOUCH_COMMENT))) {
				return new SourceRange(nodeWithComment.getStartPosition(), nodeWithComment.getLength());
			}
			return super.computeSourceRange(nodeWithComment);
		}
	};
}
