// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.

package org.wpilib.simulation;

import java.util.function.BiConsumer;
import org.wpilib.driverstation.MatchType;
import org.wpilib.driverstation.POVDirection;
import org.wpilib.driverstation.internal.DriverStationBackend;
import org.wpilib.hardware.hal.AllianceStationID;
import org.wpilib.hardware.hal.DriverStationJNI;
import org.wpilib.hardware.hal.OpModeOption;
import org.wpilib.hardware.hal.RobotMode;
import org.wpilib.hardware.hal.simulation.DriverStationDataJNI;
import org.wpilib.hardware.hal.simulation.NotifyCallback;
import org.wpilib.util.WPIUtilJNI;

/** Class to control a simulated driver station. */
public final class DriverStationSim {
  private DriverStationSim() {
    throw new UnsupportedOperationException("This is a utility class!");
  }

  /**
   * Register a callback on whether the DS is enabled.
   *
   * @param callback the callback that will be called whenever the enabled state is changed
   * @param initialNotify if true, the callback will be run on the initial value
   * @return the {@link CallbackStore} object associated with this callback.
   */
  public static CallbackStore registerEnabledCallback(
      NotifyCallback callback, boolean initialNotify) {
    int uid = DriverStationDataJNI.registerEnabledCallback(callback, initialNotify);
    return new CallbackStore(uid, DriverStationDataJNI::cancelEnabledCallback);
  }

  /**
   * Check if the DS is enabled.
   *
   * @return true if enabled
   */
  public static boolean getEnabled() {
    return DriverStationDataJNI.getEnabled();
  }

  /**
   * Change whether the DS is enabled.
   *
   * @param enabled the new value
   */
  public static void setEnabled(boolean enabled) {
    DriverStationDataJNI.setEnabled(enabled);
  }

  /**
   * Register a callback on DS robot mode changes.
   *
   * @param callback the callback that will be called when the robot mode changes
   * @param initialNotify if true, the callback will be run on the initial value
   * @return the {@link CallbackStore} object associated with this callback.
   */
  public static CallbackStore registerRobotModeCallback(
      NotifyCallback callback, boolean initialNotify) {
    int uid = DriverStationDataJNI.registerRobotModeCallback(callback, initialNotify);
    return new CallbackStore(uid, DriverStationDataJNI::cancelRobotModeCallback);
  }

  /**
   * Gets the robot mode set by the DS.
   *
   * @return robot mode
   */
  public static RobotMode getRobotMode() {
    return DriverStationDataJNI.getRobotMode();
  }

  /**
   * Change the robot mode set by the DS.
   *
   * @param mode the new value
   */
  public static void setRobotMode(RobotMode mode) {
    DriverStationDataJNI.setRobotMode(mode);
  }

  /**
   * Register a callback on the eStop state.
   *
   * @param callback the callback that will be called whenever the eStop state changes
   * @param initialNotify if true, the callback will be run on the initial value
   * @return the {@link CallbackStore} object associated with this callback.
   */
  public static CallbackStore registerEStopCallback(
      NotifyCallback callback, boolean initialNotify) {
    int uid = DriverStationDataJNI.registerEStopCallback(callback, initialNotify);
    return new CallbackStore(uid, DriverStationDataJNI::cancelEStopCallback);
  }

  /**
   * Check if eStop has been activated.
   *
   * @return true if eStopped
   */
  public static boolean getEStop() {
    return DriverStationDataJNI.getEStop();
  }

  /**
   * Set whether eStop is active.
   *
   * @param eStop true to activate
   */
  public static void setEStop(boolean eStop) {
    DriverStationDataJNI.setEStop(eStop);
  }

  /**
   * Register a callback on whether the FMS is connected.
   *
   * @param callback the callback that will be called whenever the FMS connection changes
   * @param initialNotify if true, the callback will be run on the initial value
   * @return the {@link CallbackStore} object associated with this callback.
   */
  public static CallbackStore registerFmsAttachedCallback(
      NotifyCallback callback, boolean initialNotify) {
    int uid = DriverStationDataJNI.registerFmsAttachedCallback(callback, initialNotify);
    return new CallbackStore(uid, DriverStationDataJNI::cancelFmsAttachedCallback);
  }

  /**
   * Check if the FMS is connected.
   *
   * @return true if FMS is connected
   */
  public static boolean getFmsAttached() {
    return DriverStationDataJNI.getFmsAttached();
  }

