// Released under the MIT License. See LICENSE for details.

#ifndef BALLISTICA_PLATFORM_PLATFORM_H_
#define BALLISTICA_PLATFORM_PLATFORM_H_

#include <sys/stat.h>

#include <list>
#include <string>
#include <vector>

#include "ballistica/ballistica.h"

namespace ballistica {

/// For capturing and printing stack-traces and related errors.
/// Platforms should subclass this and return instances in GetStackTrace().
class PlatformStackTrace {
 public:
  // The stack trace should capture the stack state immediately upon
  // construction but should do the bare minimum amount of work to store it. Any
  // expensive operations such as symbolification should be deferred until
  // GetDescription().
  virtual ~PlatformStackTrace() = default;

  // Return a human readable version of the trace (with symbolification if
  // available).
  virtual auto GetDescription() noexcept -> std::string = 0;

  // Should return a copy of itself allocated via new() (or nullptr if not
  // possible).
  virtual auto copy() const noexcept -> PlatformStackTrace* = 0;
};

// This class attempts to abstract away most platform-specific functionality.
// Ideally we should need to pull in no platform-specific system headers outside
// of the platform*.cc files and can just go through this.
class Platform {
 public:
  static auto Create() -> Platform*;
  Platform();
  virtual ~Platform();

#pragma mark LIFECYCLE/SETTINGS ------------------------------------------------

  /// Called right after g_platform is created/assigned. Any platform
  /// functionality depending on a complete g_platform object existing can
  /// be run here.
  virtual auto PostInit() -> void;

  /// Create the proper App module and add it to the main_thread.
  void CreateApp();

  /// Create the appropriate Graphics subclass for the app.
  Graphics* CreateGraphics();

  virtual void CreateAuxiliaryModules();
  virtual void WillExitMain(bool errored);

  // Inform the platform that all subsystems are up and running and it can
  // start talking to them.
  virtual void OnBootstrapComplete();

  // Get/set values before standard game settings are available
  // (for values needed before SDL init/etc).
  // FIXME: We should have some sort of 'bootconfig.json' file for these.
  // (or simply read the regular config in via c++ immediately)
  auto GetLowLevelConfigValue(const char* key, int default_value) -> int;
  void SetLowLevelConfigValue(const char* key, int value);

  // Called when the app config is being read/applied.
  virtual void ApplyConfig();

  // Called when the app should set itself up to intercept ctrl-c presses.
  virtual void SetupInterruptHandling();

  void FinalCleanup();

#pragma mark FILES -------------------------------------------------------------

  // remove() support UTF8 strings.
  virtual auto Remove(const char* path) -> int;

  // stat() supporting UTF8 strings.
  virtual auto Stat(const char* path, struct BA_STAT* buffer) -> int;

  // fopen() supporting UTF8 strings.
  virtual auto FOpen(const char* path, const char* mode) -> FILE*;

  // rename() supporting UTF8 strings.
  // For cross-platform consistency, this should also remove any file that
  // exists at the target location first.
  virtual auto Rename(const char* oldname, const char* newname) -> int;

  // Simple cross-platform check for existence of a file.
  auto FilePathExists(const std::string& name) -> bool;

  /// Attempt to make a directory; raise an Exception if unable,
  /// unless quiet is true.
  void MakeDir(const std::string& dir, bool quiet = false);

  // Return the current working directory.
  virtual auto GetCWD() -> std::string;

  // Unlink a file.
  virtual auto Unlink(const char* path) -> void;

  /// Return the absolute path for the provided path. Note that this requires
  /// the path to already exist.
  auto AbsPath(const std::string& path, std::string* outpath) -> bool;

#pragma mark CLIPBOARD ---------------------------------------------------------

  /// Return whether clipboard operations are supported at all.
  /// This gets called when determining whether to display clipboard related
  /// UI elements/etc.
  auto ClipboardIsSupported() -> bool;

  /// Return whether there is currently text on the clipboard.
  auto ClipboardHasText() -> bool;

  /// Set current clipboard text. Raises an Exception if clipboard is
  /// unsupported.
  auto ClipboardSetText(const std::string& text) -> void;

