//
// Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2018
//
// Distributed under the Boost Software License, Version 1.0. (See accompanying
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
//
#pragma once

#include "td/mtproto/AuthData.h"
#include "td/mtproto/PacketStorer.h"
#include "td/mtproto/utils.h"

#include "td/mtproto/mtproto_api.h"

#include "td/utils/logging.h"
#include "td/utils/Slice.h"
#include "td/utils/Time.h"

namespace td {
namespace mtproto {
template <class Object, class ObjectStorer>
class ObjectImpl {
 public:
  ObjectImpl(bool not_empty, Object &&object, AuthData *auth_data, bool need_ack = false)
      : not_empty_(not_empty), object_(std::move(object)), object_storer_(object_) {
    if (empty()) {
      return;
    }
    message_id_ = auth_data->next_message_id(Time::now_cached());
    seq_no_ = auth_data->next_seq_no(need_ack);
  }
  template <class T>
  void do_store(T &storer) const {
    if (empty()) {
      return;
    }
    storer.store_binary(message_id_);
    storer.store_binary(seq_no_);
    storer.store_binary(static_cast<int32>(object_storer_.size()));
    storer.store_storer(object_storer_);
  }
  bool not_empty() const {
    return not_empty_;
  }
  bool empty() const {
    return !not_empty_;
  }
  uint64 get_message_id() const {
    return message_id_;
  }

 private:
  bool not_empty_;
  Object object_;
  ObjectStorer object_storer_;
  uint64 message_id_;
  int32 seq_no_;
};

using AckImpl = ObjectImpl<mtproto_api::msgs_ack, TLObjectStorer<mtproto_api::msgs_ack>>;
using PingImpl = ObjectImpl<mtproto_api::ping_delay_disconnect, TLStorer<mtproto_api::ping_delay_disconnect>>;
using HttpWaitImpl = ObjectImpl<mtproto_api::http_wait, TLStorer<mtproto_api::http_wait>>;
using GetFutureSaltsImpl = ObjectImpl<mtproto_api::get_future_salts, TLStorer<mtproto_api::get_future_salts>>;
using ResendImpl = ObjectImpl<mtproto_api::msg_resend_req, TLObjectStorer<mtproto_api::msg_resend_req>>;
using CancelImpl = ObjectImpl<mtproto_api::rpc_drop_answer, TLStorer<mtproto_api::rpc_drop_answer>>;
using GetInfoImpl = ObjectImpl<mtproto_api::msgs_state_req, TLObjectStorer<mtproto_api::msgs_state_req>>;

class CancelVectorImpl {
 public:
  CancelVectorImpl(bool not_empty, const vector<int64> &to_cancel, AuthData *auth_data, bool need_ack) {
    storers_.reserve(to_cancel.size());
    for (auto &request_id : to_cancel) {
      storers_.emplace_back(true, mtproto_api::rpc_drop_answer(request_id), auth_data, true);
    }
  }

  template <class T>
  void do_store(T &storer) const {
    for (auto &s : storers_) {
      storer.store_storer(s);
    }
  }
  bool not_empty() const {
    return !storers_.empty();
  }
  uint64 get_message_id() const {
    CHECK(storers_.size() == 1);
    return storers_[0].get_message_id();
  }

 private:
  vector<PacketStorer<CancelImpl>> storers_;
};

class QueryImpl {
 public:
  QueryImpl(const Query &query, Slice header) : query_(query), header_(header) {
  }

  template <class T>
  void do_store(T &storer) const {
    storer.store_binary(query_.message_id);
    storer.store_binary(query_.seq_no);
    Slice header = this->header_;
    Slice invoke_header = Slice();

// TODO(refactor):
// invokeAfterMsg#cb9f372d {X:Type} msg_id:long query:!X = X;
// This code makes me very sad.
// InvokeAfterMsg is not even in mtproto_api. It is in telegram_api.
#pragma pack(push, 4)
    struct {
      uint32 constructor_id;
      uint64 invoke_after_id;
    } invoke_data;
#pragma pack(pop)
    if (query_.invoke_after_id != 0) {
      invoke_data.constructor_id = 0xcb9f372d;
      invoke_data.invoke_after_id = query_.invoke_after_id;
      invoke_header = Slice(reinterpret_cast<const uint8 *>(&invoke_data), sizeof(invoke_data));
    }

    Slice data = query_.packet.as_slice();
    mtproto_api::gzip_packed packed(data);
    auto plain_storer = create_storer(data);
    auto gzip_storer = create_storer(packed);
    const Storer &data_storer =
        query_.gzip_flag ? static_cast<const Storer &>(gzip_storer) : static_cast<const Storer &>(plain_storer);
    auto invoke_header_storer = create_storer(invoke_header);
    auto header_storer = create_storer(header);
    auto suff_storer = create_storer(invoke_header_storer, data_storer);
    auto all_storer = create_storer(header_storer, suff_storer);

    storer.store_binary(static_cast<uint32>(all_storer.size()));
    storer.store_storer(all_storer);
  }

