/*=============================================================================
    Copyright (c) 2006 Joel de Guzman
    http://spirit.sourceforge.net/

    Use, modification and distribution is subject to 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)
=============================================================================*/

#include <boost/bind.hpp>
#include <boost/shared_ptr.hpp>
#include <boost/spirit/include/classic_actor.hpp>
#include <boost/spirit/include/classic_confix.hpp>
#include <boost/spirit/include/classic_core.hpp>
#include "actions.hpp"
#include "block_tags.hpp"
#include "files.hpp"
#include "state.hpp"
#include "stream.hpp"
#include "template_stack.hpp"
#include "values.hpp"

namespace quickbook
{
    namespace cl = boost::spirit::classic;

    struct code_snippet_actions
    {
        code_snippet_actions(
            std::vector<template_symbol>& storage_,
            file_ptr source_file_,
            char const* source_type_)
            : last_code_pos(source_file_->source().begin())
            , in_code(false)
            , snippet_stack()
            , storage(storage_)
            , source_file(source_file_)
            , source_type(source_type_)
            , error_count(0)
        {
            source_file->is_code_snippets = true;
            content.start(source_file);
        }

        void mark(string_iterator first, string_iterator last);
        void pass_thru(string_iterator first, string_iterator last);
        void escaped_comment(string_iterator first, string_iterator last);
        void start_snippet(string_iterator first, string_iterator last);
        void start_snippet_impl(std::string const&, string_iterator);
        void end_snippet(string_iterator first, string_iterator last);
        void end_snippet_impl(string_iterator);
        void end_file(string_iterator, string_iterator);

        void append_code(string_iterator first, string_iterator last);
        void close_code();

        struct snippet_data
        {
            snippet_data(std::string const& id_) : id(id_), start_code(false) {}

            std::string id;
            bool start_code;
            string_iterator source_pos;
            mapped_file_builder::pos_type start_pos;
            boost::shared_ptr<snippet_data> next;
        };

        void push_snippet_data(std::string const& id, string_iterator pos)
        {
            boost::shared_ptr<snippet_data> new_snippet(new snippet_data(id));
            new_snippet->next = snippet_stack;
            snippet_stack = new_snippet;
            snippet_stack->start_code = in_code;
            snippet_stack->source_pos = pos;
            snippet_stack->start_pos = content.get_pos();
        }

        boost::shared_ptr<snippet_data> pop_snippet_data()
        {
            boost::shared_ptr<snippet_data> snippet(snippet_stack);
            snippet_stack = snippet->next;
            snippet->next.reset();
            return snippet;
        }

        mapped_file_builder content;
        string_iterator mark_begin, mark_end;
        string_iterator last_code_pos;
        bool in_code;
        boost::shared_ptr<snippet_data> snippet_stack;
        std::vector<template_symbol>& storage;
        file_ptr source_file;
        char const* const source_type;
        int error_count;
    };

