/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

package org.mozilla.gecko;

import static android.content.Context.UI_MODE_SERVICE;

import android.app.UiModeManager;
import android.app.UiModeManager.ContrastChangeListener;
import android.content.BroadcastReceiver;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.res.Configuration;
import android.database.ContentObserver;
import android.hardware.input.InputManager;
import android.net.Uri;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.provider.Settings;
import android.util.Log;
import android.view.InputDevice;
import androidx.annotation.RequiresApi;
import org.mozilla.gecko.annotation.WrapForJNI;
import org.mozilla.gecko.util.InputDeviceUtils;
import org.mozilla.gecko.util.ThreadUtils;

public class GeckoSystemStateListener implements InputManager.InputDeviceListener {
  private static final String LOGTAG = "SystemStateListener";

  private static final GeckoSystemStateListener listenerInstance = new GeckoSystemStateListener();

  private boolean mInitialized;
  private ContentObserver mContentObserver;
  private static Context sApplicationContext;
  private InputManager mInputManager;
  private boolean mIsNightMode;
  private BroadcastReceiver mBroadcastReceiver;
  private IntentFilter mIntentFilter;
  private volatile int mAllPointerCapabilities;
  private volatile int mPointingDeviceKinds;

  @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
  private ContrastChangeListener mContrastChangeListener;

  public static GeckoSystemStateListener getInstance() {
    return listenerInstance;
  }

  private GeckoSystemStateListener() {}

  public synchronized void initialize(final Context context) {
    if (mInitialized) {
      Log.w(LOGTAG, "Already initialized!");
      return;
    }
    mInputManager = (InputManager) context.getSystemService(Context.INPUT_SERVICE);
    mInputManager.registerInputDeviceListener(listenerInstance, ThreadUtils.getUiHandler());

    sApplicationContext = context;
    final ContentResolver contentResolver = sApplicationContext.getContentResolver();
    final Uri animationSetting = Settings.System.getUriFor(Settings.Global.ANIMATOR_DURATION_SCALE);
    mContentObserver =
        new ContentObserver(new Handler(Looper.getMainLooper())) {
          @Override
          public void onChange(final boolean selfChange) {
            onDeviceChanged();
          }
        };
    contentResolver.registerContentObserver(animationSetting, false, mContentObserver);

    final Uri invertSetting =
        Settings.Secure.getUriFor(Settings.Secure.ACCESSIBILITY_DISPLAY_INVERSION_ENABLED);
    contentResolver.registerContentObserver(invertSetting, false, mContentObserver);

    final Uri textContrastSetting =
        Settings.Secure.getUriFor(
            /*Settings.Secure.ACCESSIBILITY_HIGH_TEXT_CONTRAST_ENABLED*/ "high_text_contrast_enabled");
    contentResolver.registerContentObserver(textContrastSetting, false, mContentObserver);

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
      final UiModeManager uiModeManager =
          (UiModeManager) sApplicationContext.getSystemService(UI_MODE_SERVICE);
      mContrastChangeListener =
          new ContrastChangeListener() {
            @Override
            public void onContrastChanged(final float contrast) {
              onDeviceChanged();
            }
          };
      uiModeManager.addContrastChangeListener(
          sApplicationContext.getMainExecutor(), mContrastChangeListener);
    }

    mBroadcastReceiver =
        new BroadcastReceiver() {
          @Override
          public void onReceive(final Context context, final Intent intent) {
            if (!GeckoThread.isStateAtLeast(GeckoThread.State.PROFILE_READY)) {
              return;
            }
            if (intent.getAction().equals(Intent.ACTION_LOCALE_CHANGED)) {
              GeckoAppShell.onSystemLocaleChanged();
              return;
            }
            if (intent.getAction().equals(Intent.ACTION_TIMEZONE_CHANGED)) {
              GeckoAppShell.onTimezoneChanged();
              return;
            }
          }
        };
    final IntentFilter filter = new IntentFilter();
    filter.addAction(Intent.ACTION_LOCALE_CHANGED);
    filter.addAction(Intent.ACTION_TIMEZONE_CHANGED);
    context.registerReceiver(mBroadcastReceiver, filter);

    mIsNightMode =
        (sApplicationContext.getResources().getConfiguration().uiMode
                & Configuration.UI_MODE_NIGHT_MASK)
            == Configuration.UI_MODE_NIGHT_YES;

    mAllPointerCapabilities = GeckoAppShell.getAllPointerCapabilities();
    mPointingDeviceKinds = GeckoAppShell.getPointingDeviceKinds();

