/**
 * JBoss, Home of Professional Open Source
 * Copyright 2012, Red Hat, Inc. and individual contributors
 * by the @authors tag. See the copyright.txt in the distribution for a
 * full listing of individual contributors.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package org.jboss.arquillian.graphene.context;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Callable;

import org.jboss.arquillian.drone.api.annotation.Default;
import org.jboss.arquillian.graphene.configuration.GrapheneConfiguration;
import org.jboss.arquillian.graphene.enricher.SearchContextInterceptor;
import org.jboss.arquillian.graphene.enricher.StaleElementInterceptor;
import org.jboss.arquillian.graphene.page.extension.PageExtensionInstallatorProvider;
import org.jboss.arquillian.graphene.page.extension.PageExtensionRegistry;
import org.jboss.arquillian.graphene.page.extension.PageExtensionRegistryImpl;
import org.jboss.arquillian.graphene.page.extension.RemotePageExtensionInstallatorProvider;
import org.jboss.arquillian.graphene.proxy.GrapheneContextualHandler;
import org.jboss.arquillian.graphene.proxy.GrapheneProxy;
import org.jboss.arquillian.graphene.proxy.GrapheneProxyHandler;
import org.jboss.arquillian.graphene.proxy.GrapheneProxyInstance;
import org.jboss.arquillian.graphene.proxy.GrapheneProxyUtil;
import org.openqa.selenium.JavascriptExecutor;
import org.openqa.selenium.WebDriver;

/**
 * @author <a href="mailto:jpapouse@redhat.com">Jan Papousek</a>
 */
public class GrapheneContextImpl extends ExtendedGrapheneContext {

    private final GrapheneConfiguration configuration;
    private final WebDriver webDriver;
    private final Class<?> qualifier;
    /**
     * This field contains a reference to the context associated to the current
     * active browser actions. {@link LazyContext} instance is placed here.
     */
    private static final BrowserLocal<GrapheneContext> CURRENT_CONTEXT = new BrowserLocal<GrapheneContext>();
    /**
     * This field contains a map qualifier => real contexts. Real context is
     * an instance of {@link GrapheneContext} holding current {@link WebDriver},
     * qualifier and {@link GrapheneConfiguration}. Contexts from this field are
     * used by lazy contexts.
     */
    private static ThreadLocal<Map<Class<?>, GrapheneContext>> ALL_CONTEXTS = new ThreadLocal<Map<Class<?>, GrapheneContext>>() {
        @Override
        protected Map<Class<?>, GrapheneContext> initialValue() {
            return new HashMap<Class<?>, GrapheneContext>();
        }
    };
    /**
     * This field contains a map qualifier => lazy context. Lazy context uses
     * {@link #ALL_CONTEXTS} field for its method invocation and is able to create
     * a proxy for {@link WebDriver}.
     */
    private static ThreadLocal<Map<Class<?>, LazyContext>> LAZY_CONTEXTS = new ThreadLocal<Map<Class<?>, LazyContext>>() {
        @Override
        protected Map<Class<?>, LazyContext> initialValue() {
            return new HashMap<Class<?>, LazyContext>();
        }
    };

    private GrapheneContextImpl(GrapheneConfiguration configuration, WebDriver webDriver, Class<?> qualifier) {
        this.configuration = configuration;
        this.webDriver = webDriver;
        this.qualifier = qualifier;
    }

    public BrowserActions getBrowserActions() {
        return null;
    }

    public GrapheneConfiguration getConfiguration() {
        return configuration;
    }

    public PageExtensionInstallatorProvider getPageExtensionInstallatorProvider() {
        return null;
    }

    public PageExtensionRegistry getPageExtensionRegistry() {
        return null;
    }

    /**
     * If the {@link WebDriver} instance is not available yet, the returned
     * proxy just implements {@link WebDriver} interface. If the {@link WebDriver}
     * instance is available, its class is used to create a proxy, so the proxy extends it.
     *
     * @param interfaces interfaces which should be implemented by the returned {@link WebDriver}
     * @return proxy for the {@link WebDriver} held in the context
     */
    public WebDriver getWebDriver(Class<?>... interfaces) {
        return webDriver;
    }

    /**
     * @return qualifier identifying the context.
     */
    public Class<?> getQualifier() {
        return qualifier;
    }

    static class StaticInterfaceImplementation implements StaticInterface {

        // static methods
        @Override
        public GrapheneContext lastContext() {
            return CURRENT_CONTEXT.getLast();
        }

        /**
         * Get context associated to the given qualifier. If the {@link Default}
         * qualifier is given, the returned context tries to resolves the active context before
         * each method invocation. If the active context is available, the returned context
         * behaves like the active one.
         */
        @Override
        public GrapheneContext getContextFor(Class<?> qualifier) {
            if (qualifier == null) {
                throw new IllegalArgumentException("The parameter 'qualifer' is null.");
            }
            LazyContext context = (LazyContext) LAZY_CONTEXTS.get().get(qualifier);
            if (context == null) {
                try {
                    context = new LazyContext(qualifier, new BrowserActions(qualifier.getName()));
                    context.handler = GrapheneContextualHandler.forFuture(context, context.getFutureTarget());
                    GrapheneProxyInstance proxy = (GrapheneProxyInstance) context.getWebDriver();
                    proxy.registerInterceptor(new SearchContextInterceptor());
                    proxy.registerInterceptor(new StaleElementInterceptor());
                    context.installatorProvider = new RemotePageExtensionInstallatorProvider(context.registry, (JavascriptExecutor) context.getWebDriver(JavascriptExecutor.class));
                    final GrapheneContext finalContext = context;
                    context.getBrowserActions().performAction(new Callable<Void>() {
                        @Override
                        public Void call() throws Exception {
                            CURRENT_CONTEXT.set(finalContext);
                            return null;
                        }
                    });
                    LAZY_CONTEXTS.get().put(qualifier, context);
                } catch (Exception e) {
                    throw new IllegalStateException("Can't create a lazy context for " + qualifier.getName() + " qualifier.", e);
                }
            }
            return context;
        }

