#pragma once

#include "byte_writer.h"
#include "cescape.h"
#include "percent_scalar.h"
#include "stream_counter.h"
#include "symbols.h"
#include "varint.h"

#include <library/cpp/yson_pull/consumer.h>
#include <library/cpp/yson_pull/event.h>
#include <library/cpp/yson_pull/output.h>
#include <library/cpp/yson_pull/stream_type.h>
#include <library/cpp/yson_pull/writer.h>

#include <util/generic/vector.h>
#include <util/system/yassert.h>

#include <cmath>

namespace NYsonPull {
    namespace NDetail {
        class writer: public IConsumer {
            enum class state {
                maybe_key,
                maybe_value,
                value,
                value_noattr,
                before_begin,
                before_end,
                after_end,
            };

            byte_writer<stream_counter<false>> stream_;
            TVector<EEventType> stack_;
            bool need_item_separator_ = false;
            EStreamType mode_ = EStreamType::ListFragment;
            state state_ = state::before_begin;

        public:
            void OnBeginStream() override {
                update_state(EEventType::BeginStream);
            }

            void OnEndStream() override {
                update_state(EEventType::EndStream);
                stream_.flush_buffer();
            }

            void OnBeginList() override {
                begin_node();
                write(NSymbol::begin_list);
                update_state(EEventType::BeginList);
                begin_collection(collection_type::list);
            }

            void OnEndList() override {
                update_state(EEventType::EndList);
                end_collection(collection_type::list);
                write(NSymbol::end_list);
                end_node();
            }

            void OnBeginMap() override {
                begin_node();
                write(NSymbol::begin_map);
                update_state(EEventType::BeginMap);
                begin_collection(collection_type::map);
            }

            void OnEndMap() override {
                update_state(EEventType::EndMap);
                end_collection(collection_type::map);
                write(NSymbol::end_map);
                end_node();
            }

            void OnBeginAttributes() override {
                begin_node();
                write(NSymbol::begin_attributes);
                update_state(EEventType::BeginAttributes);
                begin_collection(collection_type::attributes);
            }

            void OnEndAttributes() override {
                update_state(EEventType::EndAttributes);
                end_collection(collection_type::attributes);
                write(NSymbol::end_attributes);
                // no end_node
            }

            void OnEntity() override {
                begin_node();
                update_state(EEventType::Scalar);
                write(NSymbol::entity);
                end_node();
            }

        protected:
            enum class collection_type {
                list,
                map,
                attributes,
            };

            writer(NYsonPull::NOutput::IStream& stream, EStreamType mode)
                : stream_(stream)
                , mode_{mode} {
            }

            bool need_item_separator() const {
                return need_item_separator_;
            }
            void need_item_separator(bool value) {
                need_item_separator_ = value;
            }

            size_t depth() const {
                Y_ASSERT(!stack_.empty());
                if (mode_ == EStreamType::Node) {
                    return stack_.size() - 1;
                } else {
                    return stack_.size() - 2;
                }
            }
            EStreamType mode() const {
                return mode_;
            }

            void write(ui8 c) {
                stream_.write(c);
            }

            void write(TStringBuf value) {
                write_raw(value.data(), value.size());
            }

            void write_raw(const void* ptr, size_t len) {
                stream_.write(static_cast<const ui8*>(ptr), len);
            }

            template <typename T>
            void write_varint(T value) {
                NVarInt::write(stream_, value);
            }

            void write_escaped_string(TStringBuf value) {
                write(NSymbol::quote);
                NCEscape::encode(stream_, value);
                write(NSymbol::quote);
            }

            void push(EEventType type) {
                stack_.push_back(type);
            }

            void pop(EEventType type) {
                if (stack_.empty()) {
                    fail("Unpaired events: empty event stack");
                }
                if (stack_.back() != type) {
                    fail("Unpaired events: expected ", type, ", got ", stack_.back());
                }
                stack_.pop_back();
            }

