/*
 *  Copyright 2022 The WebRTC project authors. All Rights Reserved.
 *
 *  Use of this source code is governed by a BSD-style license
 *  that can be found in the LICENSE file in the root of the source
 *  tree. An additional intellectual property rights grant can be found
 *  in the file PATENTS.  All contributing project authors may
 *  be found in the AUTHORS file in the root of the source tree.
 */

package org.webrtc;

import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.Message;
import androidx.annotation.GuardedBy;
import androidx.annotation.Nullable;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;

import org.webrtc.EglBase.EglConnection;

/** EGL graphics thread that allows multiple clients to share the same underlying EGLContext. */
public class EglThread implements RenderSynchronizer.Listener {
  /** Callback for externally managed reference count. */
  public interface ReleaseMonitor {
    /**
     * Called by EglThread when a client releases its reference. Returns true when there are no more
     * references and resources should be released.
     */
    boolean onRelease(EglThread eglThread);
  }

  /** Interface for clients to schedule rendering updates that will run synchronized. */
  public interface RenderUpdate {

    /**
     * Called by EglThread when the rendering window is open. `runsInline` is true when the update
     * is executed directly while the client schedules the update.
     */
    void update(boolean runsInline);
  }

  public static EglThread create(
      @Nullable ReleaseMonitor releaseMonitor,
      @Nullable final EglBase.Context sharedContext,
      final int[] configAttributes,
      @Nullable RenderSynchronizer renderSynchronizer) {
    final HandlerThread renderThread = new HandlerThread("EglThread");
    renderThread.start();
    HandlerWithExceptionCallbacks handler =
        new HandlerWithExceptionCallbacks(renderThread.getLooper());

    // Not creating the EGLContext on the thread it will be used on seems to cause issues with
    // creating window surfaces on certain devices. So keep the same legacy behavior as EglRenderer
    // and create the context on the render thread.
    EglConnection eglConnection = ThreadUtils.invokeAtFrontUninterruptibly(handler, () -> {
      // If sharedContext is null, then texture frames are disabled. This is typically for old
      // devices that might not be fully spec compliant, so force EGL 1.0 since EGL 1.4 has
      // caused trouble on some weird devices.
      if (sharedContext == null) {
        return EglConnection.createEgl10(configAttributes);
      } else {
        return EglConnection.create(sharedContext, configAttributes);
      }
    });

    return new EglThread(
        releaseMonitor != null ? releaseMonitor : eglThread -> true,
        handler,
        eglConnection,
        renderSynchronizer);
  }

  public static EglThread create(
      @Nullable ReleaseMonitor releaseMonitor,
      @Nullable final EglBase.Context sharedContext,
      final int[] configAttributes) {
    return create(releaseMonitor, sharedContext, configAttributes, /* renderSynchronizer= */ null);
  }

  /**
   * Handler that triggers callbacks when an uncaught exception happens when handling a message.
   */
  private static class HandlerWithExceptionCallbacks extends Handler {
    private final Object callbackLock = new Object();
    @GuardedBy("callbackLock") private final List<Runnable> exceptionCallbacks = new ArrayList<>();

    public HandlerWithExceptionCallbacks(Looper looper) {
      super(looper);
    }

    @Override
    public void dispatchMessage(Message msg) {
      try {
        super.dispatchMessage(msg);
      } catch (Exception e) {
        Logging.e("EglThread", "Exception on EglThread", e);
        synchronized (callbackLock) {
          for (Runnable callback : exceptionCallbacks) {
            callback.run();
          }
        }
        throw e;
      }
    }

    public void addExceptionCallback(Runnable callback) {
      synchronized (callbackLock) {
        exceptionCallbacks.add(callback);
      }
    }

    public void removeExceptionCallback(Runnable callback) {
      synchronized (callbackLock) {
        exceptionCallbacks.remove(callback);
      }
    }
  }

  private final ReleaseMonitor releaseMonitor;
  private final HandlerWithExceptionCallbacks handler;
  private final EglConnection eglConnection;
  private final RenderSynchronizer renderSynchronizer;
  // Pending render updates if they're overwritten per renderer.
  private final Map<UUID, RenderUpdate> pendingRenderUpdates = new HashMap<>();
  // Pending render updates if they're in a global queue.
  private final List<RenderUpdate> pendingRenderUpdatesQueued = new ArrayList<>();
  private boolean renderWindowOpen = true;

  private EglThread(
      ReleaseMonitor releaseMonitor,
      HandlerWithExceptionCallbacks handler,
      EglConnection eglConnection,
      RenderSynchronizer renderSynchronizer) {
    this.releaseMonitor = releaseMonitor;
    this.handler = handler;
    this.eglConnection = eglConnection;
    this.renderSynchronizer = renderSynchronizer;
    if (renderSynchronizer != null) {
      renderSynchronizer.registerListener(this);
    }
  }

  public void release() {
    if (!releaseMonitor.onRelease(this)) {
      // Thread is still in use, do not release yet.
      return;
    }

    if (renderSynchronizer != null) {
      renderSynchronizer.removeListener(this);
    }

    handler.post(eglConnection::release);
    handler.getLooper().quitSafely();
  }

  /**
   * Creates an EglBase instance with the EglThread's EglConnection. This method can be called on
   * any thread, but the returned EglBase instance should only be used on this EglThread's Handler.
   */
  public EglBase createEglBaseWithSharedConnection() {
    return EglBase.create(eglConnection);
  }

  /**
   * Returns the Handler to interact with Gl/EGL on. Callers need to make sure that their own
   * EglBase is current on the handler before running any graphics operations since the EglThread
   * can be shared by multiple clients.
   */
  public Handler getHandler() {
    return handler;
  }

  /**
   * Adds a callback that will be called on the EGL thread if there is an exception on the thread.
   */
  public void addExceptionCallback(Runnable callback) {
    handler.addExceptionCallback(callback);
  }

  /**
   * Removes a previously added exception callback.
   */
  public void removeExceptionCallback(Runnable callback) {
    handler.removeExceptionCallback(callback);
  }

  /**
   * Schedules a render update (like swapBuffers) to be run in sync with other updates on the next
   * open render window. If the render window is currently open the update will run immediately.
   * This method must be called on the EglThread during a render pass.
   *
   * @param id a unique id of the renderer that scheduled this render update.
   */
  public void scheduleRenderUpdate(UUID id, RenderUpdate update) {
    if (renderWindowOpen) {
      update.update(/* runsInline = */ true);
    } else {
      pendingRenderUpdates.put(id, update);
    }
  }

  // The same as above, except that the ids are randomly generated for each frame.
  // So this essentially becomes a queue of frame updates.
  @Deprecated
  public void scheduleRenderUpdate(RenderUpdate update) {
    if (renderWindowOpen) {
      update.update(/* runsInline = */ true);
    } else {
      pendingRenderUpdatesQueued.add(update);
    }
  }

  @Override
  public void onRenderWindowOpen() {
    handler.post(
        () -> {
          renderWindowOpen = true;
          for (RenderUpdate update : pendingRenderUpdates.values()) {
            update.update(/* runsInline = */ false);
          }
          pendingRenderUpdates.clear();
          for (RenderUpdate update: pendingRenderUpdatesQueued) {
            update.update(/* runsInline = */ false);
          }
          pendingRenderUpdatesQueued.clear();
        });
  }

  @Override
  public void onRenderWindowClose() {
    handler.post(() -> renderWindowOpen = false);
  }
}
