// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements.  See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership.  The SFC licenses this file
// to you 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 org.openqa.selenium.support;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import org.openqa.selenium.SearchContext;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.support.pagefactory.DefaultElementLocatorFactory;
import org.openqa.selenium.support.pagefactory.DefaultFieldDecorator;
import org.openqa.selenium.support.pagefactory.ElementLocatorFactory;
import org.openqa.selenium.support.pagefactory.FieldDecorator;

/**
 * Factory class to make using Page Objects simpler and easier.
 *
 * @see <a href="https://github.com/SeleniumHQ/selenium/wiki/PageObjects">Page Objects Wiki</a>
 */
public class PageFactory {
  /**
   * Instantiate an instance of the given class, and set a lazy proxy for each of the WebElement and
   * List&lt;WebElement&gt; fields that have been declared, assuming that the field name is also the
   * HTML element's "id" or "name". This means that for the class:
   *
   * <pre><code> public class Page { private WebElement submit; } </code></pre>
   *
   * there will be an element that can be located using the xpath expression "//*[@id='submit']" or
   * "//*[@name='submit']"
   *
   * <p>By default, the element or the list is looked up each and every time a method is called upon
   * it. To change this behaviour, simply annotate the field with the {@link CacheLookup}. To change
   * how the element is located, use the {@link FindBy} annotation.
   *
   * <p>This method will attempt to instantiate the class given to it, preferably using a
   * constructor which takes a WebDriver instance as its only argument or falling back on a no-arg
   * constructor. An exception will be thrown if the class cannot be instantiated.
   *
   * @param searchContext The search context that will be used to look up the elements
   * @param pageClassToProxy A class which will be initialised.
   * @param <T> Class of the PageObject
   * @return An instantiated instance of the class with WebElement and List&lt;WebElement&gt; fields
   *     proxied
   * @see FindBy
   * @see CacheLookup
   */
  public static <T> T initElements(SearchContext searchContext, Class<T> pageClassToProxy) {
    T page = instantiatePage(searchContext, pageClassToProxy);
    initElements(searchContext, page);
    return page;
  }

  /**
   * As {@link #initElements(SearchContext, Class)} but will only replace the fields of an already
   * instantiated Page Object.
   *
   * @param searchContext The driver that will be used to look up the elements
   * @param page The object with WebElement and List&lt;WebElement&gt; fields that should be
   *     proxied.
   */
  public static void initElements(SearchContext searchContext, Object page) {
    initElements(new DefaultElementLocatorFactory(searchContext), page);
  }

  /**
   * Similar to the other "initElements" methods, but takes an {@link ElementLocatorFactory} which
   * is used for providing the mechanism for finding elements. If the ElementLocatorFactory returns
   * null then the field won't be decorated.
   *
   * @param factory The factory to use
   * @param page The object to decorate the fields of
   */
  public static void initElements(ElementLocatorFactory factory, Object page) {
    initElements(new DefaultFieldDecorator(factory), page);
  }

  /**
   * Similar to the other "initElements" methods, but takes an {@link FieldDecorator} which is used
   * for decorating each of the fields.
   *
   * @param decorator the decorator to use
   * @param page The object to decorate the fields of
   */
  public static void initElements(FieldDecorator decorator, Object page) {
    Class<?> proxyIn = page.getClass();
    while (proxyIn != Object.class) {
      proxyFields(decorator, page, proxyIn);
      proxyIn = proxyIn.getSuperclass();
    }
  }

  private static void proxyFields(FieldDecorator decorator, Object page, Class<?> proxyIn) {
    Field[] fields = proxyIn.getDeclaredFields();
    for (Field field : fields) {
      Object value = decorator.decorate(page.getClass().getClassLoader(), field);
      if (value != null) {
        try {
          field.setAccessible(true);
          field.set(page, value);
        } catch (IllegalAccessException e) {
          throw new RuntimeException(e);
        }
      }
    }
  }

  private static <T> T instantiatePage(SearchContext searchContext, Class<T> pageClassToProxy) {
    try {
      try {
        Constructor<T> constructor = pageClassToProxy.getConstructor(WebDriver.class);
        return constructor.newInstance(searchContext);
      } catch (NoSuchMethodException e) {
        return pageClassToProxy.getDeclaredConstructor().newInstance();
      }
    } catch (ReflectiveOperationException e) {
      throw new RuntimeException(e);
    }
  }
}
