/*
 * Copyright 2016 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#ifndef FIREBASE_AUTH_CLIENT_CPP_SRC_INCLUDE_FIREBASE_AUTH_H_
#define FIREBASE_AUTH_CLIENT_CPP_SRC_INCLUDE_FIREBASE_AUTH_H_

#include <vector>
#include "firebase/app.h"
#include "firebase/future.h"
#include "firebase/internal/common.h"
#include "firebase/auth/user.h"

#if !defined(DOXYGEN)
FIREBASE_APP_REGISTER_CALLBACKS_REFERENCE(auth)
#endif  // !defined(DOXYGEN)

namespace firebase {

/// @brief Firebase Authentication API.
///
/// Firebase Authentication provides backend services to securely authenticate
/// users. It can authenticate users using passwords and federated identity
/// provider credentials, and it can integrate with a custom auth backend.
namespace auth {

// Predeclarations.
struct AuthData;
class AuthStateListener;
class IdTokenListener;
class PhoneAuthProvider;

/// @brief Firebase authentication object.
///
/// @if cpp_examples
///
/// firebase::auth::Auth is the gateway to the Firebase authentication API.
/// With it, you can reference @ref firebase::auth::User objects to manage user
/// accounts and credentials.
///
/// Each @ref firebase::App has up to one firebase::auth::Auth class. You
/// acquire the firebase::auth::Auth class through the static function
/// @ref firebase::auth::Auth::GetAuth.
///
/// For example:
/// @code{.cpp}
///  // Get the Auth class for your App.
///  firebase::auth::Auth* auth = firebase::auth::Auth::GetAuth(app);
///
///  // Request anonymous sign-in and wait until asynchronous call completes.
///  firebase::Future<firebase::auth::User*> sign_in_future =
///      auth->SignInAnonymously();
///  while (sign_in_future.status() == firebase::kFutureStatusPending) {
///    Wait(100);
///    printf("Signing in...\n");
///  }
///
///  // Print sign in results.
///  const firebase::auth::AuthError error =
///      static_cast<firebase::auth::AuthError>(sign_in_future.error());
///  if (error != firebase::auth::kAuthErrorNone) {
///    printf("Sign in failed with error `%s`\n",
///           sign_in_future.error_message());
///  } else {
///    firebase::auth::User* user = *sign_in_future.result();
///    printf("Signed in as %s user.\n",
///           user->Anonymous() ? "an anonymous" : "a non-anonymous");
///  }
/// @endcode
/// @endif
class Auth {
 public:
  /// @brief Results of calls @ref FetchProvidersForEmail.
  ///
  struct FetchProvidersResult {
    /// The IDPs (identity providers) that can be used for `email`.
    /// An array of length `num_providers` of null-terminated strings.
    std::vector<std::string> providers;
  };

  ~Auth();

  /// Synchronously gets the cached current user, or nullptr if there is none.
  /// @note This function may block and wait until the Auth instance finishes
  /// loading the saved user's state. This should only happen for a short
  /// period of time after the Auth instance is created.
  User* current_user();

  // ----- Providers -------------------------------------------------------
  /// Asynchronously requests the IDPs (identity providers) that can be used
  /// for the given email address.
  ///
  /// Useful for an "identifier-first" login flow.
  ///
  /// @if cpp_examples
  /// The following sample code illustrates a possible login screen
  /// that allows the user to pick an identity provider.
  /// @code{.cpp}
  ///  // This function is called every frame to display the login screen.
  ///  // Returns the identity provider name, or "" if none selected.
  ///  const char* DisplayIdentityProviders(firebase::auth::Auth& auth,
  ///                                       const char* email) {
  ///    // Get results of most recent call to FetchProvidersForEmail().
  ///    firebase::Future<firebase::auth::Auth::FetchProvidersResult> future =
  ///        auth.FetchProvidersForEmailLastResult();
  ///    const firebase::auth::Auth::FetchProvidersResult* result =
  ///        future.result();
  ///
  ///    // Header.
  ///    ShowTextBox("Sign in %s", email);
  ///
  ///    // Fetch providers from the server if we need to.
  ///    const bool refetch =
  ///        future.status() == firebase::kFutureStatusInvalid ||
  ///        (result != nullptr && strcmp(email, result->email.c_str()) != 0);
  ///    if (refetch) {
  ///      auth.FetchProvidersForEmail(email);
  ///    }
  ///
  ///    // Show a waiting icon if we're waiting for the asynchronous call to
  ///    // complete.
  ///    if (future.status() != firebase::kFutureStatusComplete) {
  ///      ShowImage("waiting icon");
  ///      return "";
  ///    }
  ///
  ///    // Show error code if the call failed.
  ///    if (future.error() != firebase::auth::kAuthErrorNone) {
  ///      ShowTextBox("Error fetching providers: %s", future.error_message());
  ///    }
  ///
  ///    // Show a button for each provider available to this email.
  ///    // Return the provider for the button that's pressed.
  ///    for (size_t i = 0; i < result->providers.size(); ++i) {
  ///      const bool selected = ShowTextButton(result->providers[i].c_str());
  ///      if (selected) return result->providers[i].c_str();
  ///    }
  ///    return "";
  ///  }
  /// @endcode
  /// @endif
  Future<FetchProvidersResult> FetchProvidersForEmail(const char* email);

  /// Get results of the most recent call to @ref FetchProvidersForEmail.
  Future<FetchProvidersResult> FetchProvidersForEmailLastResult() const;

  // ----- Sign In ---------------------------------------------------------
  /// Asynchronously logs into Firebase with the given Auth token.
  ///
  /// An error is returned, if the token is invalid, expired or otherwise
  /// not accepted by the server.
  Future<User*> SignInWithCustomToken(const char* token);

  /// Get results of the most recent call to @ref SignInWithCustomToken.
  Future<User*> SignInWithCustomTokenLastResult() const;

  /// Convenience method for @ref SignInAndRetrieveDataWithCredential that
  /// doesn't return additional identity provider data.
  Future<User*> SignInWithCredential(const Credential& credential);

  /// Get results of the most recent call to @ref SignInWithCredential.
  Future<User*> SignInWithCredentialLastResult() const;

  /// Asynchronously logs into Firebase with the given credentials.
  ///
  /// For example, the credential could wrap a Facebook login access token,
  /// a Twitter token/token-secret pair).
  ///
  /// The SignInResult contains both a reference to the User (which can be null
  /// if the sign in failed), and AdditionalUserInfo, which holds details
  /// specific to the Identity Provider used to sign in.
  ///
  /// An error is returned if the token is invalid, expired, or otherwise not
  /// accepted by the server.
  Future<SignInResult> SignInAndRetrieveDataWithCredential(
      const Credential& credential);

  /// Get results of the most recent call to
  /// @ref SignInAndRetrieveDataWithCredential.
  Future<SignInResult> SignInAndRetrieveDataWithCredentialLastResult() const;

  /// Asynchronously creates and becomes an anonymous user.
  /// If there is already an anonymous user signed in, that user will be
  /// returned instead.
  /// If there is any other existing user, that user will be signed out.
  ///
  /// @if cpp_examples
  /// The following sample code illustrates the sign-in flow that might be
  /// used by a game or some other program with a regular (for example, 30Hz)
  /// update loop.
  ///
  /// The sample calls SignIn() every frame. We don’t maintain our own
  /// Futures but instead call SignInAnonymouslyLastResult() to get the Future
  /// of our most recent call.
  ///
  /// @code{.cpp}
  ///  // Try to ensure that we get logged in.
  ///  // This function is called every frame.
  ///  bool SignIn(firebase::auth::Auth& auth) {
  ///    // Grab the result of the latest sign-in attempt.
  ///    firebase::Future<firebase::auth::User*> future =
  ///        auth.SignInAnonymouslyLastResult();
  ///
  ///    // If we're in a state where we can try to sign in, do so.
  ///    if (future.status() == firebase::kFutureStatusInvalid ||
  ///        (future.status() == firebase::kFutureStatusComplete &&
  ///         future.error() != firebase::auth::kAuthErrorNone)) {
  ///      auth.SignInAnonymously();
  ///    }
  ///
  ///    // We're signed in if the most recent result was successful.
  ///    return future.status() == firebase::kFutureStatusComplete &&
  ///           future.error() == firebase::auth::kAuthErrorNone;
  ///  }
  /// @endcode
  /// @endif
  Future<User*> SignInAnonymously();

  /// Get results of the most recent call to @ref SignInAnonymously.
  Future<User*> SignInAnonymouslyLastResult() const;

  /// Signs in using provided email address and password.
  /// An error is returned if the password is wrong or otherwise not accepted
  /// by the server.
  Future<User*> SignInWithEmailAndPassword(const char* email,
                                           const char* password);

  /// Get results of the most recent call to @ref SignInWithEmailAndPassword.
  Future<User*> SignInWithEmailAndPasswordLastResult() const;

  /// Creates, and on success, logs in a user with the given email address
  /// and password.
  ///
  /// An error is returned when account creation is unsuccessful
  /// (due to another existing account, invalid password, etc.).
  Future<User*> CreateUserWithEmailAndPassword(const char* email,
                                               const char* password);

  /// Get results of the most recent call to
  /// @ref CreateUserWithEmailAndPassword.
  Future<User*> CreateUserWithEmailAndPasswordLastResult() const;

  /// Removes any existing authentication credentials from this client.
  /// This function always succeeds.
  void SignOut();

  // ----- Password Reset -------------------------------------------------
  /// Initiates a password reset for the given email address.
  ///
  /// If the email address is not registered, then the returned task has a
  /// status of IsFaulted.
  ///
  /// @if cpp_examples
  /// The following sample code illustrating a possible password reset flow.
  /// Like in the Anonymous Sign-In example above, the ResetPasswordScreen()
  /// function is called once per frame (say 30 times per second).
  ///
  /// No state is persisted by the caller in this example. The state of the
  /// most recent calls are instead accessed through calls to functions like
  /// auth.SendPasswordResetEmailLastResult().
  /// @code{.cpp}
  ///  const char* ImageNameForStatus(const firebase::FutureBase& future) {
  ///    assert(future.status() != firebase::kFutureStatusInvalid);
  ///    return future.status() == firebase::kFutureStatusPending
  ///               ? "waiting icon"
  ///               : future.error() == firebase::auth::kAuthErrorNone
  ///                    ? "checkmark icon"
  ///                    : "x mark icon";
  ///  }
  ///
  ///  // This function is called once per frame.
  ///  void ResetPasswordScreen(firebase::auth::Auth& auth) {
  ///    // Gather email address.
  ///    // ShowInputBox() returns a value when `enter` is pressed.
  ///    const std::string email = ShowInputBox("Enter e-mail");
  ///    if (email != "") {
  ///      auth.SendPasswordResetEmail(email.c_str());
  ///    }
  ///
  ///    // Show checkmark, X-mark, or waiting icon beside the
  ///    // email input box, to indicate if email has been sent.
  ///    firebase::Future<void> send_future =
  ///        auth.SendPasswordResetEmailLastResult();
  ///    ShowImage(ImageNameForStatus(send_future));
  ///
  ///    // Display error message if the e-mail could not be sent.
  ///    if (send_future.status() == firebase::kFutureStatusComplete &&
  ///        send_future.error() != firebase::auth::kAuthErrorNone) {
  ///      ShowTextBox(send_future.error_message());
  ///    }
  ///  }
  /// @endcode
  /// @endif
  Future<void> SendPasswordResetEmail(const char* email);

  /// Get results of the most recent call to @ref SendPasswordResetEmail.
  Future<void> SendPasswordResetEmailLastResult() const;

  /// @brief Registers a listener to changes in the authentication state.
  ///
  /// There can be more than one listener registered at the same time.
  /// The listeners are called asynchronously, possibly on a different thread.
  ///
  /// Authentication state changes are:
  ///   - Right after the listener has been registered
  ///   - When a user signs in
  ///   - When the current user signs out
  ///   - When the current user changes
  ///
  /// It is a recommended practice to always listen to sign-out events, as you
  /// may want to prompt the user to sign in again and maybe restrict the
  /// information or actions they have access to.
  ///
  /// Use RemoveAuthStateListener to unregister a listener.
  ///
  /// @note The caller owns `listener` and is responsible for destroying it.
  /// When `listener` is destroyed, or when @ref Auth is destroyed,
  /// RemoveAuthStateListener is called automatically.
  void AddAuthStateListener(AuthStateListener* listener);

  /// @brief Unregisters a listener of authentication changes.
  ///
  /// Listener must previously been added with AddAuthStateListener.
  ///
  /// Note that listeners unregister themselves automatically when they
  /// are destroyed, and the Auth class unregisters its listeners when the
  /// Auth class itself is destroyed, so this function does not normally need
  /// to be called explicitly.
  void RemoveAuthStateListener(AuthStateListener* listener);

  /// @brief Registers a listener to changes in the ID token state.
  ///
  /// There can be more than one listener registered at the same time.
  /// The listeners are called asynchronously, possibly on a different thread.
  ///
  /// Authentication state changes are:
  ///   - Right after the listener has been registered
  ///   - When a user signs in
  ///   - When the current user signs out
  ///   - When the current user changes
  ///   - When there is a change in the current user's token
  ///
  /// Use RemoveIdTokenListener to unregister a listener.
  ///
  /// @note The caller owns `listener` and is responsible for destroying it.
  /// When `listener` is destroyed, or when @ref Auth is destroyed,
  /// RemoveIdTokenListener is called automatically.
  void AddIdTokenListener(IdTokenListener* listener);

  /// @brief Unregisters a listener of ID token changes.
  ///
  /// Listener must previously been added with AddIdTokenListener.
  ///
  /// Note that listeners unregister themselves automatically when they
  /// are destroyed, and the Auth class unregisters its listeners when the
  /// Auth class itself is destroyed, so this function does not normally need
  /// to be called explicitly.
  void RemoveIdTokenListener(IdTokenListener* listener);

  /// Gets the App this auth object is connected to.
  App& app();

  /// Returns the Auth object for an App. Creates the Auth if required.
  ///
  /// To get the Auth object for the default app, use,
  /// GetAuth(GetDefaultFirebaseApp());
  ///
  /// If the library Auth fails to initialize, init_result_out will be
  /// written with the result status (if a pointer is given).
  ///
  /// @param[in] app The App to use for the Auth object.
  /// @param[out] init_result_out Optional: If provided, write the init result
  /// here. Will be set to kInitResultSuccess if initialization succeeded, or
  /// kInitResultFailedMissingDependency on Android if Google Play services is
  /// not available on the current device.
  static Auth* GetAuth(App* app, InitResult* init_result_out = nullptr);

 private:
  /// @cond FIREBASE_APP_INTERNAL
  friend class ::firebase::App;
  friend class ::firebase::auth::PhoneAuthProvider;
  friend class IdTokenRefreshListener;
  friend class IdTokenRefreshThread;
  friend class UserDataPersist;
  friend class UserDesktopTest;
  friend class AuthDesktopTest;

  friend void EnableTokenAutoRefresh(AuthData* authData);
  friend void DisableTokenAutoRefresh(AuthData* authData);
  friend void ResetTokenRefreshCounter(AuthData* authData);
  /// @endcond

  // Provides access to the auth token for the current user.  Returns the
  // current user's auth token, or an empty string, if there isn't one.
  static bool GetAuthTokenForRegistry(App* app, void* /*unused*/, void* out);

  // Starts and stops a thread to ensure that the cached auth token is never
  // kept long enough for it to expire.  Refcounted, so multiple classes can
  // register this without causing problems.
  static bool StartTokenRefreshThreadForRegistry(App* app, void* /*unused*/,
                                                 void* /*unused*/);
  static bool StopTokenRefreshThreadForRegistry(App* app, void* /*unused*/,
                                                void* /*unused*/);

  // Init and Destroy the platform specific auth data.
  void InitPlatformAuth(AuthData* const auth_data);
  void DestroyPlatformAuth(AuthData* const auth_data);

  // Call GetAuth() to create an Auth object.
  // Constructors and destructors don't make any external calls.
  // They just initialize and deinitialize internal variables.
  Auth(App* app, void* auth_impl);

  // Delete the internal AuthData object.
  void DeleteInternal();

  // This class uses the pimpl mechanism to avoid exposing platform-dependent
  // implementation.
  AuthData* auth_data_;
};

