//
// Copyright 2022 Eugene Gershnik
//
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file or at
// https://github.com/gershnik/argum/blob/master/LICENSE
//
#ifndef HEADER_ARGUM_COMMAND_LINE_H_INCLUDED
#define HEADER_ARGUM_COMMAND_LINE_H_INCLUDED

#include "expected.h"
#include "messages.h"
#include "simple-file.h"

#include <string>
#include <string_view>
#include <iterator>
#include <vector>
#include <stack>
#include <span>
#include <algorithm>
#include <set>

namespace Argum {

    ARGUM_MOD_EXPORTED 
    template<class Char>
    auto makeArgSpan(int argc, Char ** argv) -> std::span<const Char *> {
        if (argc > 0)
            return std::span(const_cast<const Char **>(argv + 1), size_t(argc - 1));
        return std::span(const_cast<const Char **>(argv), size_t(0));
    }

    ARGUM_MOD_EXPORTED
    template<class Char>
    class BasicResponseFileReader {
    public:
        using CharType = Char;
        using StringType = std::basic_string<Char>;
        using StringViewType = std::basic_string_view<Char>;

        struct Exception : public BasicParsingException<CharType> {
            ARGUM_IMPLEMENT_EXCEPTION(Exception, BasicParsingException<CharType>, Error::ResponseFileError)

            Exception(const std::filesystem::path & filename, std::error_code error):
                BasicParsingException<CharType>(ErrorCode,
                                                format(Messages<CharType>::errorReadingResponseFile(), filename.native(), error.message())) {
            }
            Exception(std::basic_string<CharType> message): 
                BasicParsingException<CharType>(ErrorCode, std::move(message)) {
            }
        };

    public:
        BasicResponseFileReader(CharType prefix) {
            m_prefixes.emplace_back(1, prefix);
        }
        BasicResponseFileReader(StringType prefix) {
            m_prefixes.emplace_back(std::move(prefix));
        }
        BasicResponseFileReader(std::initializer_list<StringType> prefixes): m_prefixes(prefixes) {
        }

        auto expand(int argc, CharType ** argv) -> ARGUM_EXPECTED(CharType, std::vector<StringType>) {
            return expand(makeArgSpan(argc, argv));
        }

        template<class Splitter>
        auto expand(int argc, CharType ** argv, Splitter && splitter) -> ARGUM_EXPECTED(CharType, std::vector<StringType>) {
            return expand(makeArgSpan(argc, argv), std::forward<Splitter>(splitter));
        }

        template<ArgRange<CharType> Args>
        auto expand(const Args & args) -> ARGUM_EXPECTED(CharType, std::vector<StringType>) {
            return expand(args, [](StringType && str, auto dest) {
                trimInPlace(str);
                if (str.empty())
                    return;
                *dest = std::move(str);
            });
        }

        template<ArgRange<Char> Args, class Splitter>
        auto expand(const Args & args, Splitter && splitter) -> ARGUM_EXPECTED(CharType, std::vector<StringType>)
            requires(std::is_invocable_v<decltype(splitter), StringType &&, std::back_insert_iterator<std::vector<StringType>>>) {
            
            std::vector<StringType> ret;
            InclusionStack stack;

            for(StringViewType arg: args) {

                ARGUM_PROPAGATE_ERROR(this->handleArg(arg, ret, stack, splitter));

                while(!stack.empty()) {

                    auto entry = stack.top();
                    if (entry.current == entry.items.end()) {
                        stack.pop();
                        continue;
                    }
                    ARGUM_PROPAGATE_ERROR(this->handleArg(*entry.current, ret, stack, std::forward<Splitter>(splitter)));
                    ++entry.current;
                }
            }
            return ret;
        }
    private:
        class InclusionStack {
        public:
            struct Value {
                const std::vector<StringType> & items;
                typename std::vector<StringType>::const_iterator & current;
            };
        private:
            struct Entry {
                std::vector<StringType> items;
                typename std::vector<StringType>::const_iterator current;
                std::set<std::filesystem::path>::const_iterator filename;
            };
        public:
            bool empty() const 
                { return m_stack.empty(); }

            Value top() {
                auto & entry = m_stack.top();
                return {entry.items, entry.current};
            }
            const std::filesystem::path & top_filename() const 
                { return *m_stack.top().filename; }
            
            bool hasFilename(const std::filesystem::path & filename) const {
                return m_files.find(filename) != m_files.end();
            }

            void push(std::filesystem::path && filename, std::vector<StringType> && items) {
                auto name_it = m_files.emplace(std::move(filename)).first;
                Entry nextEntry;
                nextEntry.items = std::move(items);
                nextEntry.current = nextEntry.items.cbegin();
                nextEntry.filename = name_it;
                m_stack.emplace(std::move(nextEntry));
            }
            void pop() {
                m_files.erase(m_stack.top().filename);
                m_stack.pop();
            }
        private:
            std::stack<Entry> m_stack;
            std::set<std::filesystem::path> m_files;
        };

        template<class Splitter>
        auto handleArg(StringViewType arg, std::vector<StringType> & dest, 
                       InclusionStack & stack,
                       Splitter && splitter) -> ARGUM_EXPECTED(CharType, void) {

            auto foundIt = std::find_if(this->m_prefixes.begin(), this->m_prefixes.end(), [arg](const StringViewType & prefix) {
                return matchStrictPrefix(arg, prefix);
            });

            if (foundIt != this->m_prefixes.end()) {
                std::filesystem::path filename = arg.substr(foundIt->size());
                if (stack.hasFilename(filename))
                    ARGUM_THROW(Exception, format(Messages<CharType>::recursionInResponseFile(), filename.native(), stack.top_filename()));
                std::vector<StringType> items;
                ARGUM_PROPAGATE_ERROR(this->readResponseFile(filename, items, std::forward<Splitter>(splitter)));
                stack.push(std::move(filename), std::move(items));

            } else {
                dest.emplace_back(arg);
            }
            return ARGUM_VOID_SUCCESS;
        }
    
        template<class Splitter>
        static auto readResponseFile(const std::filesystem::path & path, std::vector<StringType> & dest, 
                                     Splitter && splitter) -> ARGUM_EXPECTED(CharType, void){

            std::error_code error;
            SimpleFile file(path, "r", error);
            if (!file)
                ARGUM_THROW(Exception, path, error);

            do {
                StringType line = file.readLine<CharType>(error);
                if (error)
                    ARGUM_THROW(Exception, path, error);
                
                if (!line.empty())
                    splitter(std::move(line), std::back_inserter(dest));
            } while(!file.eof());

            return ARGUM_VOID_SUCCESS;
        }
    private:
        std::vector<StringType> m_prefixes;
    };

    ARGUM_DECLARE_FRIENDLY_NAMES(ResponseFileReader)

}

#endif 