  /// Return current text from the clipboard. Raises an Exception if
  /// clipboard is unsupported or if there's no text on the clipboard.
  auto ClipboardGetText() -> std::string;

#pragma mark PRINTING/LOGGING --------------------------------------------------

  // Send a message to the default platform handler.
  // IMPORTANT: No Object::Refs should be created or destroyed within this call,
  // or deadlock can occur.
  virtual void HandleLog(const std::string& msg);

#pragma mark ENVIRONMENT -------------------------------------------------------

  // Return a simple name for the platform: 'mac', 'windows', 'linux', etc.
  virtual auto GetPlatformName() -> std::string;

  // Return a simple name for the subplatform: 'amazon', 'google', etc.
  virtual auto GetSubplatformName() -> std::string;

  // Are we running in event-push-mode?
  // With this on, we return from Main() and the system handles the event loop.
  // With it off, we loop in Main() ourself.
  virtual auto IsEventPushMode() -> bool;

  // Return the interface type based on the environment (phone, tablet, etc).
  virtual auto GetUIScale() -> UIScale;

  // Return a string *reasonably* likely to be unique and consistent for this
  // device. Do not assume this is globally unique and *do not* assume that it
  // will never ever change (hardware upgrades may affect it, etc).
  virtual auto GetUniqueDeviceIdentifier() -> const std::string&;

  // Returns the ID to use for the device account
  auto GetDeviceAccountID() -> std::string;
  auto GetConfigDirectory() -> std::string;
  auto GetConfigFilePath() -> std::string;
  auto GetUserPythonDirectory() -> std::string;
  auto GetAppPythonDirectory() -> std::string;
  auto GetSitePythonDirectory() -> std::string;
  auto GetReplaysDir() -> std::string;

  // Return en_US or whatnot.
  virtual auto GetLocale() -> std::string;
  virtual void SetupDataDirectory();
  virtual auto GetUserAgentString() -> std::string;
  virtual auto GetOSVersionString() -> std::string;

  /// Set an environment variable as utf8, overwriting if it already exists.
  /// Raises an exception on errors.
  virtual void SetEnv(const std::string& name, const std::string& value);

  // Are we being run from a terminal? (should we show prompts, etc?).
  virtual auto IsStdinATerminal() -> bool;

  // Return hostname or other id suitable for network searches, etc.
  auto GetDeviceName() -> std::string;

  // Are we running on a tv?
  virtual auto IsRunningOnTV() -> bool;

  // Are we on a daydream enabled android device?
  virtual auto IsRunningOnDaydream() -> bool;

  // Do we have touchscreen hardware?
  auto HasTouchScreen() -> bool;

  // Are we running on a desktop setup in general?
  virtual auto IsRunningOnDesktop() -> bool;

  // Are we running on fireTV hardware?
  virtual auto IsRunningOnFireTV() -> bool;

  // Return the external storage path (currently only relevant on android).
  virtual auto GetExternalStoragePath() -> std::string;

  // For enabling some special hardware optimizations for nvidia.
  auto is_tegra_k1() const -> bool { return is_tegra_k1_; }
  void set_is_tegra_k1(bool val) { is_tegra_k1_ = val; }

  // Return true if this platform includes its own python distribution
  // (defaults to false).
  virtual auto ContainsPythonDist() -> bool;

#pragma mark INPUT DEVICES -----------------------------------------------------

  // Return a name for a ballistica keycode.
  virtual auto GetKeyName(int keycode) -> std::string;

#pragma mark IN APP PURCHASES --------------------------------------------------

  virtual void Purchase(const std::string& item);

  // Restore purchases (currently only relevant on apple platforms).
  virtual void RestorePurchases();

  // purchase ack'ed by the master-server (so can consume)
  virtual void PurchaseAck(const std::string& purchase,
                           const std::string& order_id);

#pragma mark ANDROID -----------------------------------------------------------

