// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package io.flutter.view;

import android.annotation.TargetApi;
import android.hardware.display.DisplayManager;
import android.view.Choreographer;
import android.view.Display;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import io.flutter.embedding.engine.FlutterJNI;

// TODO(mattcarroll): add javadoc.
public class VsyncWaiter {
  @TargetApi(17)
  class DisplayListener implements DisplayManager.DisplayListener {
    DisplayListener(DisplayManager displayManager) {
      this.displayManager = displayManager;
    }

    private DisplayManager displayManager;

    void register() {
      displayManager.registerDisplayListener(this, null);
    }

    @Override
    public void onDisplayAdded(int displayId) {}

    @Override
    public void onDisplayRemoved(int displayId) {}

    @Override
    public void onDisplayChanged(int displayId) {
      if (displayId == Display.DEFAULT_DISPLAY) {
        final Display primaryDisplay = displayManager.getDisplay(Display.DEFAULT_DISPLAY);
        float fps = primaryDisplay.getRefreshRate();
        VsyncWaiter.this.refreshPeriodNanos = (long) (1000000000.0 / fps);
        VsyncWaiter.this.flutterJNI.setRefreshRateFPS(fps);
      }
    }
  }

  private static VsyncWaiter instance;
  private static DisplayListener listener;
  private long refreshPeriodNanos = -1;
  private FlutterJNI flutterJNI;
  private FrameCallback frameCallback = new FrameCallback(0);

  @NonNull
  public static VsyncWaiter getInstance(float fps, @NonNull FlutterJNI flutterJNI) {
    if (instance == null) {
      instance = new VsyncWaiter(flutterJNI);
    }
    flutterJNI.setRefreshRateFPS(fps);
    instance.refreshPeriodNanos = (long) (1000000000.0 / fps);
    return instance;
  }

  @TargetApi(17)
  @NonNull
  public static VsyncWaiter getInstance(
      @NonNull DisplayManager displayManager, @NonNull FlutterJNI flutterJNI) {
    if (instance == null) {
      instance = new VsyncWaiter(flutterJNI);
    }
    if (listener == null) {
      listener = instance.new DisplayListener(displayManager);
      listener.register();
    }
    if (instance.refreshPeriodNanos == -1) {
      final Display primaryDisplay = displayManager.getDisplay(Display.DEFAULT_DISPLAY);
      float fps = primaryDisplay.getRefreshRate();
      instance.refreshPeriodNanos = (long) (1000000000.0 / fps);
      flutterJNI.setRefreshRateFPS(fps);
    }
    return instance;
  }

  // For tests, to reset the singleton between tests.
  @VisibleForTesting
  public static void reset() {
    instance = null;
    listener = null;
  }

  private class FrameCallback implements Choreographer.FrameCallback {

    private long cookie;

    FrameCallback(long cookie) {
      this.cookie = cookie;
    }

    @Override
    public void doFrame(long frameTimeNanos) {
      long delay = System.nanoTime() - frameTimeNanos;
      if (delay < 0) {
        delay = 0;
      }
      flutterJNI.onVsync(delay, refreshPeriodNanos, cookie);
      frameCallback = this;
    }
  }

  private final FlutterJNI.AsyncWaitForVsyncDelegate asyncWaitForVsyncDelegate =
      new FlutterJNI.AsyncWaitForVsyncDelegate() {

        private Choreographer.FrameCallback obtainFrameCallback(final long cookie) {
          if (frameCallback != null) {
            frameCallback.cookie = cookie;
            FrameCallback ret = frameCallback;
            frameCallback = null;
            return ret;
          }
          return new FrameCallback(cookie);
        }

        @Override
        public void asyncWaitForVsync(long cookie) {
          Choreographer.getInstance().postFrameCallback(obtainFrameCallback(cookie));
        }
      };

  private VsyncWaiter(@NonNull FlutterJNI flutterJNI) {
    this.flutterJNI = flutterJNI;
  }

  public void init() {
    flutterJNI.setAsyncWaitForVsyncDelegate(asyncWaitForVsyncDelegate);
  }
}
