#ifndef ANDROID_PDX_CLIENT_H_
#define ANDROID_PDX_CLIENT_H_

#include <errno.h>
#include <sys/types.h>

#include <memory>
#include <string>
#include <type_traits>

#include <pdx/channel_handle.h>
#include <pdx/client_channel.h>
#include <pdx/client_channel_factory.h>
#include <pdx/file_handle.h>
#include <pdx/message_reader.h>
#include <pdx/message_writer.h>
#include <pdx/rpc/remote_method_type.h>
#include <pdx/status.h>

namespace android {
namespace pdx {

class Transaction;

/*
 * Base class of client-side service API classes.
 */
class Client {
 public:
  static const int64_t kInfiniteTimeout = -1;

  virtual ~Client() = default;

  /*
   * Returns true if the Client instance successfully initialized, false
   * otherwise. Subclasses that can fail to initialize must override this and
   * AND their initialization result with this base class method's result.
   *
   * This method is not intended to perform initialization, only to report
   * the status of the initialization.
   */
  virtual bool IsInitialized() const;

  /*
   * Returns the error code describing the Client initialization failure, or 0
   * if there was no failure.
   */
  int error() const;

  // Returns a reference to IPC channel handle.
  LocalChannelHandle& GetChannelHandle();

 protected:
  friend Transaction;
  explicit Client(std::unique_ptr<ClientChannel> channel);
  explicit Client(std::unique_ptr<ClientChannelFactory> channel_factory,
                  int64_t timeout_ms = kInfiniteTimeout);

  /*
   * Called by Client::Connect() after successfully connecting to the service
   * endpoint. Subclasses may override this method to perform additional setup,
   * including sending messages to complete the connection process.
   *
   * Subclasses may call Client::Close() within this method to terminate the
   * connection; Client::Connect() returns the negated error passed to
   * Client::Close() when this happens.
   */
  virtual void OnConnect();

  enum : size_t { MAX_IMPULSE_LENGTH = sizeof(uint64_t) * 4 };

  Status<void> SendImpulse(int opcode);
  Status<void> SendImpulse(int opcode, const void* buffer, size_t length);

  /*
   * Remote method call API using pdx::rpc serialization.
   * Include pdx/rpc/remote_method.h to use these methods.
   */
  template <typename RemoteMethodType, typename... Args>
  Status<typename RemoteMethodType::Return> InvokeRemoteMethod(Args&&... args);

  template <typename RemoteMethodType, typename ReturnType, typename... Args>
  Status<void> InvokeRemoteMethodInPlace(ReturnType* return_value,
                                         Args&&... args);

  /*
   * Close the endpoint file descriptor and optionally indicate an error, which
   * may be retrieved through error(). Subclasses may use this in their
   * constructor to signal failure during initialization or at other times
   * during operation.
   */
  void Close(int error);

  /*
   * Returns true if the client is connected to the service, false otherwise.
   */
  bool IsConnected() const;

  /*
   * Enables auto-reconnect with the given timeout. Use kInfiniteTimeout (-1)
   * for no timeout. Auto-reconnect can only be enabled if the Client class
   * was constructed with a ClientChannelFactory.
   */
  void EnableAutoReconnect(int64_t reconnect_timeout_ms);

  /*
   * Disables auto-reconnect.
   */
  void DisableAutoReconnect();

  /*
   * Returns an fd that the client may use to check/wait for asynchronous
   * notifications to the channel. It is implementation dependent how the
   * transport backend handles this feature, however all implementations must
   * support at least POLLIN/EPOLLIN/readable.
   *
   * For uses that require more than one type of event, use
   * ClientChannel::GetEventMask() to distinguish between events.
   */
  int event_fd() const;

  /*
   * Returns the underlying ClientChannel object.
   */
  ClientChannel* GetChannel() const { return channel_.get(); }
  std::unique_ptr<ClientChannel>&& TakeChannel() { return std::move(channel_); }

 private:
  Client(const Client&) = delete;
  void operator=(const Client&) = delete;

  Status<void> CheckReconnect();
  bool NeedToDisconnectChannel(int error) const;
  void CheckDisconnect(int error);

  template <typename T>
  inline void CheckDisconnect(const Status<T>& status) {
    if (!status)
      CheckDisconnect(status.error());
  }

  std::unique_ptr<ClientChannel> channel_;
  int error_{0};

  // Reconnection state.
  std::unique_ptr<ClientChannelFactory> channel_factory_;
  int64_t reconnect_timeout_ms_{0};
  bool auto_reconnect_enabled_{false};
};

/*
 * Utility template base class for client-side service API classes. Handles
 * initialization checks during allocation and automatically cleans up on
 * failure.
 *
 * @tparam T Type of the class extending this one.
 * @tparam C Client class to wrap. Defaults to the Client class.
 */
template <typename T, typename ParentClient = Client>
class ClientBase : public ParentClient {
 public:
  // Type of the client this class wraps.
  using ClientType = ParentClient;