  virtual auto GetAndroidExecArg() -> std::string;
  virtual void AndroidSetResString(const std::string& res);
  virtual auto AndroidIsGPGSConnectionToClient(ConnectionToClient* c) -> bool;
  virtual auto AndroidGPGSNewConnectionToClient(int id) -> ConnectionToClient*;
  virtual auto AndroidGPGSNewConnectionToHost() -> ConnectionToHost*;
  virtual void AndroidSynthesizeBackPress();
  virtual void AndroidQuitActivity();
  virtual void AndroidShowAppInvite(const std::string& title,
                                    const std::string& message,
                                    const std::string& code);
  virtual void AndroidRefreshFile(const std::string& file);
  virtual void AndroidGPGSPartyInvitePlayers();
  virtual void AndroidGPGSPartyShowInvites();
  virtual void AndroidGPGSPartyInviteAccept(const std::string& invite_id);
  virtual void AndroidShowWifiSettings();

#pragma mark PERMISSIONS -------------------------------------------------------

  /// Request the permission asynchronously.
  /// If the permission cannot be requested (due to having been denied, etc)
  /// then this may also present a message or pop-up instructing the user how
  /// to manually grant the permission (up to individual platforms to
  /// implement).
  virtual void RequestPermission(Permission p);

  /// Returns true if this permission has been granted (or if asking is not
  /// required for it).
  virtual auto HavePermission(Permission p) -> bool;

#pragma mark ANALYTICS ---------------------------------------------------------

  virtual void SetAnalyticsScreen(const std::string& screen);
  virtual void IncrementAnalyticsCount(const std::string& name, int increment);
  virtual void IncrementAnalyticsCountRaw(const std::string& name,
                                          int increment);
  virtual void IncrementAnalyticsCountRaw2(const std::string& name,
                                           int uses_increment, int increment);
  virtual void SubmitAnalyticsCounts();

#pragma mark APPLE -------------------------------------------------------------

  virtual auto NewAutoReleasePool() -> void*;
  virtual void DrainAutoReleasePool(void* pool);
  // FIXME: Can we consolidate these with the general music playback calls?
  virtual void MacMusicAppInit();
  virtual auto MacMusicAppGetVolume() -> int;
  virtual void MacMusicAppSetVolume(int volume);
  virtual void MacMusicAppGetLibrarySource();
  virtual void MacMusicAppStop();
  virtual auto MacMusicAppPlayPlaylist(const std::string& playlist) -> bool;
  virtual auto MacMusicAppGetPlaylists() -> std::list<std::string>;

#pragma mark TEXT RENDERING ----------------------------------------------------

  // Set bounds/width info for a bit of text.
  // (will only be called in BA_ENABLE_OS_FONT_RENDERING is set)
  virtual void GetTextBoundsAndWidth(const std::string& text, Rect* r,
                                     float* width);
  virtual void FreeTextTexture(void* tex);
  virtual auto CreateTextTexture(int width, int height,
                                 const std::vector<std::string>& strings,
                                 const std::vector<float>& positions,
                                 const std::vector<float>& widths, float scale)
      -> void*;
  virtual auto GetTextTextureData(void* tex) -> uint8_t*;

#pragma mark ACCOUNTS ----------------------------------------------------------

  virtual auto SignIn(const std::string& account_type) -> void;
  virtual auto SignOut() -> void;
  virtual auto GameCenterLogin() -> void;
  virtual auto LoginDidChange() -> void;

#pragma mark MUSIC PLAYBACK ----------------------------------------------------

  // FIXME: currently these are wired up on android; need to generalize
  //  to support mac/itunes or other music player types.
  virtual void MusicPlayerPlay(PyObject* target);
  virtual void MusicPlayerStop();
  virtual void MusicPlayerShutdown();
  virtual void MusicPlayerSetVolume(float volume);

#pragma mark ADS ---------------------------------------------------------------

  virtual void ShowAd(const std::string& purpose);

  // Return whether we have the ability to show *any* ads.
  virtual auto GetHasAds() -> bool;

  // Return whether we have the ability to show longer-form video ads (suitable
  // for rewards).
  virtual auto GetHasVideoAds() -> bool;

#pragma mark GAME SERVICES -----------------------------------------------------

  // Given a raw leaderboard score, convert it to what the game uses.
  // For instance, platforms may return times as milliseconds while we require
  // hundredths of a second, etc.
  virtual auto ConvertIncomingLeaderboardScore(
      const std::string& leaderboard_id, int score) -> int;

