//  Copyright (c) 2011-present, Facebook, Inc.  All rights reserved.
//  This source code is licensed under both the GPLv2 (found in the
//  COPYING file in the root directory) and Apache 2.0 License
//  (found in the LICENSE.Apache file in the root directory).

/*
 * This file defines FbsonWriterT (template) and FbsonWriter.
 *
 * FbsonWriterT is a template class which implements an FBSON serializer.
 * Users call various write functions of FbsonWriterT object to write values
 * directly to FBSON packed bytes. All write functions of value or key return
 * the number of bytes written to FBSON, or 0 if there is an error. To write an
 * object, an array, or a string, you must call writeStart[..] before writing
 * values or key, and call writeEnd[..] after finishing at the end.
 *
 * By default, an FbsonWriterT object creates an output stream buffer.
 * Alternatively, you can also pass any output stream object to a writer, as
 * long as the stream object implements some basic functions of std::ostream
 * (such as FbsonOutStream, see FbsonStream.h).
 *
 * FbsonWriter specializes FbsonWriterT with FbsonOutStream type (see
 * FbsonStream.h). So unless you want to provide own a different output stream
 * type, use FbsonParser object.
 *
 * @author Tian Xia <tianx@fb.com>
 */

#pragma once

#include <stack>
#include "FbsonDocument.h"
#include "FbsonStream.h"

// conversion' conversion from 'type1' to 'type2', possible loss of data
// Can not restore at the header end as the warnings are emitted at the point of
// template instantiation
#if defined(_MSC_VER)
#pragma warning(disable : 4244)
#endif

namespace fbson {

template <class OS_TYPE>
class FbsonWriterT {
 public:
  FbsonWriterT()
      : alloc_(true), hasHdr_(false), kvState_(WS_Value), str_pos_(0) {
    os_ = new OS_TYPE();
  }

  explicit FbsonWriterT(OS_TYPE& os)
      : os_(&os),
        alloc_(false),
        hasHdr_(false),
        kvState_(WS_Value),
        str_pos_(0) {}

  ~FbsonWriterT() {
    if (alloc_) {
      delete os_;
    }
  }

  void reset() {
    os_->clear();
    os_->seekp(0);
    hasHdr_ = false;
    kvState_ = WS_Value;
    for (; !stack_.empty(); stack_.pop())
      ;
  }

  // write a key string (or key id if an external dict is provided)
  uint32_t writeKey(const char* key,
                    uint8_t len,
                    hDictInsert handler = nullptr) {
    if (len && !stack_.empty() && verifyKeyState()) {
      int key_id = -1;
      if (handler) {
        key_id = handler(key, len);
      }

      uint32_t size = sizeof(uint8_t);
      if (key_id < 0) {
        os_->put(len);
        os_->write(key, len);
        size += len;
      } else if (key_id <= FbsonKeyValue::sMaxKeyId) {
        FbsonKeyValue::keyid_type idx = key_id;
        os_->put(0);
        os_->write((char*)&idx, sizeof(FbsonKeyValue::keyid_type));
        size += sizeof(FbsonKeyValue::keyid_type);
      } else { // key id overflow
        assert(0);
        return 0;
      }

      kvState_ = WS_Key;
      return size;
    }

    return 0;
  }

  // write a key id
  uint32_t writeKey(FbsonKeyValue::keyid_type idx) {
    if (!stack_.empty() && verifyKeyState()) {
      os_->put(0);
      os_->write((char*)&idx, sizeof(FbsonKeyValue::keyid_type));
      kvState_ = WS_Key;
      return sizeof(uint8_t) + sizeof(FbsonKeyValue::keyid_type);
    }

    return 0;
  }

  uint32_t writeNull() {
    if (!stack_.empty() && verifyValueState()) {
      os_->put((FbsonTypeUnder)FbsonType::T_Null);
      kvState_ = WS_Value;
      return sizeof(FbsonValue);
    }

    return 0;
  }