 private:
  const Query &query_;
  Slice header_;
};

class QueryVectorImpl {
 public:
  QueryVectorImpl(const vector<Query> &to_send, Slice header) : to_send_(to_send), header_(header) {
  }

  template <class T>
  void do_store(T &storer) const {
    if (to_send_.empty()) {
      return;
    }
    for (auto &query : to_send_) {
      storer.store_storer(PacketStorer<QueryImpl>(query, header_));
    }
  }

 private:
  const vector<Query> &to_send_;
  Slice header_;
};

class ContainerImpl {
 public:
  ContainerImpl(int32 cnt, Storer &storer) : cnt_(cnt), storer_(storer) {
  }

  template <class T>
  void do_store(T &storer) const {
    storer.store_binary(mtproto_api::msg_container::ID);
    storer.store_binary(cnt_);
    storer.store_storer(storer_);
  }

 private:
  int32 cnt_;
  Storer &storer_;
};

class CryptoImpl {
 public:
  CryptoImpl(const vector<Query> &to_send, Slice header, vector<int64> &&to_ack, int64 ping_id, int ping_timeout,
             int max_delay, int max_after, int max_wait, int future_salt_n, vector<int64> get_info,
             vector<int64> resend, vector<int64> cancel, AuthData *auth_data, uint64 *container_id, uint64 *get_info_id,
             uint64 *resend_id, uint64 *ping_message_id, uint64 *parent_message_id)
      : query_storer_(to_send, header)
      , ack_empty_(to_ack.empty())
      , ack_storer_(!ack_empty_, mtproto_api::msgs_ack(std::move(to_ack)), auth_data)
      , ping_storer_(ping_id != 0, mtproto_api::ping_delay_disconnect(ping_id, ping_timeout), auth_data)
      , http_wait_storer_(max_delay >= 0, mtproto_api::http_wait(max_delay, max_after, max_wait), auth_data)
      , get_future_salts_storer_(future_salt_n > 0, mtproto_api::get_future_salts(future_salt_n), auth_data)
      , get_info_not_empty_(!get_info.empty())
      , get_info_storer_(get_info_not_empty_, mtproto_api::msgs_state_req(std::move(get_info)), auth_data, true)
      , resend_not_empty_(!resend.empty())
      , resend_storer_(resend_not_empty_, mtproto_api::msg_resend_req(std::move(resend)), auth_data, true)
      , cancel_not_empty_(!cancel.empty())
      , cancel_cnt_(static_cast<int32>(cancel.size()))
      , cancel_storer_(cancel_not_empty_, std::move(cancel), auth_data, true)
      , tmp_storer_(query_storer_, ack_storer_)
      , tmp2_storer_(tmp_storer_, http_wait_storer_)
      , tmp3_storer_(tmp2_storer_, get_future_salts_storer_)
      , tmp4_storer_(tmp3_storer_, get_info_storer_)
      , tmp5_storer_(tmp4_storer_, resend_storer_)
      , tmp6_storer_(tmp5_storer_, cancel_storer_)
      , concat_storer_(tmp6_storer_, ping_storer_)
      , cnt_(static_cast<int32>(to_send.size()) + ack_storer_.not_empty() + ping_storer_.not_empty() +
             http_wait_storer_.not_empty() + get_future_salts_storer_.not_empty() + get_info_storer_.not_empty() +
             resend_storer_.not_empty() + cancel_cnt_)
      , container_storer_(cnt_, concat_storer_) {
    CHECK(cnt_ != 0);
    if (get_info_storer_.not_empty() && get_info_id) {
      *get_info_id = get_info_storer_.get_message_id();
    }
    if (resend_storer_.not_empty() && resend_id) {
      *resend_id = resend_storer_.get_message_id();
    }
    if (ping_storer_.not_empty() && ping_message_id) {
      *ping_message_id = ping_storer_.get_message_id();
    }

    if (cnt_ > 1 ||
        (!to_send.empty() && !auth_data->is_valid_outbound_msg_id(to_send[0].message_id, Time::now_cached()))) {
      type_ = Mixed;
      message_id_ = auth_data->next_message_id(Time::now_cached());
      seq_no_ = auth_data->next_seq_no(false);

      *container_id = message_id_;
      *parent_message_id = message_id_;
    } else if (!to_send.empty()) {
      CHECK(to_send.size() == 1u);
      type_ = OnlyQuery;
      *parent_message_id = to_send[0].message_id;
    } else if (ack_storer_.not_empty()) {
      type_ = OnlyAck;
      *parent_message_id = ack_storer_.get_message_id();
    } else if (ping_storer_.not_empty()) {
      type_ = OnlyPing;
      *parent_message_id = ping_storer_.get_message_id();
    } else if (http_wait_storer_.not_empty()) {
      type_ = OnlyHttpWait;
      *parent_message_id = http_wait_storer_.get_message_id();
    } else if (get_future_salts_storer_.not_empty()) {
      type_ = OnlyGetFutureSalts;
      *parent_message_id = get_future_salts_storer_.get_message_id();
    } else if (get_info_storer_.not_empty()) {
      type_ = OnlyGetInfo;
      *parent_message_id = get_info_storer_.get_message_id();
    } else if (resend_storer_.not_empty()) {
      type_ = OnlyResend;
      *parent_message_id = resend_storer_.get_message_id();
    } else if (cancel_storer_.not_empty()) {
      type_ = OnlyCancel;
      *parent_message_id = cancel_storer_.get_message_id();
    } else {
      UNREACHABLE();
    }
  }