  virtual void GetFriendScores(const std::string& game,
                               const std::string& game_version,
                               void* py_callback);
  virtual void SubmitScore(const std::string& game, const std::string& version,
                           int64_t score);
  virtual void ReportAchievement(const std::string& achievement);
  virtual auto HaveLeaderboard(const std::string& game,
                               const std::string& config) -> bool;

  virtual void ShowOnlineScoreUI(const std::string& show,
                                 const std::string& game,
                                 const std::string& game_version);
  virtual void ResetAchievements();

#pragma mark NETWORKING --------------------------------------------------------

  virtual void CloseSocket(int socket);
  virtual auto SocketPair(int domain, int type, int protocol, int socks[2])
      -> int;
  virtual auto GetBroadcastAddrs() -> std::vector<uint32_t>;
  virtual auto SetSocketNonBlocking(int sd) -> bool;

#pragma mark ERRORS & DEBUGGING ------------------------------------------------

  // Should return a subclass of PlatformStackTrace allocated via new.
  // Platforms with no meaningful stack trace functionality can return nullptr.
  virtual auto GetStackTrace() -> PlatformStackTrace*;

  // Called during stress testing.
  virtual auto GetMemUsageInfo() -> std::string;

  // Optionally override fatal error reporting. If true is returned, default
  // fatal error reporting will not run.
  virtual auto ReportFatalError(const std::string& message,
                                bool in_top_level_exception_handler) -> bool;

  // Optionally override fatal error handling. If true is returned, default
  // fatal error handling will not run.
  virtual auto HandleFatalError(bool exit_cleanly,
                                bool in_top_level_exception_handler) -> bool;

  // If this platform has the ability to show a blocking dialog on the main
  // thread for fatal errors, return true here.
  virtual auto CanShowBlockingFatalErrorDialog() -> bool;

  // Called on the main thread when a fatal error occurs.
  // Will only be called if CanShowBlockingFatalErrorDialog() is true.
  virtual auto BlockingFatalErrorDialog(const std::string& message) -> void;

  // Use this instead of looking at errno (translates winsock errors to errno).
  virtual auto GetSocketError() -> int;

  // Return a string for the current value of errno.
  virtual auto GetErrnoString() -> std::string;

  // Return a description of errno (unix) or WSAGetLastError() (windows).
  virtual auto GetSocketErrorString() -> std::string;

  /// Set a key to be included in crash logs or other debug cases.
  /// This is expected to be lightweight as it may be called often.
  virtual auto SetDebugKey(const std::string& key, const std::string& value)
      -> void;

  /// Print a log message to be included in crash logs or other debug
  /// mechanisms. Standard log messages (at least with to_server=true) get
  /// send here as well. It can be useful to call this directly to report
  /// extra details that may help in debugging, as these calls are not
  /// considered 'noteworthy' or presented to the user as standard Log()
  /// calls are.
  virtual auto HandleDebugLog(const std::string& msg) -> void;

  static auto DebugLog(const std::string& msg) -> void {
    if (g_platform) {
      g_platform->HandleDebugLog(msg);
    }
  }

  /// Shortcut to set last native Python call we made.
  static auto SetLastPyCall(const std::string& name) {
    if (g_platform) {
      g_platform->py_call_num_++;
      g_platform->SetDebugKey(
          "LastPyCall" + std::to_string(g_platform->py_call_num_ % 10),
          std::to_string(g_platform->py_call_num_) + ":" + name + "@"
              + std::to_string(GetCurrentMilliseconds()));
    }
  }

#pragma mark MISC --------------------------------------------------------------

  // Return a monotonic time measurement in milliseconds since launch.
  // To get a time value that is guaranteed to not jump around or go backwards,
  // use ballistica::GetRealTime() (which is an abstraction around this).
  auto GetTicks() -> millisecs_t;

  // A raw milliseconds value (not relative to launch time).
  static auto GetCurrentMilliseconds() -> millisecs_t;
  static auto GetCurrentSeconds() -> int64_t;

  static void SleepMS(millisecs_t ms);

  // Pop up a text edit dialog.
  virtual void EditText(const std::string& title, const std::string& value,
                        int max_chars);