    mInitialized = true;
  }

  public synchronized void shutdown() {
    if (!mInitialized) {
      Log.w(LOGTAG, "Already shut down!");
      return;
    }

    if (mInputManager == null) {
      Log.e(LOGTAG, "mInputManager should be valid!");
      return;
    }

    mInputManager.unregisterInputDeviceListener(listenerInstance);

    final ContentResolver contentResolver = sApplicationContext.getContentResolver();
    contentResolver.unregisterContentObserver(mContentObserver);

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
      final UiModeManager uiModeManager =
          (UiModeManager) sApplicationContext.getSystemService(UI_MODE_SERVICE);
      uiModeManager.removeContrastChangeListener(mContrastChangeListener);
    }

    GeckoAppShell.getApplicationContext().unregisterReceiver(mBroadcastReceiver);

    mInitialized = false;
    mInputManager = null;
    mContentObserver = null;
    mBroadcastReceiver = null;
  }

  @WrapForJNI(calledFrom = "gecko")
  /**
   * For prefers-reduced-motion media queries feature.
   *
   * <p>Uses `Settings.Global` which was introduced in API version 17.
   */
  private static boolean prefersReducedMotion() {
    final ContentResolver contentResolver = sApplicationContext.getContentResolver();

    return Settings.Global.getFloat(contentResolver, Settings.Global.ANIMATOR_DURATION_SCALE, 1)
        == 0.0f;
  }

  @WrapForJNI(calledFrom = "gecko")
  /**
   * For inverted-colors queries feature.
   *
   * <p>Uses `Settings.Secure.ACCESSIBILITY_DISPLAY_INVERSION_ENABLED` which was introduced in API
   * version 21.
   */
  private static boolean isInvertedColors() {
    final ContentResolver contentResolver = sApplicationContext.getContentResolver();

    return Settings.Secure.getInt(
            contentResolver, Settings.Secure.ACCESSIBILITY_DISPLAY_INVERSION_ENABLED, 0)
        == 1;
  }

  @WrapForJNI(calledFrom = "gecko")
  /**
   * For prefers-contrast queries feature.
   *
   * <p>Uses `Settings.Secure.ACCESSIBILITY_HIGH_TEXT_CONTRAST_ENABLED` which was introduced in API
   * version 21. Also uses `UiModeManager.getContrast()` which was introduced in API version 34.
   */
  private static boolean prefersContrast() {
    final ContentResolver contentResolver = sApplicationContext.getContentResolver();

    final boolean highTextContrastEnabled =
        Settings.Secure.getInt(
                contentResolver, /*Settings.Secure.ACCESSIBILITY_HIGH_TEXT_CONTRAST_ENABLED*/
                "high_text_contrast_enabled",
                0)
            == 1;

    float contrastLevel = 0f;

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
      final UiModeManager uiModeManager =
          (UiModeManager) sApplicationContext.getSystemService(UI_MODE_SERVICE);
      contrastLevel = uiModeManager.getContrast();
    }

    return highTextContrastEnabled || contrastLevel == 1f;
  }

  /** For prefers-color-scheme media queries feature. */
  public boolean isNightMode() {
    return mIsNightMode;
  }

  public void updateNightMode(final int newUIMode) {
    final boolean isNightMode =
        (newUIMode & Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES;
    if (isNightMode == mIsNightMode) {
      return;
    }
    mIsNightMode = isNightMode;
    onDeviceChanged();
  }

  @WrapForJNI(calledFrom = "gecko")
  private static int getAllPointerCapabilities() {
    return getInstance().mAllPointerCapabilities;
  }

  @WrapForJNI(calledFrom = "gecko")
  private static int getPointingDeviceKinds() {
    return getInstance().mPointingDeviceKinds;
  }

  @WrapForJNI(stubName = "OnDeviceChanged", calledFrom = "any", dispatchTo = "gecko")
  private static native void nativeOnDeviceChanged();

  public static void onDeviceChanged() {
    if (GeckoThread.isStateAtLeast(GeckoThread.State.PROFILE_READY)) {
      nativeOnDeviceChanged();
    } else {
      GeckoThread.queueNativeCallUntil(
          GeckoThread.State.PROFILE_READY, GeckoSystemStateListener.class, "nativeOnDeviceChanged");
    }
  }

  private boolean updatePointerCapabilities() {
    final int allPointerCapabilities = GeckoAppShell.getAllPointerCapabilities();
    final int pointingDeviceKinds = GeckoAppShell.getPointingDeviceKinds();
    if (mAllPointerCapabilities == allPointerCapabilities
        && mPointingDeviceKinds == pointingDeviceKinds) {
      return false;
    }
    mAllPointerCapabilities = allPointerCapabilities;
    mPointingDeviceKinds = pointingDeviceKinds;
    return true;
  }

  private void notifyDeviceChanged(final int deviceId) {
    final InputDevice device = InputDevice.getDevice(deviceId);
    if (device == null || !InputDeviceUtils.isPointerTypeDevice(device)) {
      return;
    }

    if (updatePointerCapabilities()) {
      onDeviceChanged();
    }
  }

  @Override
  public void onInputDeviceAdded(final int deviceId) {
    notifyDeviceChanged(deviceId);
  }

  @Override
  public void onInputDeviceRemoved(final int deviceId) {
    // Call onDeviceChanged directly without checking device source types
    // since we can no longer get a valid `InputDevice` in the case of
    // device removal.
    if (updatePointerCapabilities()) {
      onDeviceChanged();
    }
  }

  @Override
  public void onInputDeviceChanged(final int deviceId) {
    notifyDeviceChanged(deviceId);
  }
}