  /**
   * Change whether the FMS is connected.
   *
   * @param fmsAttached the new value
   */
  public static void setFmsAttached(boolean fmsAttached) {
    DriverStationDataJNI.setFmsAttached(fmsAttached);
  }

  /**
   * Register a callback on whether the DS is connected.
   *
   * @param callback the callback that will be called whenever the DS connection changes
   * @param initialNotify if true, the callback will be run on the initial value
   * @return the {@link CallbackStore} object associated with this callback.
   */
  public static CallbackStore registerDsAttachedCallback(
      NotifyCallback callback, boolean initialNotify) {
    int uid = DriverStationDataJNI.registerDsAttachedCallback(callback, initialNotify);
    return new CallbackStore(uid, DriverStationDataJNI::cancelDsAttachedCallback);
  }

  /**
   * Check if the DS is attached.
   *
   * @return true if attached
   */
  public static boolean getDsAttached() {
    return DriverStationDataJNI.getDsAttached();
  }

  /**
   * Change whether the DS is attached.
   *
   * @param dsAttached the new value
   */
  public static void setDsAttached(boolean dsAttached) {
    DriverStationDataJNI.setDsAttached(dsAttached);
  }

  /**
   * Register a callback on the alliance station ID.
   *
   * @param callback the callback that will be called whenever the alliance station changes
   * @param initialNotify if true, the callback will be run on the initial value
   * @return the {@link CallbackStore} object associated with this callback.
   */
  public static CallbackStore registerAllianceStationIdCallback(
      NotifyCallback callback, boolean initialNotify) {
    int uid = DriverStationDataJNI.registerAllianceStationIdCallback(callback, initialNotify);
    return new CallbackStore(uid, DriverStationDataJNI::cancelAllianceStationIdCallback);
  }

  /**
   * Get the alliance station ID (color + number).
   *
   * @return the alliance station color and number
   */
  public static AllianceStationID getAllianceStationId() {
    return switch (DriverStationDataJNI.getAllianceStationId()) {
      case DriverStationJNI.ALLIANCE_STATION_UNKNOWN -> AllianceStationID.UNKNOWN;
      case DriverStationJNI.ALLIANCE_STATION_RED_1 -> AllianceStationID.RED_1;
      case DriverStationJNI.ALLIANCE_STATION_RED_2 -> AllianceStationID.RED_2;
      case DriverStationJNI.ALLIANCE_STATION_RED_3 -> AllianceStationID.RED_3;
      case DriverStationJNI.ALLIANCE_STATION_BLUE_1 -> AllianceStationID.BLUE_1;
      case DriverStationJNI.ALLIANCE_STATION_BLUE_2 -> AllianceStationID.BLUE_2;
      case DriverStationJNI.ALLIANCE_STATION_BLUE_3 -> AllianceStationID.BLUE_3;
      default -> AllianceStationID.UNKNOWN;
    };
  }

  /**
   * Change the alliance station.
   *
   * @param allianceStationId the new alliance station
   */
  public static void setAllianceStationId(AllianceStationID allianceStationId) {
    int allianceStation =
        switch (allianceStationId) {
          case UNKNOWN -> DriverStationJNI.ALLIANCE_STATION_UNKNOWN;
          case RED_1 -> DriverStationJNI.ALLIANCE_STATION_RED_1;
          case RED_2 -> DriverStationJNI.ALLIANCE_STATION_RED_2;
          case RED_3 -> DriverStationJNI.ALLIANCE_STATION_RED_3;
          case BLUE_1 -> DriverStationJNI.ALLIANCE_STATION_BLUE_1;
          case BLUE_2 -> DriverStationJNI.ALLIANCE_STATION_BLUE_2;
          case BLUE_3 -> DriverStationJNI.ALLIANCE_STATION_BLUE_3;
        };
    DriverStationDataJNI.setAllianceStationId(allianceStation);
  }

  /**
   * Register a callback on match time.
   *
   * @param callback the callback that will be called whenever match time changes
   * @param initialNotify if true, the callback will be run on the initial value
   * @return the {@link CallbackStore} object associated with this callback.
   */
  public static CallbackStore registerMatchTimeCallback(
      NotifyCallback callback, boolean initialNotify) {
    int uid = DriverStationDataJNI.registerMatchTimeCallback(callback, initialNotify);
    return new CallbackStore(uid, DriverStationDataJNI::cancelMatchTimeCallback);
  }

  /**
   * Get the current value of the match timer.
   *
   * @return the current match time
   */
  public static double getMatchTime() {
    return DriverStationDataJNI.getMatchTime();
  }