            void update_state(EEventType event) {
                switch (state_) {
                    case state::before_begin:
                        if (event != EEventType::BeginStream) {
                            fail("Expected begin_stream, got ", event);
                        }
                        begin_stream();
                        return;

                    case state::before_end:
                        if (event != EEventType::EndStream) {
                            fail("Expected end_stream, got ", event);
                        }
                        end_stream();
                        return;

                    case state::after_end:
                        fail("Attempted write past stream end");

                    case state::maybe_key:
                        if (event == EEventType::Key) {
                            state_ = state::value;
                            return;
                        }

                        switch (event) {
                            case EEventType::EndStream:
                                end_stream();
                                return;

                            case EEventType::EndMap:
                                pop(EEventType::BeginMap);
                                next_state();
                                return;

                            case EEventType::EndAttributes:
                                pop(EEventType::BeginAttributes);
                                state_ = state::value_noattr;
                                return;

                            default:
                                fail("Unexpected event ", event, " in maybe_key");
                        }
                        break;

                    case state::maybe_value:
                        switch (event) {
                            case EEventType::EndList:
                                pop(EEventType::BeginList);
                                next_state();
                                return;

                            case EEventType::EndStream:
                                end_stream();
                                return;

                            default:
                                break;
                        }
                        [[fallthrough]];
                    case state::value:
                        if (event == EEventType::BeginAttributes) {
                            push(EEventType::BeginAttributes);
                            next_state();
                            return;
                        }
                        [[fallthrough]];
                    case state::value_noattr:
                        switch (event) {
                            case EEventType::Scalar:
                                next_state();
                                return;

                            case EEventType::BeginList:
                                push(EEventType::BeginList);
                                next_state();
                                return;

                            case EEventType::BeginMap:
                                push(EEventType::BeginMap);
                                next_state();
                                return;

                            default:
                                fail("Unexpected event ", event, " (in value_*)");
                        }
                        break;
                }
            }

            void next_state() {
                Y_ASSERT(!stack_.empty());
                switch (stack_.back()) {
                    case EEventType::BeginMap:
                    case EEventType::BeginAttributes:
                        state_ = state::maybe_key;
                        break;

                    case EEventType::BeginList:
                        state_ = state::maybe_value;
                        break;

                    case EEventType::BeginStream:
                        state_ = state::before_end;
                        break;

                    default:
                        Y_UNREACHABLE();
                }
            }

            void begin_stream() {
                push(EEventType::BeginStream);
                switch (mode_) {
                    case EStreamType::ListFragment:
                        push(EEventType::BeginList);
                        state_ = state::maybe_value;
                        break;

                    case EStreamType::MapFragment:
                        push(EEventType::BeginMap);
                        state_ = state::maybe_key;
                        break;

                    case EStreamType::Node:
                        state_ = state::value;
                        break;
                }
            }

            void end_stream() {
                switch (mode_) {
                    case EStreamType::ListFragment:
                        pop(EEventType::BeginList);
                        break;

                    case EStreamType::MapFragment:
                        pop(EEventType::BeginMap);
                        break;

                    case EStreamType::Node:
                        break;
                }
                pop(EEventType::BeginStream);
                state_ = state::after_end;
            }

            virtual void begin_node() {
                if (need_item_separator_) {
                    write(NSymbol::item_separator);
                }
            }

            virtual void end_node() {
                need_item_separator_ = true;
            }

            virtual void begin_key() {
                begin_node();
            }

            virtual void end_key() {
                need_item_separator_ = false;
                write(NSymbol::key_value_separator);
            }

            virtual void begin_collection(collection_type type) {
                Y_UNUSED(type);
                need_item_separator_ = false;
            }

            virtual void end_collection(collection_type type) {
                need_item_separator_ = (type != collection_type::attributes);
            }

            template <typename... Args>
            ATTRIBUTE(noinline, cold)
            void fail[[noreturn]](const char* msg, Args&&... args) {
                auto formatted_message = format_string(
                    msg,
                    std::forward<Args>(args)...);
                throw NException::TBadOutput(
                    formatted_message,
                    stream_.counter().info());
            }
        };

        class TBinaryWriterImpl final: public writer {
        public:
            TBinaryWriterImpl(NYsonPull::NOutput::IStream& stream, EStreamType mode)
                : writer(stream, mode)
            {
            }

            void OnScalarBoolean(bool value) override {
                update_state(EEventType::Scalar);

                begin_node();
                write(value ? NSymbol::true_marker : NSymbol::false_marker);
                end_node();
            }

            void OnScalarInt64(i64 value) override {
                update_state(EEventType::Scalar);

                begin_node();
                write(NSymbol::int64_marker);
                write_varint(value);
                end_node();
            }