  uint32_t writeBool(bool b) {
    if (!stack_.empty() && verifyValueState()) {
      if (b) {
        os_->put((FbsonTypeUnder)FbsonType::T_True);
      } else {
        os_->put((FbsonTypeUnder)FbsonType::T_False);
      }

      kvState_ = WS_Value;
      return sizeof(FbsonValue);
    }

    return 0;
  }

  uint32_t writeInt8(int8_t v) {
    if (!stack_.empty() && verifyValueState()) {
      os_->put((FbsonTypeUnder)FbsonType::T_Int8);
      os_->put(v);
      kvState_ = WS_Value;
      return sizeof(Int8Val);
    }

    return 0;
  }

  uint32_t writeInt16(int16_t v) {
    if (!stack_.empty() && verifyValueState()) {
      os_->put((FbsonTypeUnder)FbsonType::T_Int16);
      os_->write((char*)&v, sizeof(int16_t));
      kvState_ = WS_Value;
      return sizeof(Int16Val);
    }

    return 0;
  }

  uint32_t writeInt32(int32_t v) {
    if (!stack_.empty() && verifyValueState()) {
      os_->put((FbsonTypeUnder)FbsonType::T_Int32);
      os_->write((char*)&v, sizeof(int32_t));
      kvState_ = WS_Value;
      return sizeof(Int32Val);
    }

    return 0;
  }

  uint32_t writeInt64(int64_t v) {
    if (!stack_.empty() && verifyValueState()) {
      os_->put((FbsonTypeUnder)FbsonType::T_Int64);
      os_->write((char*)&v, sizeof(int64_t));
      kvState_ = WS_Value;
      return sizeof(Int64Val);
    }

    return 0;
  }

  uint32_t writeDouble(double v) {
    if (!stack_.empty() && verifyValueState()) {
      os_->put((FbsonTypeUnder)FbsonType::T_Double);
      os_->write((char*)&v, sizeof(double));
      kvState_ = WS_Value;
      return sizeof(DoubleVal);
    }

    return 0;
  }

  // must call writeStartString before writing a string val
  bool writeStartString() {
    if (!stack_.empty() && verifyValueState()) {
      os_->put((FbsonTypeUnder)FbsonType::T_String);
      str_pos_ = os_->tellp();

      // fill the size bytes with 0 for now
      uint32_t size = 0;
      os_->write((char*)&size, sizeof(uint32_t));

      kvState_ = WS_String;
      return true;
    }

    return false;
  }

  // finish writing a string val
  bool writeEndString() {
    if (kvState_ == WS_String) {
      std::streampos cur_pos = os_->tellp();
      int32_t size = (int32_t)(cur_pos - str_pos_ - sizeof(uint32_t));
      assert(size >= 0);

      os_->seekp(str_pos_);
      os_->write((char*)&size, sizeof(uint32_t));
      os_->seekp(cur_pos);

      kvState_ = WS_Value;
      return true;
    }

    return false;
  }

  uint32_t writeString(const char* str, uint32_t len) {
    if (kvState_ == WS_String) {
      os_->write(str, len);
      return len;
    }

    return 0;
  }

  uint32_t writeString(char ch) {
    if (kvState_ == WS_String) {
      os_->put(ch);
      return 1;
    }

    return 0;
  }

  // must call writeStartBinary before writing a binary val
  bool writeStartBinary() {
    if (!stack_.empty() && verifyValueState()) {
      os_->put((FbsonTypeUnder)FbsonType::T_Binary);
      str_pos_ = os_->tellp();

      // fill the size bytes with 0 for now
      uint32_t size = 0;
      os_->write((char*)&size, sizeof(uint32_t));

      kvState_ = WS_Binary;
      return true;
    }

    return false;
  }

  // finish writing a binary val
  bool writeEndBinary() {
    if (kvState_ == WS_Binary) {
      std::streampos cur_pos = os_->tellp();
      int32_t size = (int32_t)(cur_pos - str_pos_ - sizeof(uint32_t));
      assert(size >= 0);

      os_->seekp(str_pos_);
      os_->write((char*)&size, sizeof(uint32_t));
      os_->seekp(cur_pos);

      kvState_ = WS_Value;
      return true;
    }

    return false;
  }

