package de.retest.recheck.ignore;

import java.io.Reader;
import java.io.StringReader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.util.HashSet;
import java.util.Set;

import javax.script.Invocable;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import de.retest.recheck.ui.descriptors.Element;
import de.retest.recheck.ui.diff.AttributeDifference;

public class JSFilterImpl implements Filter {

	private static final Logger logger = LoggerFactory.getLogger( JSFilterImpl.class );

	private static final String JS_ENGINE_NAME = "JavaScript";

	private final ScriptEngine engine;
	private final Set<String> errorFunctions = new HashSet<>();

	public JSFilterImpl( final Path ignoreFilePath ) {
		final ScriptEngineManager manager = new ScriptEngineManager();
		engine = manager.getEngineByName( JS_ENGINE_NAME );
		if ( engine == null ) {
			logger.error(
					"Cannot execute JavaScript rules, `ScriptEngineManager` returned null for engine `JavaScript`. Try switching JDK or add a JavaScript engine: ",
					new IllegalStateException( "No JavaScript available." ) );
			return;
		}
		try {
			engine.eval( readScriptFile( ignoreFilePath ) );
		} catch ( final Exception e ) {
			logger.error( "Reading script file '{}' caused exception: ", ignoreFilePath, e );
		}
	}

	Reader readScriptFile( final Path ignoreFilePath ) {
		try {
			logger.info( "Reading JS ignore rules file from {}.", ignoreFilePath );
			return Files.newBufferedReader( ignoreFilePath, StandardCharsets.UTF_8 );
		} catch ( final NoSuchFileException e ) {
			logger.warn( "No JS filter file found at '{}': ", ignoreFilePath, e.getMessage() );
		} catch ( final Exception e ) {
			logger.error( "Error opening JS file from '{}': ", ignoreFilePath, e );
		}
		return new StringReader( "" );
	}

	@Override
	public boolean matches( final Element element ) {
		return callBooleanJSFunction( "matches", element ) || callBooleanJSFunction( "shouldIgnoreElement", element );
	}

	@Override
	public boolean matches( final Element element, final AttributeDifference attributeDifference ) {
		return callBooleanJSFunction( "matches", element, attributeDifference )
				|| callBooleanJSFunction( "shouldIgnoreAttributeDifference", element, attributeDifference );
	}

	private boolean callBooleanJSFunction( final String functionName, final Object... args ) {
		if ( engine == null || errorFunctions.contains( functionName ) ) {
			return false;
		}
		final Invocable inv = (Invocable) engine;
		// call function from script file
		try {
			final Object callResult = inv.invokeFunction( functionName, args );
			if ( callResult == null ) {
				logger.warn( "{} returned 'null' instead of a boolean value. Interpreting that as 'false'.",
						functionName );
				return false;
			}
			if ( !(callResult instanceof Boolean) ) {
				logger.error( "'{}' of {} cannot be cast to java.lang.Boolean.", callResult, callResult.getClass() );
				errorFunctions.add( functionName );
			}
			return (boolean) callResult;
		} catch ( final ScriptException e ) {
			logger.error( "JS '{}' method caused an exception: {}", functionName, e.getMessage() );
			errorFunctions.add( functionName );
		} catch ( final NoSuchMethodException e ) {
			if ( !functionName.startsWith( "shouldIgnore" ) ) {
				logger.warn( "Specified JS ignore file has no '{}' function.", functionName );
				errorFunctions.add( functionName );
			}
		}
		return false;
	}
}

@@ -768,16 +768,48 @@
 ript%22;%0A%0A
+%09private final String filePath;%0A
 %09private
@@ -923,38 +923,38 @@
 mpl( final Path 
-ignore
+filter
 FilePath ) %7B%0A%09%09f
@@ -950,16 +950,56 @@
 ath ) %7B%0A
+%09%09filePath = filterFilePath.toString();%0A
 %09%09final 
@@ -1420,22 +1420,22 @@
 ptFile( 
-ignore
+filter
 FilePath
@@ -1534,30 +1534,30 @@
 ception: %22, 
-ignore
+filter
 FilePath, e 
@@ -1657,22 +1657,22 @@
 ding JS 
-ignore
+filter
  rules f
@@ -2960,16 +2960,27 @@
 an value
+ in file %7B%7D
 . Interp
@@ -3011,16 +3011,26 @@
 ,%0A%09%09%09%09%09%09
+filePath, 
 function
@@ -3101,24 +3101,64 @@
 oolean) ) %7B%0A
+%09%09%09%09errorFunctions.add( functionName );%0A
 %09%09%09%09logger.e
@@ -3210,16 +3210,27 @@
 .Boolean
+ in file %7B%7D
 .%22, call
@@ -3236,17 +3236,23 @@
 lResult,
- 
+%0A%09%09%09%09%09%09
 callResu
@@ -3268,48 +3268,18 @@
 ss()
- );%0A%09%09%09%09errorFunctions.add( functionName
+, filePath
  );%0A
@@ -3347,32 +3347,71 @@
 Exception e ) %7B%0A
+%09%09%09errorFunctions.add( functionName );%0A
 %09%09%09logger.error(
@@ -3450,16 +3450,28 @@
 tion: %7B%7D
+ in file %7B%7D.
 %22, funct
@@ -3497,47 +3497,23 @@
 ge()
- );
+,
 %0A%09%09%09
-errorFunctions.add( functionName
+%09%09filePath
  );%0A
@@ -3609,24 +3609,64 @@
 nore%22 ) ) %7B%0A
+%09%09%09%09errorFunctions.add( functionName );%0A
 %09%09%09%09logger.w
@@ -3688,19 +3688,22 @@
  JS 
-ignore
+filter
  file
+ %7B%7D
  has
@@ -3728,46 +3728,16 @@
 %22, f
-unctionName );%0A%09%09%09%09errorFunctions.add(
+ilePath,
  fun

Improve logging by adding which JS file is at fault