            void OnScalarUInt64(ui64 value) override {
                update_state(EEventType::Scalar);

                begin_node();
                write(NSymbol::uint64_marker);
                write_varint(value);
                end_node();
            }

            void OnScalarFloat64(double value) override {
                update_state(EEventType::Scalar);

                begin_node();
                write(NSymbol::double_marker);
                write_raw(&value, sizeof value);
                end_node();
            }

            void OnScalarString(TStringBuf value) override {
                update_state(EEventType::Scalar);

                begin_node();
                write(NSymbol::string_marker);
                write_varint(static_cast<i32>(value.size()));
                write_raw(value.data(), value.size());
                end_node();
            }

            void OnKey(TStringBuf name) override {
                update_state(EEventType::Key);

                begin_key();
                write(NSymbol::string_marker);
                write_varint(static_cast<i32>(name.size()));
                write_raw(name.data(), name.size());
                end_key();
            }
        };

        class TTextWriterImpl: public writer {
        public:
            TTextWriterImpl(NYsonPull::NOutput::IStream& stream, EStreamType mode)
                : writer(stream, mode)
            {
            }

            void OnScalarBoolean(bool value) override {
                update_state(EEventType::Scalar);

                begin_node();
                write(value ? percent_scalar::true_literal : percent_scalar::false_literal);
                end_node();
            }

            void OnScalarInt64(i64 value) override {
                update_state(EEventType::Scalar);

                char buf[32];
                auto len = ::snprintf(buf, sizeof(buf), "%" PRIi64, value);

                begin_node();
                write_raw(buf, len);
                end_node();
            }

            void OnScalarUInt64(ui64 value) override {
                update_state(EEventType::Scalar);

                char buf[32];
                auto len = ::snprintf(buf, sizeof(buf), "%" PRIu64, value);

                begin_node();
                write_raw(buf, len);
                write('u');
                end_node();
            }

            void OnScalarFloat64(double value) override {
                update_state(EEventType::Scalar);

                begin_node();

                if (std::isfinite(value)) {
                    char buf[32];
                    auto len = ::snprintf(buf, sizeof(buf), "%#.17lg", value);
                    write_raw(buf, len);
                } else if (std::isnan(value)) {
                    write(percent_scalar::nan_literal);
                } else if (value > 0) {
                    write(percent_scalar::positive_inf_literal);
                } else {
                    write(percent_scalar::negative_inf_literal);
                }

                end_node();
            }

            void OnScalarString(TStringBuf value) override {
                update_state(EEventType::Scalar);

                begin_node();
                write_escaped_string(value);
                end_node();
            }

            void OnKey(TStringBuf name) override {
                update_state(EEventType::Key);

                begin_key();
                write_escaped_string(name);
                end_key();
            }

        protected:
            void begin_node() override {
                if (need_item_separator()) {
                    write(NSymbol::item_separator);
                    write(' ');
                }
            }

            void end_node() override {
                if (mode() != EStreamType::Node && depth() == 0) {
                    write(NSymbol::item_separator);
                    write('\n');
                    need_item_separator(false);
                } else {
                    writer::end_node();
                }
            }

            void end_key() override {
                write(' ');
                writer::end_key();
                write(' ');
            }
        };

        class TPrettyWriterImpl final: public TTextWriterImpl {
            size_t indent_size_;

        public:
            TPrettyWriterImpl(
                NYsonPull::NOutput::IStream& stream,
                EStreamType mode,
                size_t indent_size)
                : TTextWriterImpl(stream, mode)
                , indent_size_{indent_size} {
            }

        protected:
            void begin_node() override {
                if (need_item_separator()) {
                    write(NSymbol::item_separator);
                    newline();
                }
            }

            void begin_collection(collection_type type) override {
                TTextWriterImpl::begin_collection(type);
                newline();
            }

            void end_collection(collection_type type) override {
                TTextWriterImpl::end_collection(type);
                newline();
            }

            void newline() {
                write('\n');
                indent(depth());
            }

            void indent(size_t count) {
                for (size_t i = 0; i < count * indent_size_; ++i) {
                    write(' ');
                }
            }
        };

        template <typename T, typename... Args>
        NYsonPull::TWriter make_writer(
            THolder<NYsonPull::NOutput::IStream> stream,
            Args&&... args) {
            auto impl = MakeHolder<T>(*stream, std::forward<Args>(args)...);
            return NYsonPull::TWriter(std::move(stream), std::move(impl));
        }
    }
}