        /**
         * Creates a new context for the given webdriver, configuration and qualifier.
         * <strong>When you create the context, you are responsible to invoke {@link #removeContextFor(java.lang.Class) }
         * after the context is no longer valid.</strong>
         *
         * @return created context
         * @see #getContextFor(java.lang.Class)
         * @see #removeContextFor(java.lang.Class)
         */
        @Override
        public GrapheneContext setContextFor(GrapheneConfiguration configuration, WebDriver driver, Class<?> qualifier) {
            GrapheneContext grapheneContext = new GrapheneContextImpl(configuration, driver, qualifier);
            ALL_CONTEXTS.get().put(qualifier, grapheneContext);
            return getContextFor(qualifier);
        }

        /**
         * Removes the context associated to the given qualifier.
         * @param qualifier
         * @see #setContextFor(org.jboss.arquillian.graphene.configuration.GrapheneConfiguration, org.openqa.selenium.WebDriver, java.lang.Class)
         */
        @Override
        public void removeContextFor(Class<?> qualifier) {
            final GrapheneContext context = LAZY_CONTEXTS.get().get(qualifier);
            if (context != null) {
                try {
                    ((LazyContext) context).getBrowserActions().performAction(new Callable<Void>() {
                            @Override
                            public Void call() throws Exception {
                                CURRENT_CONTEXT.remove();
                                return null;
                            }
                        });
                } catch (Exception ignored) {
                }
                LAZY_CONTEXTS.get().remove(qualifier);
            }
            ALL_CONTEXTS.get().remove(qualifier);
        }
    }

    private static class LazyContext extends GrapheneContextImpl {

        private final Class<?> qualifier;
        private final PageExtensionRegistry registry;
        private final BrowserActions browserActions;
        private PageExtensionInstallatorProvider installatorProvider;
        private GrapheneProxyHandler handler;

        public LazyContext(Class<?> qualifier, BrowserActions browserActions) {
            super(null, null, null);
            this.qualifier = qualifier;
            this.browserActions = browserActions;
            this.registry = new PageExtensionRegistryImpl();
        }

        @Override
        public BrowserActions getBrowserActions() {
            LazyContext context = LAZY_CONTEXTS.get().get(getQualifier());
            if (context == null) {
                return browserActions;
            } else {
                return context.browserActions;
            }
        }

        @Override
        public GrapheneConfiguration getConfiguration() {
            return getContext(true).getConfiguration();
        }

        @Override
        public PageExtensionInstallatorProvider getPageExtensionInstallatorProvider() {
            LazyContext context = LAZY_CONTEXTS.get().get(getQualifier());
            if (context == null) {
                return installatorProvider;
            } else {
                return context.installatorProvider;
            }
        }

        @Override
        public PageExtensionRegistry getPageExtensionRegistry() {
            LazyContext context = LAZY_CONTEXTS.get().get(getQualifier());
            if (context == null) {
                return registry;
            } else {
                return context.registry;
            }
        }

        @Override
        public Class<?> getQualifier() {
            if (Default.class.equals(qualifier)) {
                GrapheneContext context = getContext(false);
                if (context == null) {
                    return qualifier;
                } if (context instanceof LazyContext) {
                    return ((LazyContext) context).qualifier;
                } else {
                    return context.getQualifier();
                }
            } else {
                return qualifier;
            }
        }

        @Override
        public WebDriver getWebDriver(Class<?>... interfaces) {
            GrapheneContext context = getContext(false);
            if (context == null) {
                return GrapheneProxy.getProxyForHandler(handler, WebDriver.class, interfaces);
            } else if (GrapheneProxyUtil.isProxy(context.getWebDriver())) {
                Class<?> superclass = context.getWebDriver().getClass().getSuperclass();
                if (superclass != null && !GrapheneProxyUtil.isProxy(superclass) && WebDriver.class.isAssignableFrom(superclass)) {
                    return GrapheneProxy.getProxyForHandler(handler, context.getWebDriver().getClass().getSuperclass(), interfaces);
                } else {
                    return GrapheneProxy.getProxyForHandler(handler, WebDriver.class, interfaces);
                }
            } else {
                return GrapheneProxy.getProxyForHandler(handler, context.getWebDriver().getClass(), interfaces);
            }
        }

        protected GrapheneContext getContext(boolean exception) {
            GrapheneContext context = null;
            if (qualifier.equals(Default.class)) {
                try {
                    context = CURRENT_CONTEXT.get();
                } catch (Exception ignored) {
                }
            }
            if (context == null || context.equals(this)) {
                context = ALL_CONTEXTS.get().get(qualifier);
            }
            if (context == null && exception) {
                throw new IllegalStateException("There is no context available for qualifier " + qualifier.getName() + ". Available contexts are " + ALL_CONTEXTS.get().keySet() + ".");
            }
            return context;
        }

        protected GrapheneProxy.FutureTarget getFutureTarget() {
            return new GrapheneProxy.FutureTarget() {
                @Override
                public Object getTarget() {
                    return getContext(true).getWebDriver();
                }
            };
        }
    }
}