  // Open the provided URL in a browser or whatnot.
  void OpenURL(const std::string& url);
  virtual auto DemangleCXXSymbol(const std::string& s) -> std::string;

  // Called each time through the main event loop; for custom pumping/handling.
  virtual void RunEvents();

  // Called when the app module is pausing.
  // Note: only app-thread (main thread) stuff should happen here.
  // (don't push calls to other threads/etc).
  virtual void OnAppPause();

  // Called when the app module is resuming.
  virtual void OnAppResume();

  // Is the OS currently playing music? (so we can avoid doing so).
  virtual auto IsOSPlayingMusic() -> bool;

  // Pass platform-specific misc-read-vals along to the OS (as a json string).
  virtual void SetPlatformMiscReadVals(const std::string& vals);

  // Show/hide the hardware cursor.
  virtual void SetHardwareCursorVisible(bool visible);

  // Get the most up-to-date cursor position.
  virtual void GetCursorPosition(float* x, float* y);

  // Quit the app (can be immediate or via posting some high level event).
  virtual void QuitApp();

  // Do we want to deprecate this?...
  virtual void GetScoresToBeat(const std::string& level,
                               const std::string& config, void* py_callback);

  // Open a file using the system default method (in another app, etc.)
  virtual void OpenFileExternally(const std::string& path);

  // Open a directory using the system default method (Finder, etc.)
  virtual void OpenDirExternally(const std::string& path);

  // Currently mac-only (could be generalized though).
  virtual void StartListeningForWiiRemotes();

  // Currently mac-only (could be generalized though).
  virtual void StopListeningForWiiRemotes();

  // Set the name of the current thread (for debugging).
  virtual void SetCurrentThreadName(const std::string& name);

  // If display-resolution can be directly set on this platform,
  // return true and set the native full res here.  Otherwise return false;
  virtual auto GetDisplayResolution(int* x, int* y) -> bool;

  auto using_custom_app_python_dir() const {
    return using_custom_app_python_dir_;
  }

 protected:
  // Open the provided URL in a browser or whatnot.
  virtual void DoOpenURL(const std::string& url);

  // Called once per platform to determine touchscreen presence.
  virtual auto DoHasTouchScreen() -> bool;
  virtual auto DoGetDeviceName() -> std::string;

  // Attempt to actually create a directory.
  // Should not except if it already exists or if quiet is true.
  virtual void DoMakeDir(const std::string& dir, bool quiet);

  // Attempt to actually get an abs path. This will only be called if
  // the path is valid and exists.
  virtual auto DoAbsPath(const std::string& path, std::string* outpath) -> bool;

  // Calc the user scripts dir path for this platform.
  // This will be called once and the path cached.
  virtual auto DoGetUserPythonDirectory() -> std::string;

  // Return the default config directory for this platform.
  virtual auto GetDefaultConfigDir() -> std::string;

  // Return the prefix to use for device UUIDs on this platform.
  virtual auto GetDeviceUUIDPrefix() -> std::string;

  // Return whether there is an actual unique UUID available for this platform,
  // and also return it if so.
  virtual auto GetRealDeviceUUID(std::string* uuid) -> bool;

  // Generate a random UUID string.
  virtual auto GenerateUUID() -> std::string;

  virtual auto DoClipboardIsSupported() -> bool;
  virtual auto DoClipboardHasText() -> bool;
  virtual auto DoClipboardSetText(const std::string& text) -> void;
  virtual auto DoClipboardGetText() -> std::string;

 private:
  int py_call_num_{};
  bool using_custom_app_python_dir_{};
  bool have_config_dir_{};
  bool have_has_touchscreen_value_{};
  bool have_touchscreen_{};
  bool is_tegra_k1_{};
  bool have_clipboard_is_supported_{};
  bool clipboard_is_supported_{};
  millisecs_t starttime_{};
  std::string device_uuid_;
  bool have_device_uuid_{};
  std::string config_dir_;
  std::string user_scripts_dir_;
  std::string app_python_dir_;
  std::string site_python_dir_;
  std::string replays_dir_;
};

}  // namespace ballistica

#endif  // BALLISTICA_PLATFORM_PLATFORM_H_