    struct python_code_snippet_grammar
        : cl::grammar<python_code_snippet_grammar>
    {
        typedef code_snippet_actions actions_type;

        python_code_snippet_grammar(actions_type& actions_) : actions(actions_)
        {
        }

        template <typename Scanner> struct definition
        {
            typedef code_snippet_actions actions_type;

            definition(python_code_snippet_grammar const& self)
            {
                // clang-format off

                start_ = (*code_elements)           [boost::bind(&actions_type::end_file, &self.actions, _1, _2)]
                    ;

                identifier =
                    (cl::alpha_p | '_') >> *(cl::alnum_p | '_')
                    ;

                code_elements =
                        start_snippet               [boost::bind(&actions_type::start_snippet, &self.actions, _1, _2)]
                    |   end_snippet                 [boost::bind(&actions_type::end_snippet, &self.actions, _1, _2)]
                    |   escaped_comment             [boost::bind(&actions_type::escaped_comment, &self.actions, _1, _2)]
                    |   pass_thru_comment           [boost::bind(&actions_type::pass_thru, &self.actions, _1, _2)]
                    |   ignore                      [boost::bind(&actions_type::append_code, &self.actions, _1, _2)]
                    |   cl::anychar_p
                    ;

                start_snippet =
                        *cl::blank_p
                    >>  !(cl::eol_p >> *cl::blank_p)
                    >>  "#["
                    >>  *cl::blank_p
                    >>  identifier                  [boost::bind(&actions_type::mark, &self.actions, _1, _2)]
                    >>  *(cl::anychar_p - cl::eol_p)
                    ;

                end_snippet =
                        *cl::blank_p
                    >>  !(cl::eol_p >> *cl::blank_p)
                    >>  "#]"
                    >>  *(cl::anychar_p - cl::eol_p)
                    ;

                ignore
                    =   cl::confix_p(
                            *cl::blank_p >> "#<-",
                            *cl::anychar_p,
                            "#->" >> *cl::blank_p >> (cl::eol_p | cl::end_p)
                        )
                    |   cl::confix_p(
                            "\"\"\"<-\"\"\"",
                            *cl::anychar_p,
                            "\"\"\"->\"\"\""
                        )
                    |   cl::confix_p(
                            "\"\"\"<-",
                            *cl::anychar_p,
                            "->\"\"\""
                        )
                    ;

                escaped_comment =
                        cl::confix_p(
                            *cl::space_p >> "#`",
                            (*cl::anychar_p)        [boost::bind(&actions_type::mark, &self.actions, _1, _2)],
                            (cl::eol_p | cl::end_p)
                        )
                    |   cl::confix_p(
                            *cl::space_p >> "\"\"\"`",
                            (*cl::anychar_p)        [boost::bind(&actions_type::mark, &self.actions, _1, _2)],
                            "\"\"\""
                        )
                    ;

                // Note: Unlike escaped_comment and ignore, this doesn't
                // swallow preceeding whitespace.
                pass_thru_comment
                    =   "#=" >> (cl::eps_p - '=')
                    >>  (   *(cl::anychar_p - cl::eol_p)
                        >>  (cl::eol_p | cl::end_p)
                        )                           [boost::bind(&actions_type::mark, &self.actions, _1, _2)]
                    |   cl::confix_p(
                            "\"\"\"=" >> (cl::eps_p - '='),
                            (*cl::anychar_p)        [boost::bind(&actions_type::mark, &self.actions, _1, _2)],
                            "\"\"\""
                        )
                    ;

                // clang-format on
            }

            cl::rule<Scanner> start_, identifier, code_elements, start_snippet,
                end_snippet, escaped_comment, pass_thru_comment, ignore;

            cl::rule<Scanner> const& start() const { return start_; }
        };

        actions_type& actions;
    };

    struct cpp_code_snippet_grammar : cl::grammar<cpp_code_snippet_grammar>
    {
        typedef code_snippet_actions actions_type;

        cpp_code_snippet_grammar(actions_type& actions_) : actions(actions_) {}

