package io.fluentlenium.configuration;

import io.fluentlenium.utils.ReflectionUtils;
import org.openqa.selenium.Capabilities;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.remote.DesiredCapabilities;

import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * A simple {@link WebDriverFactory} that create {@link WebDriver} instances using reflection.
 */
public class ReflectiveWebDriverFactory implements WebDriverFactory, ReflectiveFactory, FactoryNames {
    protected String name;
    protected Object[] args;
    protected String webDriverClassName;
    protected Class<? extends WebDriver> webDriverClass;
    protected boolean available;

    /**
     * Creates a new reflective web driver factory.
     *
     * @param name               factory name
     * @param webDriverClassName web driver class name
     * @param args               web driver class constructor arguments
     */
    public ReflectiveWebDriverFactory(String name, String webDriverClassName, Object... args) {
        this.name = name;
        this.webDriverClassName = webDriverClassName;
        this.args = args;
        try {
            webDriverClass = (Class<? extends WebDriver>) Class.forName(webDriverClassName);
            available = WebDriver.class.isAssignableFrom(webDriverClass);
        } catch (ClassNotFoundException e) {
            available = false;
        }
    }

    /**
     * Creates a new reflective web driver factory.
     *
     * @param name           factory name
     * @param webDriverClass web driver class
     * @param args           web driver class constructor arguments
     */
    public ReflectiveWebDriverFactory(String name, Class<? extends WebDriver> webDriverClass, Object... args) {
        this.name = name;
        this.webDriverClass = webDriverClass;
        this.args = args;
        webDriverClassName = webDriverClass.getName();
        available = WebDriver.class.isAssignableFrom(this.webDriverClass);
    }

    /**
     * Get web driver class.
     *
     * @return web driver class
     */
    public Class<? extends WebDriver> getWebDriverClass() {
        return webDriverClass;
    }

    @Override
    public boolean isAvailable() {
        return available;
    }

    /**
     * Creates new default capabilities.
     *
     * @return default capabilities
     */
    protected DesiredCapabilities newDefaultCapabilities() {
        return null;
    }

    @Override
    public WebDriver newWebDriver(Capabilities capabilities, ConfigurationProperties configuration) {
        if (!available) {
            throw new ConfigurationException("WebDriver " + webDriverClassName + " is not available.");
        }

        try {
            DesiredCapabilities defaultCapabilities = newDefaultCapabilities();
            if (defaultCapabilities != null) {
                defaultCapabilities.merge(capabilities);
                capabilities = defaultCapabilities;
            }

            if (capabilities != null && !capabilities.asMap().isEmpty()) {
                ArrayList<Object> argsList = new ArrayList<>(Arrays.asList(args));
                argsList.add(0, capabilities);
                try {
                    return newInstance(webDriverClass, configuration, argsList.toArray());
                } catch (NoSuchMethodException e) { // NOPMD EmptyCatchBlock
                    // Ignore capabilities.
                }
            }
            return newInstance(webDriverClass, configuration, args);
        } catch (InstantiationException | IllegalAccessException | NoSuchMethodException |
                 InvocationTargetException e) {
            throw new ConfigurationException("Can't create new WebDriver instance", e);
        }
    }

    /**
     * Creates a new instance of web driver.
     *
     * @param webDriverClass web driver class
     * @param configuration  configuration
     * @param args           web driver class constructor arguments
     * @return new web driver instance
     * @throws NoSuchMethodException     if a matching method is not found.
     * @throws IllegalAccessException    if this {@code Constructor} object
     *                                   is enforcing Java language access control and the underlying
     *                                   constructor is inaccessible.
     * @throws InstantiationException    if the class that declares the
     *                                   underlying constructor represents an abstract class.
     * @throws InvocationTargetException if the underlying constructor
     *                                   throws an exception.
     */
    protected WebDriver newInstance(Class<? extends WebDriver> webDriverClass, ConfigurationProperties configuration,
                                    Object... args)
            throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException {
        return ReflectionUtils.newInstance(webDriverClass, args);
    }

    @Override
    public String[] getNames() {
        List<String> names = new ArrayList<>(Arrays.asList(name));
        if (webDriverClass != null) {
            names.add(webDriverClass.getName());
            names.add(webDriverClass.getSimpleName());
        }
        return names.toArray(new String[names.size()]);
    }
}
