#ifndef EMITTERSTATE_H_62B23520_7C8E_11DE_8A39_0800200C9A66
#define EMITTERSTATE_H_62B23520_7C8E_11DE_8A39_0800200C9A66

#if defined(_MSC_VER) ||                                            \
    (defined(__GNUC__) && (__GNUC__ == 3 && __GNUC_MINOR__ >= 4) || \
     (__GNUC__ >= 4))  // GCC supports "pragma once" correctly since 3.4
#pragma once
#endif

#include "setting.h"
#include "yaml-cpp/emitterdef.h"
#include "yaml-cpp/emittermanip.h"

#include <cassert>
#include <memory>
#include <stack>
#include <stdexcept>
#include <vector>

namespace YAML {
struct FmtScope {
  enum value { Local, Global };
};
struct GroupType {
  enum value { NoType, Seq, Map };
};
struct FlowType {
  enum value { NoType, Flow, Block };
};

class EmitterState {
 public:
  EmitterState();
  ~EmitterState();

  // basic state checking
  bool good() const { return m_isGood; }
  const std::string GetLastError() const { return m_lastError; }
  void SetError(const std::string& error) {
    m_isGood = false;
    m_lastError = error;
  }

  // node handling
  void SetAnchor();
  void SetAlias();
  void SetTag();
  void SetNonContent();
  void SetLongKey();
  void ForceFlow();
  void StartedDoc();
  void EndedDoc();
  void StartedScalar();
  void StartedGroup(GroupType::value type);
  void EndedGroup(GroupType::value type);

  EmitterNodeType::value NextGroupType(GroupType::value type) const;
  EmitterNodeType::value CurGroupNodeType() const;

  GroupType::value CurGroupType() const;
  FlowType::value CurGroupFlowType() const;
  std::size_t CurGroupIndent() const;
  std::size_t CurGroupChildCount() const;
  bool CurGroupLongKey() const;

  std::size_t LastIndent() const;
  std::size_t CurIndent() const { return m_curIndent; }
  bool HasAnchor() const { return m_hasAnchor; }
  bool HasAlias() const { return m_hasAlias; }
  bool HasTag() const { return m_hasTag; }
  bool HasBegunNode() const {
    return m_hasAnchor || m_hasTag || m_hasNonContent;
  }
  bool HasBegunContent() const { return m_hasAnchor || m_hasTag; }

  void ClearModifiedSettings();
  void RestoreGlobalModifiedSettings();

  // formatters
  void SetLocalValue(EMITTER_MANIP value);

  bool SetOutputCharset(EMITTER_MANIP value, FmtScope::value scope);
  EMITTER_MANIP GetOutputCharset() const { return m_charset.get(); }

  bool SetStringFormat(EMITTER_MANIP value, FmtScope::value scope);
  EMITTER_MANIP GetStringFormat() const { return m_strFmt.get(); }

  bool SetBoolFormat(EMITTER_MANIP value, FmtScope::value scope);
  EMITTER_MANIP GetBoolFormat() const { return m_boolFmt.get(); }

  bool SetBoolLengthFormat(EMITTER_MANIP value, FmtScope::value scope);
  EMITTER_MANIP GetBoolLengthFormat() const { return m_boolLengthFmt.get(); }

  bool SetBoolCaseFormat(EMITTER_MANIP value, FmtScope::value scope);
  EMITTER_MANIP GetBoolCaseFormat() const { return m_boolCaseFmt.get(); }

  bool SetNullFormat(EMITTER_MANIP value, FmtScope::value scope);
  EMITTER_MANIP GetNullFormat() const { return m_nullFmt.get(); }

  bool SetIntFormat(EMITTER_MANIP value, FmtScope::value scope);
  EMITTER_MANIP GetIntFormat() const { return m_intFmt.get(); }

  bool SetIndent(std::size_t value, FmtScope::value scope);
  std::size_t GetIndent() const { return m_indent.get(); }

