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

import java.time.Clock;
import java.time.Duration;
import java.time.Instant;

/**
 * A {@link LoadableComponent} which might not have finished loading when load() returns. After a
 * call to load(), the isLoaded() method should continue to fail until the component has fully
 * loaded.
 *
 * <pre class="code">
 * new SlowHypotheticalComponent().get();
 * </pre>
 *
 * @param <T> The type to be returned (normally the subclass' type)
 */
public abstract class SlowLoadableComponent<T extends LoadableComponent<T>>
    extends LoadableComponent<T> {

  private final Clock clock;
  private final Duration timeOutInSeconds;

  public SlowLoadableComponent(java.time.Clock clock, int timeOutInSeconds) {
    this.clock = clock;
    this.timeOutInSeconds = Duration.ofSeconds(timeOutInSeconds);
  }

  @Override
  @SuppressWarnings("unchecked")
  public T get() {
    try {
      isLoaded();
      return (T) this;
    } catch (Error e) {
      load();
    }

    Instant end = clock.instant().plus(timeOutInSeconds);

    while (clock.instant().isBefore(end)) {
      try {
        isLoaded();
        return (T) this;
      } catch (Error e) {
        // Not a problem, we could still be loading
      }

      isError();

      waitFor();
    }

    isLoaded();
    return (T) this;
  }

  /**
   * Check for well known error cases, which would mean that loading has finished, but an error
   * condition was seen. If an error has occurred throw an Error, possibly by using JUnit's
   * Assert.assert* methods
   *
   * @throws Error When a well-known error condition has caused the load to fail
   */
  protected void isError() throws Error {
    // no-op by default
  }

  protected long sleepFor() {
    return 200;
  }

  private void waitFor() {
    try {
      Thread.sleep(sleepFor());
    } catch (InterruptedException e) {
      Thread.currentThread().interrupt();
      throw new AssertionError(e);
    }
  }
}