  static_assert(std::is_base_of<Client, ParentClient>::value,
                "The provided parent client is not a Client subclass.");

  /*
   * Allocates a new instance of the superclass and checks for successful
   * initialization.
   *
   * The variadic arguments must expand to match one of type T's constructors
   * and are passed through unchanged. If a timeout is desired, subclasses are
   * responsible for passing this through to the appropriate ClientBase
   * constructor.
   *
   * Returns a unique_ptr to the new instance on success, or an empty unique_ptr
   * otherwise.
   */
  template <typename... Args>
  static inline std::unique_ptr<T> Create(Args&&... args) {
    std::unique_ptr<T> client(new T(std::forward<Args>(args)...));
    if (client->IsInitialized())
      return client;
    else
      return nullptr;
  }

 protected:
  /*
   * Type of the base class. Useful for referencing the base class type and
   * constructor in subclasses. Subclasses with non-public constructors
   * must declare BASE a friend.
   */
  using BASE = ClientBase<T, ParentClient>;

  /*
   * Type of the unique_ptr deleter. Useful for friend declarations.
   */
  using deleter_type = typename std::unique_ptr<T>::deleter_type;

  using ParentClient::ParentClient;
};

class Transaction final : public OutputResourceMapper,
                          public InputResourceMapper {
 public:
  Transaction(Client& client);
  ~Transaction();

  template <typename T>
  Status<T> Send(int opcode) {
    return SendVector<T>(opcode, nullptr, 0, nullptr, 0);
  }

  template <typename T>
  Status<T> Send(int opcode, const void* send_buffer, size_t send_length,
                 void* receive_buffer, size_t receive_length) {
    const bool send = (send_buffer && send_length);
    const bool receive = (receive_buffer && receive_length);
    const iovec send_vector = {const_cast<void*>(send_buffer), send_length};
    const iovec receive_vector = {receive_buffer, receive_length};
    return SendVector<T>(opcode, send ? &send_vector : nullptr, send ? 1 : 0,
                         receive ? &receive_vector : nullptr, receive ? 1 : 0);
  }

  template <typename T>
  Status<T> SendVector(int opcode, const iovec* send_vector, size_t send_count,
                       const iovec* receive_vector, size_t receive_count) {
    Status<T> ret;
    SendTransaction(opcode, &ret, send_vector, send_count, receive_vector,
                    receive_count);
    return ret;
  }

  template <typename T, size_t send_count, size_t receive_count>
  Status<T> SendVector(int opcode, const iovec (&send_vector)[send_count],
                       const iovec (&receive_vector)[receive_count]) {
    return SendVector<T>(opcode, send_vector, send_count, receive_vector,
                         receive_count);
  }

  template <typename T, size_t send_count>
  Status<T> SendVector(int opcode, const iovec (&send_vector)[send_count],
                       std::nullptr_t) {
    return SendVector<T>(opcode, send_vector, send_count, nullptr, 0);
  }

  template <typename T, size_t receive_count>
  Status<T> SendVector(int opcode, std::nullptr_t,
                       const iovec (&receive_vector)[receive_count]) {
    return SendVector<T>(opcode, nullptr, 0, receive_vector, receive_count);
  }

  // OutputResourceMapper
  Status<FileReference> PushFileHandle(const LocalHandle& handle) override;
  Status<FileReference> PushFileHandle(const BorrowedHandle& handle) override;
  Status<FileReference> PushFileHandle(const RemoteHandle& handle) override;
  Status<ChannelReference> PushChannelHandle(
      const LocalChannelHandle& handle) override;
  Status<ChannelReference> PushChannelHandle(
      const BorrowedChannelHandle& handle) override;
  Status<ChannelReference> PushChannelHandle(
      const RemoteChannelHandle& handle) override;

  // InputResourceMapper
  bool GetFileHandle(FileReference ref, LocalHandle* handle) override;
  bool GetChannelHandle(ChannelReference ref,
                        LocalChannelHandle* handle) override;

 private:
  bool EnsureStateAllocated();
  void SendTransaction(int opcode, Status<void>* ret, const iovec* send_vector,
                       size_t send_count, const iovec* receive_vector,
                       size_t receive_count);
  void SendTransaction(int opcode, Status<int>* ret, const iovec* send_vector,
                       size_t send_count, const iovec* receive_vector,
                       size_t receive_count);
  void SendTransaction(int opcode, Status<LocalHandle>* ret,
                       const iovec* send_vector, size_t send_count,
                       const iovec* receive_vector, size_t receive_count);
  void SendTransaction(int opcode, Status<LocalChannelHandle>* ret,
                       const iovec* send_vector, size_t send_count,
                       const iovec* receive_vector, size_t receive_count);
  void CheckDisconnect(int error);

  template <typename T>
  inline void CheckDisconnect(const Status<T>& status) {
    if (!status)
      CheckDisconnect(status.error());
  }

  Client& client_;
  void* state_{nullptr};
  bool state_allocated_{false};
};

}  // namespace pdx
}  // namespace android

#endif  // ANDROID_PDX_CLIENT_H_