        template <typename Scanner> struct definition
        {
            definition(cpp_code_snippet_grammar const& self)
            {
                // clang-format off

                start_ = (*code_elements)           [boost::bind(&actions_type::end_file, &self.actions, _1, _2)]
                    ;

                identifier =
                    (cl::alpha_p | '_') >> *(cl::alnum_p | '_')
                    ;

                code_elements =
                        start_snippet               [boost::bind(&actions_type::start_snippet, &self.actions, _1, _2)]
                    |   end_snippet                 [boost::bind(&actions_type::end_snippet, &self.actions, _1, _2)]
                    |   escaped_comment             [boost::bind(&actions_type::escaped_comment, &self.actions, _1, _2)]
                    |   ignore                      [boost::bind(&actions_type::append_code, &self.actions, _1, _2)]
                    |   pass_thru_comment           [boost::bind(&actions_type::pass_thru, &self.actions, _1, _2)]
                    |   cl::anychar_p
                    ;

                start_snippet =
                            *cl::blank_p
                        >>  !(cl::eol_p >> *cl::blank_p)
                        >>  "//["
                        >>  *cl::blank_p
                        >>  identifier              [boost::bind(&actions_type::mark, &self.actions, _1, _2)]
                        >>  *(cl::anychar_p - cl::eol_p)
                    |
                            *cl::blank_p
                        >>  cl::eol_p
                        >>  *cl::blank_p
                        >>  "/*["
                        >>  *cl::space_p
                        >>  identifier              [boost::bind(&actions_type::mark, &self.actions, _1, _2)]
                        >>  *cl::space_p
                        >>  "*/"
                        >>  *cl::blank_p
                        >>  cl::eps_p(cl::eol_p)
                    |
                            "/*["
                        >>  *cl::space_p
                        >>  identifier              [boost::bind(&actions_type::mark, &self.actions, _1, _2)]
                        >>  *cl::space_p
                        >>  "*/"
                    ;

                end_snippet =
                            *cl::blank_p
                        >>  !(cl::eol_p >> *cl::blank_p)
                        >>  "//]"
                        >>  *(cl::anychar_p - cl::eol_p)
                    |
                            *cl::blank_p
                        >>  cl::eol_p
                        >>  *cl::blank_p
                        >>  "/*]*/"
                        >>  *cl::blank_p
                        >>  cl::eps_p(cl::eol_p)
                    |
                            "/*[*/"
                    ;

                ignore
                    =   cl::confix_p(
                            *cl::blank_p >> "//<-",
                            *cl::anychar_p,
                            "//->"
                        )
                    >>  *cl::blank_p
                    >>  cl::eol_p
                    |   cl::confix_p(
                            "/*<-*/",
                            *cl::anychar_p,
                            "/*->*/"
                        )
                    |   cl::confix_p(
                            "/*<-",
                            *cl::anychar_p,
                            "->*/"
                        )
                    ;

                escaped_comment
                    =   cl::confix_p(
                            *cl::space_p >> "//`",
                            (*cl::anychar_p)        [boost::bind(&actions_type::mark, &self.actions, _1, _2)],
                            (cl::eol_p | cl::end_p)
                        )
                    |   cl::confix_p(
                            *cl::space_p >> "/*`",
                            (*cl::anychar_p)        [boost::bind(&actions_type::mark, &self.actions, _1, _2)],
                            "*/"
                        )
                    ;

                // Note: Unlike escaped_comment and ignore, this doesn't
                // swallow preceeding whitespace.
                pass_thru_comment
                    =   "//=" >> (cl::eps_p - '=')
                    >>  (   *(cl::anychar_p - cl::eol_p)
                        >>  (cl::eol_p | cl::end_p)
                        )                           [boost::bind(&actions_type::mark, &self.actions, _1, _2)]
                    |   cl::confix_p(
                            "/*=" >> (cl::eps_p - '='),
                            (*cl::anychar_p)        [boost::bind(&actions_type::mark, &self.actions, _1, _2)],
                            "*/"
                        )
                    ;

                // clang-format on
            }

            cl::rule<Scanner> start_, identifier, code_elements, start_snippet,
                end_snippet, escaped_comment, pass_thru_comment, ignore;

            cl::rule<Scanner> const& start() const { return start_; }
        };

        actions_type& actions;
    };

    int load_snippets(
        fs::path const& filename,
        std::vector<template_symbol>& storage // snippets are stored in a
                                              // vector of template_symbols
        ,
        std::string const& extension,
        value::tag_type load_type)
    {
        ignore_variable(&load_type); // Avoid unreferenced parameter warning.
        assert(
            load_type == block_tags::include ||
            load_type == block_tags::import);

        bool is_python = extension == ".py" || extension == ".jam";
        code_snippet_actions a(
            storage, load(filename, qbk_version_n),
            is_python ? "[python]" : "[c++]");

        string_iterator first(a.source_file->source().begin());
        string_iterator last(a.source_file->source().end());

        cl::parse_info<string_iterator> info;

        if (is_python) {
            info = boost::spirit::classic::parse(
                first, last, python_code_snippet_grammar(a));
        }
        else {
            info = boost::spirit::classic::parse(
                first, last, cpp_code_snippet_grammar(a));
        }

        assert(info.full);
        return a.error_count;
    }

