// Copyright 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#ifndef GOOGLE_APIS_GCM_ENGINE_MCS_CLIENT_H_
#define GOOGLE_APIS_GCM_ENGINE_MCS_CLIENT_H_

#include <stdint.h>

#include <deque>
#include <map>
#include <memory>
#include <string>
#include <vector>

#include "base/files/file_path.h"
#include "base/macros.h"
#include "base/memory/linked_ptr.h"
#include "base/memory/weak_ptr.h"
#include "google_apis/gcm/base/gcm_export.h"
#include "google_apis/gcm/base/mcs_message.h"
#include "google_apis/gcm/engine/connection_factory.h"
#include "google_apis/gcm/engine/connection_handler.h"
#include "google_apis/gcm/engine/gcm_store.h"
#include "google_apis/gcm/engine/heartbeat_manager.h"

namespace base {
class Clock;
class Timer;
}  // namespace base

namespace google {
namespace protobuf {
class MessageLite;
}  // namespace protobuf
}  // namespace google

namespace mcs_proto {
class LoginRequest;
}

namespace gcm {

class CollapseKey;
class ConnectionFactory;
class GCMStatsRecorder;
struct ReliablePacketInfo;

// An MCS client. This client is in charge of all communications with an
// MCS endpoint, and is capable of reliably sending/receiving GCM messages.
// NOTE: Not thread safe. This class should live on the same thread as that
// network requests are performed on.
class GCM_EXPORT MCSClient {
 public:
  // Any change made to this enum should have corresponding change in the
  // GetStateString(...) function.
  enum State {
    UNINITIALIZED,  // Uninitialized.
    LOADED,         // GCM Load finished, waiting to connect.
    CONNECTING,     // Connection in progress.
    CONNECTED,      // Connected and running.
  };

  // Any change made to this enum should have corresponding change in the
  // GetMessageSendStatusString(...) function in mcs_client.cc.
  enum MessageSendStatus {
    // Message was queued successfully.
    QUEUED,
    // Message was sent to the server and the ACK was received.
    SENT,
    // Message not saved, because total queue size limit reached.
    QUEUE_SIZE_LIMIT_REACHED,
    // Message not saved, because app queue size limit reached.
    APP_QUEUE_SIZE_LIMIT_REACHED,
    // Message too large to send.
    MESSAGE_TOO_LARGE,
    // Message not send becuase of TTL = 0 and no working connection.
    NO_CONNECTION_ON_ZERO_TTL,
    // Message exceeded TTL.
    TTL_EXCEEDED,

    // NOTE: always keep this entry at the end. Add new status types only
    // immediately above this line. Make sure to update the corresponding
    // histogram enum accordingly.
    SEND_STATUS_COUNT
  };

  // Callback for MCSClient's error conditions.
  // TODO(fgorski): Keeping it as a callback with intention to add meaningful
  // error information.
  typedef base::Callback<void()> ErrorCallback;
  // Callback when a message is received.
  typedef base::Callback<void(const MCSMessage& message)>
      OnMessageReceivedCallback;
  // Callback when a message is sent (and receipt has been acknowledged by
  // the MCS endpoint).
  typedef base::Callback<void(int64_t user_serial_number,
                              const std::string& app_id,
                              const std::string& message_id,
                              MessageSendStatus status)> OnMessageSentCallback;

  MCSClient(const std::string& version_string,
            base::Clock* clock,
            ConnectionFactory* connection_factory,
            GCMStore* gcm_store,
            GCMStatsRecorder* recorder);
  virtual ~MCSClient();

  // Initialize the client. Will load any previous id/token information as well
  // as unacknowledged message information from the GCM storage, if it exists,
  // passing the id/token information back via |initialization_callback| along
  // with a |success == true| result. If no GCM information is present (and
  // this is therefore a fresh client), a clean GCM store will be created and
  // values of 0 will be returned via |initialization_callback| with
  // |success == true|.
  // If an error loading the GCM store is encountered,
  // |initialization_callback| will be invoked with |success == false|.
  void Initialize(const ErrorCallback& initialization_callback,
                  const OnMessageReceivedCallback& message_received_callback,
                  const OnMessageSentCallback& message_sent_callback,
                  std::unique_ptr<GCMStore::LoadResult> load_result);

