/*
 * (C) Copyright 2017 Boni Garcia (http://bonigarcia.github.io/)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */
package io.github.bonigarcia;

import static io.github.bonigarcia.SeleniumJupiter.config;
import static java.lang.invoke.MethodHandles.lookup;
import static java.util.Arrays.asList;
import static java.util.Optional.empty;
import static java.util.Optional.of;
import static org.slf4j.LoggerFactory.getLogger;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Parameter;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.List;
import java.util.Optional;
import java.util.StringTokenizer;

import org.apache.commons.lang3.StringUtils;
import org.openqa.selenium.Capabilities;
import org.openqa.selenium.remote.DesiredCapabilities;
import org.slf4j.Logger;

/**
 * Options/capabilities reader from annotated parameters or test instance to the
 * proper type (ChromeOptions, FirefoxOptions, Capabilities, etc).
 *
 * @author Boni Garcia (boni.gg@gmail.com)
 * @since 1.0.0
 */
public class AnnotationsReader {

    final Logger log = getLogger(lookup().lookupClass());

    public Optional<Capabilities> getCapabilities(Parameter parameter,
            Optional<Object> testInstance) throws IllegalAccessException {
        Optional<Capabilities> out = empty();
        DriverCapabilities driverCapabilities = parameter
                .getAnnotation(DriverCapabilities.class);

        Capabilities capabilities = null;
        if (driverCapabilities != null) {
            // Search first DriverCapabilities annotation in parameter
            capabilities = new DesiredCapabilities();
            for (String capability : driverCapabilities.value()) {
                Optional<List<String>> keyValue = getKeyValue(capability);
                if (keyValue.isPresent()) {
                    ((DesiredCapabilities) capabilities).setCapability(
                            keyValue.get().get(0), keyValue.get().get(1));
                }
            }
            out = of(capabilities);
        } else {
            // If not, search DriverCapabilities in any field
            Optional<Object> annotatedField = seekFieldAnnotatedWith(
                    testInstance, DriverCapabilities.class);
            if (annotatedField.isPresent()) {
                capabilities = (Capabilities) annotatedField.get();
                out = of(capabilities);
            }
        }
        return out;
    }

    public Optional<URL> getUrl(Parameter parameter,
            Optional<Object> testInstance)
            throws MalformedURLException, IllegalAccessException {
        Optional<URL> out = empty();
        String seleniumServerUrl = config().getSeleniumServerUrl();

        if (seleniumServerUrl != null && !seleniumServerUrl.isEmpty()) {
            out = of(new URL(seleniumServerUrl));
        } else {
            String urlValue = null;
            DriverUrl driverUrl = parameter.getAnnotation(DriverUrl.class);
            if (driverUrl != null) {
                // Search first DriverUrl annotation in parameter
                urlValue = driverUrl.value();
                out = of(new URL(urlValue));
            } else {
                // If not, search DriverUrl in any field
                Optional<Object> annotatedField = seekFieldAnnotatedWith(
                        testInstance, DriverUrl.class);
                if (annotatedField.isPresent()) {
                    urlValue = (String) annotatedField.get();
                    out = of(new URL(urlValue));
                }
            }
        }
        return out;
    }

    public boolean isBoolean(String s) {
        boolean isBool = s.equalsIgnoreCase("true")
                || s.equalsIgnoreCase("false");
        if (!isBool) {
            log.warn("Value {} is not boolean", s);
        }
        return isBool;
    }

    public boolean isNumeric(String s) {
        boolean numeric = StringUtils.isNumeric(s);
        if (!numeric) {
            log.warn("Value {} is not numeric", s);
        }
        return numeric;
    }

    public Object getOptionsFromAnnotatedField(Optional<Object> testInstance,
            Class<Options> annotationClass) throws IllegalAccessException {
        Object out = null;
        Optional<Object> annotatedField = seekFieldAnnotatedWith(testInstance,
                annotationClass);
        if (annotatedField.isPresent()) {
            out = annotatedField.get();
        }
        return out;
    }

    public Optional<Object> seekFieldAnnotatedWith(
            Optional<Object> testInstance,
            Class<? extends Annotation> annotation)
            throws IllegalAccessException {
        Optional<Object> out = empty();
        if (testInstance.isPresent()) {
            Object object = testInstance.get();
            Class<? extends Object> clazz = object.getClass();
            out = getField(annotation, clazz, object);

            // If annotation not present in class, look for it in the parent(s)
            Class<?> superclass;
            while ((superclass = clazz.getSuperclass()) != Object.class) {
                out = getField(annotation, superclass, object);
                if (out.isPresent()) {
                    break;
                }
                clazz = clazz.getSuperclass();
            }
        }
        return out;
    }

    private Optional<Object> getField(Class<? extends Annotation> annotation,
            Class<? extends Object> clazz, Object object)
            throws IllegalAccessException {
        Field[] declaredFields = clazz.getDeclaredFields();
        for (Field field : declaredFields) {
            if (field.isAnnotationPresent(annotation)) {
                field.setAccessible(true);
                return of(field.get(object));
            }
        }
        return empty();
    }

    public Optional<DockerBrowser> getDocker(Parameter parameter) {
        Optional<DockerBrowser> out = empty();
        DockerBrowser dockerBrowser = parameter
                .getAnnotation(DockerBrowser.class);
        if (dockerBrowser != null) {
            out = of(dockerBrowser);
        }
        return out;
    }

    public Optional<List<String>> getKeyValue(String keyValue) {
        StringTokenizer st = new StringTokenizer(keyValue, "=");
        if (st.countTokens() != 2) {
            log.warn("Invalid format in {} (expected key=value)", keyValue);
            return empty();
        }
        return of(asList(st.nextToken(), st.nextToken()));
    }

}

@@ -5467,17 +5467,56 @@
 bject);%0A
-%0A
+            if (!out.isPresent()) %7B%0A    
         
@@ -5576,16 +5576,35 @@
 t in the
+%0A                //
  parent(
@@ -5606,16 +5606,20 @@
 rent(s)%0A
+    
         
@@ -5655,16 +5655,20 @@
         
+   
+ 
 while ((
@@ -5730,32 +5730,36 @@
                 
+    
 out = getField(a
@@ -5802,24 +5802,28 @@
             
+  
+  
 if (out.isPr
@@ -5825,32 +5825,36 @@
 .isPresent()) %7B%0A
+    
                 
@@ -5872,34 +5872,42 @@
                 
-%7D%0A
+    %7D%0A    
                 
@@ -5929,32 +5929,50 @@
 etSuperclass();%0A
+                %7D%0A
             %7D%0A  

Fix issue #20: @Options annotation is ignored when there is a superclass