    void code_snippet_actions::append_code(
        string_iterator first, string_iterator last)
    {
        assert(last_code_pos <= first);

        if (snippet_stack) {
            if (last_code_pos != first) {
                if (!in_code) {
                    content.add_at_pos("\n\n", last_code_pos);
                    content.add_at_pos(source_type, last_code_pos);
                    content.add_at_pos("```\n", last_code_pos);

                    in_code = true;
                }

                content.add(quickbook::string_view(
                    last_code_pos, first - last_code_pos));
            }
        }

        last_code_pos = last;
    }

    void code_snippet_actions::close_code()
    {
        if (!snippet_stack) return;

        if (in_code) {
            content.add_at_pos("\n```\n\n", last_code_pos);
            in_code = false;
        }
    }

    void code_snippet_actions::mark(string_iterator first, string_iterator last)
    {
        mark_begin = first;
        mark_end = last;
    }

    void code_snippet_actions::pass_thru(
        string_iterator first, string_iterator last)
    {
        if (!snippet_stack) return;
        append_code(first, last);

        if (!in_code) {
            content.add_at_pos("\n\n", first);
            content.add_at_pos(source_type, first);
            content.add_at_pos("```\n", first);
            in_code = true;
        }

        content.add(quickbook::string_view(mark_begin, mark_end - mark_begin));
    }

    void code_snippet_actions::escaped_comment(
        string_iterator first, string_iterator last)
    {
        append_code(first, last);
        close_code();

        if (mark_begin != mark_end) {
            if (!snippet_stack) {
                start_snippet_impl("!", first);
            }

            snippet_data& snippet = *snippet_stack;

            content.add_at_pos("\n", mark_begin);
            content.unindent_and_add(
                quickbook::string_view(mark_begin, mark_end - mark_begin));

            if (snippet.id == "!") {
                end_snippet_impl(last);
            }
        }
    }

    void code_snippet_actions::start_snippet(
        string_iterator first, string_iterator last)
    {
        append_code(first, last);
        start_snippet_impl(std::string(mark_begin, mark_end), first);
    }

    void code_snippet_actions::end_snippet(
        string_iterator first, string_iterator last)
    {
        append_code(first, last);

        if (!snippet_stack) {
            if (qbk_version_n >= 106u) {
                detail::outerr(source_file, first)
                    << "Mismatched end snippet." << std::endl;
                ++error_count;
            }
            else {
                detail::outwarn(source_file, first)
                    << "Mismatched end snippet." << std::endl;
            }
            return;
        }

        end_snippet_impl(first);
    }

    void code_snippet_actions::end_file(string_iterator, string_iterator pos)
    {
        append_code(pos, pos);
        close_code();

        while (snippet_stack) {
            if (qbk_version_n >= 106u) {
                detail::outerr(source_file->path)
                    << "Unclosed snippet '" << snippet_stack->id << "'"
                    << std::endl;
                ++error_count;
            }
            else {
                detail::outwarn(source_file->path)
                    << "Unclosed snippet '" << snippet_stack->id << "'"
                    << std::endl;
            }

            end_snippet_impl(pos);
        }
    }

    void code_snippet_actions::start_snippet_impl(
        std::string const& id, string_iterator position)
    {
        push_snippet_data(id, position);
    }

    void code_snippet_actions::end_snippet_impl(string_iterator position)
    {
        assert(snippet_stack);

        boost::shared_ptr<snippet_data> snippet = pop_snippet_data();

        mapped_file_builder f;
        f.start(source_file);
        if (snippet->start_code) {
            f.add_at_pos("\n\n", snippet->source_pos);
            f.add_at_pos(source_type, snippet->source_pos);
            f.add_at_pos("```\n", snippet->source_pos);
        }
        f.add(content, snippet->start_pos, content.get_pos());
        if (in_code) {
            f.add_at_pos("\n```\n\n", position);
        }

        std::vector<std::string> params;

        file_ptr body = f.release();

        storage.push_back(template_symbol(
            snippet->id, params,
            qbk_value(
                body, body->source().begin(), body->source().end(),
                template_tags::snippet)));
    }
}
