// Copyright 2015-present 650 Industries. All rights reserved.
import ExpoModulesCore

let motionGestureEnabledKey = "EXDevMenuMotionGestureEnabled"
let touchGestureEnabledKey = "EXDevMenuTouchGestureEnabled"
let keyCommandsEnabledKey = "EXDevMenuKeyCommandsEnabled"
let showsAtLaunchKey = "EXDevMenuShowsAtLaunch"
let isOnboardingFinishedKey = "EXDevMenuIsOnboardingFinished"
let showFloatingActionButtonKey = "EXDevMenuShowFloatingActionButton"

public class DevMenuPreferences: Module {
  public func definition() -> ModuleDefinition {
    Name("DevMenuPreferences")

    AsyncFunction("getPreferencesAsync") {
      return DevMenuPreferences.serialize()
    }

    AsyncFunction("setPreferencesAsync") { (settings: [String: Any]) in
      DevMenuPreferences.setSettings(settings)
    }
  }

  /*
   Initializes dev menu preferences by registering user defaults
   and applying some preferences to static classes like interceptors.
   */
  static func setup() {
    let fabDefault = Bundle.main.object(forInfoDictionaryKey: showFloatingActionButtonKey) as? Bool
    let showsAtLaunchDefault = Bundle.main.object(forInfoDictionaryKey: showsAtLaunchKey) as? Bool
    let isOnboardingFinishedDefault = Bundle.main.object(forInfoDictionaryKey: isOnboardingFinishedKey) as? Bool

    #if os(tvOS)
    UserDefaults.standard.register(defaults: [
      motionGestureEnabledKey: false,
      touchGestureEnabledKey: false,
      keyCommandsEnabledKey: true,
      showsAtLaunchKey: showsAtLaunchDefault ?? false,
      isOnboardingFinishedKey: isOnboardingFinishedDefault ?? true,
      showFloatingActionButtonKey: fabDefault ?? false
    ])
    #else
    UserDefaults.standard.register(defaults: [
      motionGestureEnabledKey: true,
      touchGestureEnabledKey: true,
      keyCommandsEnabledKey: true,
      showsAtLaunchKey: showsAtLaunchDefault ?? true,
      isOnboardingFinishedKey: isOnboardingFinishedDefault ?? false,
      showFloatingActionButtonKey: fabDefault ?? true
    ])
    #endif

    NotificationCenter.default.addObserver(
      self,
      selector: #selector(toggleMenu),
      name: Notification.Name("RCTShowDevMenuNotification"),
      object: nil
    )

    /*
     We don't want to uninstall `DevMenuMotionInterceptor`, because otherwise, the app on shake gesture will bring up the dev-menu from the RN.
     So we added `isEnabled` to disable it, but not uninstall.
     */
#if !os(macOS)
    DevMenuTouchInterceptor.isInstalled = DevMenuPreferences.touchGestureEnabled
#endif
    DevMenuKeyCommandsInterceptor.isInstalled = DevMenuPreferences.keyCommandsEnabled
  }

  /**
   Whether to enable shake gesture.
   */
  static var motionGestureEnabled: Bool {
    get {
      return boolForKey(motionGestureEnabledKey)
    }
    set {
      setBool(newValue, forKey: motionGestureEnabledKey)
    }
  }

  /**
   Whether to enable three-finger long press gesture.
   */
  static var touchGestureEnabled: Bool {
    get {
      return boolForKey(touchGestureEnabledKey)
    }
    set {
      setBool(newValue, forKey: touchGestureEnabledKey)
#if !os(macOS)
      DevMenuTouchInterceptor.isInstalled = newValue
#endif
    }
  }

  /**
   Whether to enable key commands.
   */
  static var keyCommandsEnabled: Bool {
    get {
      return boolForKey(keyCommandsEnabledKey)
    }
    set {
      setBool(newValue, forKey: keyCommandsEnabledKey)
#if !os(macOS)
      DevMenuKeyCommandsInterceptor.isInstalled = newValue
#endif
    }
  }

  /**
   Whether to automatically show the dev menu once its delegate is set and the bridge is loaded.
   */
  static var showsAtLaunch: Bool {
    get {
      return DevMenuTestInterceptorManager.interceptor?.shouldShowAtLaunch ?? boolForKey(showsAtLaunchKey)
    }
    set {
      setBool(newValue, forKey: showsAtLaunchKey)
      DevMenuManager.shared.updateAutoLaunchObserver()
    }
  }

  /**
   Returns `true` only if the user finished onboarding, `false` otherwise.
   This is now public because expo-dev-launcher needs access
   in order to disable the onboarding popup for certain bundle URLs
   */
  public static var isOnboardingFinished: Bool {
    get {
      return DevMenuTestInterceptorManager.interceptor?.isOnboardingFinishedKey ?? boolForKey(isOnboardingFinishedKey)
    }
    set {
      setBool(newValue, forKey: isOnboardingFinishedKey)
    }
  }

  public static var showFloatingActionButton: Bool {
    get {
      return boolForKey(showFloatingActionButtonKey)
    }
    set {
      setBool(newValue, forKey: showFloatingActionButtonKey)
      DevMenuManager.shared.updateFABVisibility()
    }
  }

  /**
   Serializes settings into a dictionary so they can be passed through the bridge.
   */
  static func serialize() -> [String: Any] {
    return [
      "motionGestureEnabled": DevMenuPreferences.motionGestureEnabled,
      "touchGestureEnabled": DevMenuPreferences.touchGestureEnabled,
      "keyCommandsEnabled": DevMenuPreferences.keyCommandsEnabled,
      "showsAtLaunch": DevMenuPreferences.showsAtLaunch,
      "isOnboardingFinished": DevMenuPreferences.isOnboardingFinished,
      "showFloatingActionButton": DevMenuPreferences.showFloatingActionButton
    ]
  }

  static func setSettings(_ settings: [String: Any]) {
    if let motionGestureEnabled = settings["motionGestureEnabled"] as? Bool {
      DevMenuPreferences.motionGestureEnabled = motionGestureEnabled
    }
    if let touchGestureEnabled = settings["touchGestureEnabled"] as? Bool {
      DevMenuPreferences.touchGestureEnabled = touchGestureEnabled
    }
    if let keyCommandsEnabled = settings["keyCommandsEnabled"] as? Bool {
      DevMenuPreferences.keyCommandsEnabled = keyCommandsEnabled
    }
    if let showsAtLaunch = settings["showsAtLaunch"] as? Bool {
      DevMenuPreferences.showsAtLaunch = showsAtLaunch
    }
    if let showFloatingActionButton = settings["showFloatingActionButton"] as? Bool {
      DevMenuPreferences.showFloatingActionButton = showFloatingActionButton
    }
  }

  @objc static func toggleMenu() {
    if DevMenuPreferences.motionGestureEnabled {
      DevMenuManager.shared.toggleMenu()
    }
  }

  deinit {
    NotificationCenter.default.removeObserver(self)
  }
}

private func boolForKey(_ key: String) -> Bool {
  return UserDefaults.standard.bool(forKey: key)
}

private func setBool(_ value: Bool, forKey key: String) {
  UserDefaults.standard.set(value, forKey: key)
}