  // Logs the client into the server. Client must be initialized.
  // |android_id| and |security_token| are optional if this is not a new
  // client, else they must be non-zero.
  // Successful login will result in |message_received_callback| being invoked
  // with a valid LoginResponse.
  // Login failure (typically invalid id/token) will shut down the client, and
  // |initialization_callback| to be invoked with |success = false|.
  virtual void Login(uint64_t android_id, uint64_t security_token);

  // Sends a message, with or without reliable message queueing (RMQ) support.
  // Will asynchronously invoke the OnMessageSent callback regardless.
  // Whether to use RMQ depends on whether the protobuf has |ttl| set or not.
  // |ttl == 0| denotes the message should only be sent if the connection is
  // open. |ttl > 0| will keep the message saved for |ttl| seconds, after which
  // it will be dropped if it was unable to be sent. When a message is dropped,
  // |message_sent_callback_| is invoked with a TTL expiration error.
  virtual void SendMessage(const MCSMessage& message);

  // Returns the current state of the client.
  State state() const { return state_; }

  // Returns the size of the send message queue.
  int GetSendQueueSize() const;

  // Returns the size of the resend messaage queue.
  int GetResendQueueSize() const;

  // Returns text representation of the state enum.
  std::string GetStateString() const;

  // Updates the timer used by |heartbeat_manager_| for sending heartbeats.
  void UpdateHeartbeatTimer(std::unique_ptr<base::Timer> timer);

  // Allows a caller to set a heartbeat interval (in milliseconds) with which
  // the MCS connection will be monitored on both ends, to detect device
  // presence. In case the newly set interval is smaller than the current one,
  // connection will be restarted with new heartbeat interval. Valid values have
  // to be between GetMax/GetMinClientHeartbeatIntervalMs of HeartbeatManager,
  // otherwise the setting won't take effect.
  void AddHeartbeatInterval(const std::string& scope, int interval_ms);
  void RemoveHeartbeatInterval(const std::string& scope);

  HeartbeatManager* GetHeartbeatManagerForTesting() {
    return &heartbeat_manager_;
  }

 private:
  typedef uint32_t StreamId;
  typedef std::string PersistentId;
  typedef std::vector<StreamId> StreamIdList;
  typedef std::vector<PersistentId> PersistentIdList;
  typedef std::map<StreamId, PersistentId> StreamIdToPersistentIdMap;
  typedef linked_ptr<ReliablePacketInfo> MCSPacketInternal;

  // Resets the internal state and builds a new login request, acknowledging
  // any pending server-to-device messages and rebuilding the send queue
  // from all unacknowledged device-to-server messages.
  // Should only be called when the connection has been reset.
  void ResetStateAndBuildLoginRequest(mcs_proto::LoginRequest* request);

  // Send a heartbeat to the MCS server.
  void SendHeartbeat();

  // GCM Store callback.
  void OnGCMUpdateFinished(bool success);

  // Attempt to send a message.
  void MaybeSendMessage();

  // Helper for sending a protobuf along with any unacknowledged ids to the
  // wire.
  void SendPacketToWire(ReliablePacketInfo* packet_info);

  // Handle a data message sent to the MCS client system from the MCS server.
  void HandleMCSDataMesssage(
      std::unique_ptr<google::protobuf::MessageLite> protobuf);

  // Handle a packet received over the wire.
  void HandlePacketFromWire(
      std::unique_ptr<google::protobuf::MessageLite> protobuf);