  /**
   * Sets the match timer.
   *
   * @param matchTime the new match time
   */
  public static void setMatchTime(double matchTime) {
    DriverStationDataJNI.setMatchTime(matchTime);
  }

  /**
   * Register a callback on opmode changes.
   *
   * @param callback the callback that will be called when the opmode changes
   * @param initialNotify if true, the callback will be run on the initial value
   * @return the {@link CallbackStore} object associated with this callback.
   */
  public static CallbackStore registerOpModeCallback(
      NotifyCallback callback, boolean initialNotify) {
    int uid = DriverStationDataJNI.registerOpModeCallback(callback, initialNotify);
    return new CallbackStore(uid, DriverStationDataJNI::cancelOpModeCallback);
  }

  /**
   * Gets the opmode.
   *
   * @return opmode
   */
  public static long getOpMode() {
    return DriverStationDataJNI.getOpMode();
  }

  /**
   * Change the opmode.
   *
   * @param opmode the new value
   */
  public static void setOpMode(long opmode) {
    DriverStationDataJNI.setOpMode(opmode);
  }

  /**
   * Register a callback on opmode options changes.
   *
   * @param callback the callback that will be called when the list of opmodes changes
   * @param initialNotify if true, the callback will be run on the initial value
   * @return the {@link CallbackStore} object associated with this callback.
   */
  public static CallbackStore registerOpModeOptionsCallback(
      BiConsumer<String, OpModeOption[]> callback, boolean initialNotify) {
    int uid = DriverStationDataJNI.registerOpModeOptionsCallback(callback, initialNotify);
    return new CallbackStore(uid, DriverStationDataJNI::cancelOpModeOptionsCallback);
  }

  /**
   * Gets the list of opmode options.
   *
   * @return opmodes list
   */
  public static OpModeOption[] getOpModeOptions() {
    return DriverStationDataJNI.getOpModeOptions();
  }