  uint32_t writeBinary(const char* bin, uint32_t len) {
    if (kvState_ == WS_Binary) {
      os_->write(bin, len);
      return len;
    }

    return 0;
  }

  // must call writeStartObject before writing an object val
  bool writeStartObject() {
    if (stack_.empty() || verifyValueState()) {
      if (stack_.empty()) {
        // if this is a new FBSON, write the header
        if (!hasHdr_) {
          writeHeader();
        } else
          return false;
      }

      os_->put((FbsonTypeUnder)FbsonType::T_Object);
      // save the size position
      stack_.push(WriteInfo({WS_Object, os_->tellp()}));

      // fill the size bytes with 0 for now
      uint32_t size = 0;
      os_->write((char*)&size, sizeof(uint32_t));

      kvState_ = WS_Value;
      return true;
    }

    return false;
  }

  // finish writing an object val
  bool writeEndObject() {
    if (!stack_.empty() && stack_.top().state == WS_Object &&
        kvState_ == WS_Value) {
      WriteInfo& ci = stack_.top();
      std::streampos cur_pos = os_->tellp();
      int32_t size = (int32_t)(cur_pos - ci.sz_pos - sizeof(uint32_t));
      assert(size >= 0);

      os_->seekp(ci.sz_pos);
      os_->write((char*)&size, sizeof(uint32_t));
      os_->seekp(cur_pos);
      stack_.pop();

      return true;
    }

    return false;
  }

  // must call writeStartArray before writing an array val
  bool writeStartArray() {
    if (stack_.empty() || verifyValueState()) {
      if (stack_.empty()) {
        // if this is a new FBSON, write the header
        if (!hasHdr_) {
          writeHeader();
        } else
          return false;
      }

      os_->put((FbsonTypeUnder)FbsonType::T_Array);
      // save the size position
      stack_.push(WriteInfo({WS_Array, os_->tellp()}));

      // fill the size bytes with 0 for now
      uint32_t size = 0;
      os_->write((char*)&size, sizeof(uint32_t));

      kvState_ = WS_Value;
      return true;
    }

    return false;
  }

  // finish writing an array val
  bool writeEndArray() {
    if (!stack_.empty() && stack_.top().state == WS_Array &&
        kvState_ == WS_Value) {
      WriteInfo& ci = stack_.top();
      std::streampos cur_pos = os_->tellp();
      int32_t size = (int32_t)(cur_pos - ci.sz_pos - sizeof(uint32_t));
      assert(size >= 0);

      os_->seekp(ci.sz_pos);
      os_->write((char*)&size, sizeof(uint32_t));
      os_->seekp(cur_pos);
      stack_.pop();

      return true;
    }

    return false;
  }

  OS_TYPE* getOutput() { return os_; }

 private:
  // verify we are in the right state before writing a value
  bool verifyValueState() {
    assert(!stack_.empty());
    return (stack_.top().state == WS_Object && kvState_ == WS_Key) ||
           (stack_.top().state == WS_Array && kvState_ == WS_Value);
  }

  // verify we are in the right state before writing a key
  bool verifyKeyState() {
    assert(!stack_.empty());
    return stack_.top().state == WS_Object && kvState_ == WS_Value;
  }

  void writeHeader() {
    os_->put(FBSON_VER);
    hasHdr_ = true;
  }

 private:
  enum WriteState {
    WS_NONE,
    WS_Array,
    WS_Object,
    WS_Key,
    WS_Value,
    WS_String,
    WS_Binary,
  };

  struct WriteInfo {
    WriteState state;
    std::streampos sz_pos;
  };

 private:
  OS_TYPE* os_;
  bool alloc_;
  bool hasHdr_;
  WriteState kvState_; // key or value state
  std::streampos str_pos_;
  std::stack<WriteInfo> stack_;
};

typedef FbsonWriterT<FbsonOutStream> FbsonWriter;

} // namespace fbson