  // ReliableMessageQueue acknowledgment helpers.
  // Handle a StreamAck sent by the server confirming receipt of all
  // messages up to the message with stream id |last_stream_id_received|.
  void HandleStreamAck(StreamId last_stream_id_received_);
  // Handle a SelectiveAck sent by the server confirming all messages
  // in |id_list|.
  void HandleSelectiveAck(const PersistentIdList& id_list);
  // Handle server confirmation of a device message, including device's
  // acknowledgment of receipt of messages.
  void HandleServerConfirmedReceipt(StreamId device_stream_id);

  // Generates a new persistent id for messages.
  // Virtual for testing.
  virtual PersistentId GetNextPersistentId();

  // Helper for the heartbeat manager to signal a connection reset.
  void OnConnectionResetByHeartbeat(
      ConnectionFactory::ConnectionResetReason reason);

  // Runs the message_sent_callback_ with send |status| of the |protobuf|.
  void NotifyMessageSendStatus(const google::protobuf::MessageLite& protobuf,
                               MessageSendStatus status);

  // Pops the next message from the front of the send queue (cleaning up
  // any associated state).
  MCSPacketInternal PopMessageForSend();

  // Gets the minimum interval from the map of scopes to intervals in
  // milliseconds.
  int GetMinHeartbeatIntervalMs();

  // Local version string. Sent on login.
  const std::string version_string_;

  // Clock for enforcing TTL. Passed in for testing.
  base::Clock* const clock_;

  // Client state.
  State state_;

  // Callbacks for owner.
  ErrorCallback mcs_error_callback_;
  OnMessageReceivedCallback message_received_callback_;
  OnMessageSentCallback message_sent_callback_;

  // The android id and security token in use by this device.
  uint64_t android_id_;
  uint64_t security_token_;

  // Factory for creating new connections and connection handlers.
  ConnectionFactory* connection_factory_;

  // Connection handler to handle all over-the-wire protocol communication
  // with the mobile connection server.
  ConnectionHandler* connection_handler_;

  // -----  Reliablie Message Queue section -----
  // Note: all queues/maps are ordered from oldest (front/begin) message to
  // most recent (back/end).

  // Send/acknowledge queues.
  std::deque<MCSPacketInternal> to_send_;
  std::deque<MCSPacketInternal> to_resend_;

  // Map of collapse keys to their pending messages.
  std::map<CollapseKey, ReliablePacketInfo*> collapse_key_map_;

  // Last device_to_server stream id acknowledged by the server.
  StreamId last_device_to_server_stream_id_received_;
  // Last server_to_device stream id acknowledged by this device.
  StreamId last_server_to_device_stream_id_received_;
  // The stream id for the last sent message. A new message should consume
  // stream_id_out_ + 1.
  StreamId stream_id_out_;
  // The stream id of the last received message. The LoginResponse will always
  // have a stream id of 1, and stream ids increment by 1 for each received
  // message.
  StreamId stream_id_in_;

  // The server messages that have not been acked by the device yet. Keyed by
  // server stream id.
  StreamIdToPersistentIdMap unacked_server_ids_;

  // Those server messages that have been acked. They must remain tracked
  // until the ack message is itself confirmed. The list of all message ids
  // acknowledged are keyed off the device stream id of the message that
  // acknowledged them.
  std::map<StreamId, PersistentIdList> acked_server_ids_;

  // Those server messages from a previous connection that were not fully
  // acknowledged. They do not have associated stream ids, and will be
  // acknowledged on the next login attempt.
  PersistentIdList restored_unackeds_server_ids_;

  // The GCM persistent store. Not owned.
  GCMStore* gcm_store_;

  // Manager to handle triggering/detecting heartbeats.
  HeartbeatManager heartbeat_manager_;

  // Custom heartbeat intervals requested by different components.
  std::map<std::string, int> custom_heartbeat_intervals_;

  // Recorder that records GCM activities for debugging purpose. Not owned.
  GCMStatsRecorder* recorder_;

  base::WeakPtrFactory<MCSClient> weak_ptr_factory_;

  DISALLOW_COPY_AND_ASSIGN(MCSClient);
};

} // namespace gcm

#endif  // GOOGLE_APIS_GCM_ENGINE_MCS_CLIENT_H_