  template <class T>
  void do_store(T &storer) const {
    switch (type_) {
      case OnlyAck:
        return storer.store_storer(ack_storer_);

      case OnlyQuery:
        return storer.store_storer(query_storer_);

      case OnlyPing:
        return storer.store_storer(ping_storer_);

      case OnlyHttpWait:
        return storer.store_storer(http_wait_storer_);

      case OnlyGetFutureSalts:
        return storer.store_storer(get_future_salts_storer_);

      case OnlyResend:
        return storer.store_storer(resend_storer_);

      case OnlyCancel:
        return storer.store_storer(cancel_storer_);

      case OnlyGetInfo:
        return storer.store_storer(get_info_storer_);

      default:
        storer.store_binary(message_id_);
        storer.store_binary(seq_no_);
        storer.store_binary(static_cast<int32>(container_storer_.size()));
        storer.store_storer(container_storer_);
    }
  }

 private:
  PacketStorer<QueryVectorImpl> query_storer_;
  bool ack_empty_;
  PacketStorer<AckImpl> ack_storer_;
  PacketStorer<PingImpl> ping_storer_;
  PacketStorer<HttpWaitImpl> http_wait_storer_;
  PacketStorer<GetFutureSaltsImpl> get_future_salts_storer_;
  bool get_info_not_empty_;
  PacketStorer<GetInfoImpl> get_info_storer_;
  bool resend_not_empty_;
  PacketStorer<ResendImpl> resend_storer_;
  bool cancel_not_empty_;
  int32 cancel_cnt_;
  PacketStorer<CancelVectorImpl> cancel_storer_;
  ConcatStorer tmp_storer_;
  ConcatStorer tmp2_storer_;
  ConcatStorer tmp3_storer_;
  ConcatStorer tmp4_storer_;
  ConcatStorer tmp5_storer_;
  ConcatStorer tmp6_storer_;
  ConcatStorer concat_storer_;
  int32 cnt_;
  PacketStorer<ContainerImpl> container_storer_;
  enum Type {
    OnlyQuery,
    OnlyAck,
    OnlyPing,
    OnlyHttpWait,
    OnlyGetFutureSalts,
    OnlyResend,
    OnlyCancel,
    OnlyGetInfo,
    Mixed
  };
  Type type_;
  uint64 message_id_;
  int32 seq_no_;
};
}  // namespace mtproto
}  // namespace td