// 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.pagefactory;

import java.lang.reflect.Field;
import java.time.Clock;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import org.openqa.selenium.NoSuchElementException;
import org.openqa.selenium.SearchContext;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.ui.SlowLoadableComponent;

/**
 * An element locator that will wait for the specified number of seconds for an element to appear,
 * rather than failing instantly if it's not present. This works by polling the UI on a regular
 * basis. The element returned will be present on the DOM, but may not actually be visible: override
 * {@link #isElementUsable(WebElement)} if this is important to you.
 *
 * <p>Because this class polls the interface on a regular basis, it is strongly recommended that
 * users avoid locating elements by XPath.
 */
public class AjaxElementLocator extends DefaultElementLocator {
  protected final int timeOutInSeconds;
  private final Clock clock;

  /**
   * Use this constructor in order to process custom annotations.
   *
   * @param context The context to use when finding the element
   * @param timeOutInSeconds How long to wait for the element to appear. Measured in seconds.
   * @param annotations AbstractAnnotations class implementation
   */
  public AjaxElementLocator(
      SearchContext context, int timeOutInSeconds, AbstractAnnotations annotations) {
    this(Clock.systemDefaultZone(), context, timeOutInSeconds, annotations);
  }

  public AjaxElementLocator(
      Clock clock, SearchContext context, int timeOutInSeconds, AbstractAnnotations annotations) {
    super(context, annotations);
    this.timeOutInSeconds = timeOutInSeconds;
    this.clock = clock;
  }

  /**
   * Main constructor.
   *
   * @param searchContext The context to use when finding the element
   * @param field The field representing this element
   * @param timeOutInSeconds How long to wait for the element to appear. Measured in seconds.
   */
  public AjaxElementLocator(SearchContext searchContext, Field field, int timeOutInSeconds) {
    this(Clock.systemDefaultZone(), searchContext, field, timeOutInSeconds);
  }

  public AjaxElementLocator(
      Clock clock, SearchContext searchContext, Field field, int timeOutInSeconds) {
    this(clock, searchContext, timeOutInSeconds, new Annotations(field));
  }

  /**
   * {@inheritDoc}
   *
   * <p>Will poll the interface on a regular basis until the element is present.
   */
  @Override
  public WebElement findElement() {
    SlowLoadingElement loadingElement = new SlowLoadingElement(clock, timeOutInSeconds);
    try {
      return loadingElement.get().getElement();
    } catch (NoSuchElementError e) {
      throw new NoSuchElementException(
          String.format("Timed out after %d seconds. %s", timeOutInSeconds, e.getMessage()),
          e.getCause());
    }
  }

  /**
   * {@inheritDoc}
   *
   * <p>Will poll the interface on a regular basis until at least one element is present.
   */
  @Override
  public List<WebElement> findElements() {
    SlowLoadingElementList list = new SlowLoadingElementList(clock, timeOutInSeconds);
    try {
      return list.get().getElements();
    } catch (NoSuchElementError e) {
      return new ArrayList<>();
    }
  }

  /**
   * By default, we sleep for 250ms between polls. You may override this method in order to change
   * how it sleeps.
   *
   * @return Duration to sleep in milliseconds
   */
  protected long sleepFor() {
    return 250;
  }

  /**
   * By default, elements are considered "found" if they are in the DOM. Override this method in
   * order to change whether you consider the element loaded. For example, perhaps you need the
   * element to be displayed:
   *
   * <pre>{@code
   * return element.isDisplayed();
   * }</pre>
   *
   * @param element The element to use
   * @return Whether it meets your criteria for "found"
   */
  protected boolean isElementUsable(WebElement element) {
    return true;
  }

  private class SlowLoadingElement extends SlowLoadableComponent<SlowLoadingElement> {
    private NoSuchElementException lastException;
    private WebElement element;

    public SlowLoadingElement(Clock clock, int timeOutInSeconds) {
      super(clock, Duration.ofSeconds(timeOutInSeconds));
    }

    @Override
    protected void load() {
      // Does nothing
    }

    @Override
    protected long sleepFor() {
      return AjaxElementLocator.this.sleepFor();
    }

    @Override
    protected void isLoaded() throws Error {
      try {
        element = AjaxElementLocator.super.findElement();
        if (!isElementUsable(element)) {
          throw new NoSuchElementException("Element is not usable");
        }
      } catch (NoSuchElementException e) {
        lastException = e;
        // Should use JUnit's AssertionError, but it may not be present
        throw new NoSuchElementError("Unable to locate the element", e);
      }
    }

    public NoSuchElementException getLastException() {
      return lastException;
    }

    public WebElement getElement() {
      return element;
    }
  }

  private class SlowLoadingElementList extends SlowLoadableComponent<SlowLoadingElementList> {
    private NoSuchElementException lastException;
    private List<WebElement> elements;

    public SlowLoadingElementList(Clock clock, int timeOutInSeconds) {
      super(clock, Duration.ofSeconds(timeOutInSeconds));
    }

    @Override
    protected void load() {
      // Does nothing
    }

    @Override
    protected long sleepFor() {
      return AjaxElementLocator.this.sleepFor();
    }

    @Override
    protected void isLoaded() throws Error {
      try {
        elements = AjaxElementLocator.super.findElements();
        if (elements.isEmpty()) {
          throw new NoSuchElementException("Unable to locate the element");
        }
        for (WebElement element : elements) {
          if (!isElementUsable(element)) {
            throw new NoSuchElementException("Element is not usable");
          }
        }
      } catch (NoSuchElementException e) {
        lastException = e;
        // Should use JUnit's AssertionError, but it may not be present
        throw new NoSuchElementError("Unable to locate the element", e);
      }
    }

    public NoSuchElementException getLastException() {
      return lastException;
    }

    public List<WebElement> getElements() {
      return elements;
    }
  }

  private static class NoSuchElementError extends Error {
    private NoSuchElementError(String message, Throwable throwable) {
      super(message, throwable);
    }
  }
}