  /** Updates DriverStation data so that new values are visible to the user program. */
  public static void notifyNewData() {
    int handle = WPIUtilJNI.makeEvent(false, false);
    DriverStationJNI.provideNewDataEventHandle(handle);
    DriverStationDataJNI.notifyNewData();
    try {
      WPIUtilJNI.waitForObject(handle);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
    DriverStationJNI.removeNewDataEventHandle(handle);
    WPIUtilJNI.destroyEvent(handle);
    DriverStationBackend.refreshData();
  }

  /**
   * Sets suppression of DriverStation.reportError and reportWarning messages.
   *
   * @param shouldSend If false then messages will be suppressed.
   */
  public static void setSendError(boolean shouldSend) {
    DriverStationDataJNI.setSendError(shouldSend);
  }

  /**
   * Sets suppression of DriverStation.sendConsoleLine messages.
   *
   * @param shouldSend If false then messages will be suppressed.
   */
  public static void setSendConsoleLine(boolean shouldSend) {
    DriverStationDataJNI.setSendConsoleLine(shouldSend);
  }

  /**
   * Gets the joystick outputs.
   *
   * @param stick The joystick number
   * @return The joystick outputs
   */
  public static int getJoystickLeds(int stick) {
    return DriverStationDataJNI.getJoystickLeds(stick);
  }

  /**
   * Gets the joystick rumble.
   *
   * @param stick The joystick number
   * @param rumbleNum Rumble to get (0=left, 1=right, 2=left trigger, 3=right trigger)
   * @return The joystick rumble value
   */
  public static int getJoystickRumble(int stick, int rumbleNum) {
    return DriverStationDataJNI.getJoystickRumble(stick, rumbleNum);
  }

  /**
   * Sets the state of one joystick button. Button indexes begin at 0.
   *
   * @param stick The joystick number
   * @param button The button index, beginning at 0
   * @param state The state of the joystick button
   */
  public static void setJoystickButton(int stick, int button, boolean state) {
    DriverStationDataJNI.setJoystickButton(stick, button, state);
  }

  /**
   * Gets the value of the axis on a joystick.
   *
   * @param stick The joystick number
   * @param axis The analog axis number
   * @param value The value of the axis on the joystick
   */
  public static void setJoystickAxis(int stick, int axis, double value) {
    DriverStationDataJNI.setJoystickAxis(stick, axis, value);
  }

  /**
   * Gets the state of a POV on a joystick.
   *
   * @param stick The joystick number
   * @param pov The POV number
   * @param value the angle of the POV
   */
  public static void setJoystickPOV(int stick, int pov, POVDirection value) {
    DriverStationDataJNI.setJoystickPOV(stick, pov, value.value);
  }

  /**
   * Sets the maximum index that an axis is available at.
   *
   * @param stick The joystick number
   * @param maximumIndex The maximum index an axis is available at.
   */
  public static void setJoystickAxesMaximumIndex(int stick, int maximumIndex) {
    setJoystickAxesAvailable(stick, (1 << maximumIndex) - 1);
  }

  /**
   * Sets the number of axes for a joystick.
   *
   * @param stick The joystick number
   * @param count The number of axes on the indicated joystick
   */
  public static void setJoystickAxesAvailable(int stick, int count) {
    DriverStationDataJNI.setJoystickAxesAvailable(stick, count);
  }

  /**
   * Sets the maximum index that a pov is available at.
   *
   * @param stick The joystick number
   * @param maximumIndex The maximum index a pov is available at.
   */
  public static void setJoystickPOVsMaximumIndex(int stick, int maximumIndex) {
    setJoystickPOVsAvailable(stick, (1 << maximumIndex) - 1);
  }

  /**
   * Sets the number of POVs for a joystick.
   *
   * @param stick The joystick number
   * @param count The number of POVs on the indicated joystick
   */
  public static void setJoystickPOVsAvailable(int stick, int count) {
    DriverStationDataJNI.setJoystickPOVsAvailable(stick, count);
  }

  /**
   * Sets the maximum index that a button is available at.
   *
   * @param stick The joystick number
   * @param maximumIndex The maximum index a button is available at.
   */
  public static void setJoystickButtonsMaximumIndex(int stick, int maximumIndex) {
    if (maximumIndex >= 64) {
      setJoystickButtonsAvailable(stick, 0xFFFFFFFFFFFFFFFFL);
    } else {
      setJoystickButtonsAvailable(stick, (1L << maximumIndex) - 1);
    }
  }

  /**
   * Sets the number of buttons for a joystick.
   *
   * @param stick The joystick number
   * @param count The number of buttons on the indicated joystick
   */
  public static void setJoystickButtonsAvailable(int stick, long count) {
    DriverStationDataJNI.setJoystickButtonsAvailable(stick, count);
  }

  /**
   * Sets the value of isGamepad for a joystick.
   *
   * @param stick The joystick number
   * @param isGamepad The value of isGamepad
   */
  public static void setJoystickIsGamepad(int stick, boolean isGamepad) {
    DriverStationDataJNI.setJoystickIsGamepad(stick, isGamepad);
  }

  /**
   * Sets the value of type for a joystick.
   *
   * @param stick The joystick number
   * @param type The value of type
   */
  public static void setJoystickGamepadType(int stick, int type) {
    DriverStationDataJNI.setJoystickGamepadType(stick, type);
  }

  /**
   * Sets the value of supported outputs for a joystick.
   *
   * @param stick The joystick number
   * @param supportedOutputs The value of supported outputs
   */
  public static void setJoystickSupportedOutputs(int stick, int supportedOutputs) {
    DriverStationDataJNI.setJoystickSupportedOutputs(stick, supportedOutputs);
  }

  /**
   * Sets the name of a joystick.
   *
   * @param stick The joystick number
   * @param name The value of name
   */
  public static void setJoystickName(int stick, String name) {
    DriverStationDataJNI.setJoystickName(stick, name);
  }

  /**
   * Sets the game specific message.
   *
   * @param message the game specific message
   */
  public static void setGameData(String message) {
    DriverStationDataJNI.setGameData(message);
  }

  /**
   * Sets the event name.
   *
   * @param name the event name
   */
  public static void setEventName(String name) {
    DriverStationDataJNI.setEventName(name);
  }

  /**
   * Sets the match type.
   *
   * @param type the match type
   */
  public static void setMatchType(MatchType type) {
    int matchType =
        switch (type) {
          case PRACTICE -> 1;
          case QUALIFICATION -> 2;
          case ELIMINATION -> 3;
          case NONE -> 0;
        };
    DriverStationDataJNI.setMatchType(matchType);
  }

  /**
   * Sets the match number.
   *
   * @param matchNumber the match number
   */
  public static void setMatchNumber(int matchNumber) {
    DriverStationDataJNI.setMatchNumber(matchNumber);
  }

  /**
   * Sets the replay number.
   *
   * @param replayNumber the replay number
   */
  public static void setReplayNumber(int replayNumber) {
    DriverStationDataJNI.setReplayNumber(replayNumber);
  }

  /** Reset all simulation data for the Driver Station. */
  public static void resetData() {
    DriverStationDataJNI.resetData();
  }
}