  bool SetPreCommentIndent(std::size_t value, FmtScope::value scope);
  std::size_t GetPreCommentIndent() const { return m_preCommentIndent.get(); }
  bool SetPostCommentIndent(std::size_t value, FmtScope::value scope);
  std::size_t GetPostCommentIndent() const { return m_postCommentIndent.get(); }

  bool SetFlowType(GroupType::value groupType, EMITTER_MANIP value,
                   FmtScope::value scope);
  EMITTER_MANIP GetFlowType(GroupType::value groupType) const;

  bool SetMapKeyFormat(EMITTER_MANIP value, FmtScope::value scope);
  EMITTER_MANIP GetMapKeyFormat() const { return m_mapKeyFmt.get(); }

  bool SetFloatPrecision(std::size_t value, FmtScope::value scope);
  std::size_t GetFloatPrecision() const { return m_floatPrecision.get(); }
  bool SetDoublePrecision(std::size_t value, FmtScope::value scope);
  std::size_t GetDoublePrecision() const { return m_doublePrecision.get(); }

 private:
  template <typename T>
  void _Set(Setting<T>& fmt, T value, FmtScope::value scope);

  void StartedNode();

 private:
  // basic state ok?
  bool m_isGood;
  std::string m_lastError;

  // other state
  Setting<EMITTER_MANIP> m_charset;
  Setting<EMITTER_MANIP> m_strFmt;
  Setting<EMITTER_MANIP> m_boolFmt;
  Setting<EMITTER_MANIP> m_boolLengthFmt;
  Setting<EMITTER_MANIP> m_boolCaseFmt;
  Setting<EMITTER_MANIP> m_nullFmt;
  Setting<EMITTER_MANIP> m_intFmt;
  Setting<std::size_t> m_indent;
  Setting<std::size_t> m_preCommentIndent, m_postCommentIndent;
  Setting<EMITTER_MANIP> m_seqFmt;
  Setting<EMITTER_MANIP> m_mapFmt;
  Setting<EMITTER_MANIP> m_mapKeyFmt;
  Setting<std::size_t> m_floatPrecision;
  Setting<std::size_t> m_doublePrecision;

  SettingChanges m_modifiedSettings;
  SettingChanges m_globalModifiedSettings;

  struct Group {
    explicit Group(GroupType::value type_)
        : type(type_),
          flowType{},
          indent(0),
          childCount(0),
          longKey(false),
          modifiedSettings{} {}

    GroupType::value type;
    FlowType::value flowType;
    std::size_t indent;
    std::size_t childCount;
    bool longKey;

    SettingChanges modifiedSettings;

    EmitterNodeType::value NodeType() const {
      if (type == GroupType::Seq) {
        if (flowType == FlowType::Flow)
          return EmitterNodeType::FlowSeq;
        else
          return EmitterNodeType::BlockSeq;
      } else {
        if (flowType == FlowType::Flow)
          return EmitterNodeType::FlowMap;
        else
          return EmitterNodeType::BlockMap;
      }

      // can't get here
      assert(false);
      return EmitterNodeType::NoType;
    }
  };

  std::vector<std::unique_ptr<Group>> m_groups;
  std::size_t m_curIndent;
  bool m_hasAnchor;
  bool m_hasAlias;
  bool m_hasTag;
  bool m_hasNonContent;
  std::size_t m_docCount;
};

template <typename T>
void EmitterState::_Set(Setting<T>& fmt, T value, FmtScope::value scope) {
  switch (scope) {
    case FmtScope::Local:
      m_modifiedSettings.push(fmt.set(value));
      break;
    case FmtScope::Global:
      fmt.set(value);
      m_globalModifiedSettings.push(
          fmt.set(value));  // this pushes an identity set, so when we restore,
      // it restores to the value here, and not the previous one
      break;
    default:
      assert(false);
  }
}
}  // namespace YAML

#endif  // EMITTERSTATE_H_62B23520_7C8E_11DE_8A39_0800200C9A66