/// @brief Listener called when there is a change in the authentication state.
///
/// Override base class method to handle authentication state changes.
/// Methods are invoked asynchronously and may be invoked on other threads.
class AuthStateListener {
 public:
  /// @note: Destruction of the listener automatically calls
  /// RemoveAuthStateListener() from the Auths this listener is registered with,
  /// if those Auths have not yet been destroyed.
  virtual ~AuthStateListener();

  /// Called when the authentication state of `auth` changes.
  ///   - Right after the listener has been registered
  ///   - When a user is signed in
  ///   - When the current user is signed out
  ///   - When the current user changes
  ///
  /// @param[in] auth Disambiguates which @ref Auth instance the event
  /// corresponds to, in the case where you are using more than one at the same
  /// time.
  virtual void OnAuthStateChanged(Auth* auth) = 0;

 private:
  /// @cond FIREBASE_APP_INTERNAL
  friend class Auth;
  /// @endcond

  /// The Auths with which this listener has been registered.
  std::vector<Auth*> auths_;
};

/// @brief Listener called when there is a change in the ID token.
///
/// Override base class method to handle ID token changes.
/// Methods are invoked asynchronously and may be invoked on other threads.
class IdTokenListener {
 public:
  /// @note: Destruction of the listener automatically calls
  /// RemoveIdTokenListener() from the Auths this listener is registered with,
  /// if those Auths have not yet been destroyed.
  virtual ~IdTokenListener();

  /// Called when there is a change in the current user's token.
  ///   - Right after the listener has been registered
  ///   - When a user signs in
  ///   - When the current user signs out
  ///   - When the current user changes
  ///   - When there is a change in the current user's token
  ///
  /// @param[in] auth Disambiguates which @ref Auth instance the event
  /// corresponds to, in the case where you are using more than one at the same
  /// time.
  virtual void OnIdTokenChanged(Auth* auth) = 0;

 private:
  /// @cond FIREBASE_APP_INTERNAL
  friend class Auth;
  /// @endcond

  /// The Auths with which this listener has been registered.
  std::vector<Auth*> auths_;
};

}  // namespace auth
}  // namespace firebase

#endif  // FIREBASE_AUTH_CLIENT_CPP_SRC_INCLUDE_FIREBASE_AUTH_H_
