/*
  Copyright 2021 Statoil ASA.

  This file is part of the Open Porous Media project (OPM).

  OPM is free software: you can redistribute it and/or modify
  it under the terms of the GNU General Public License as published by
  the Free Software Foundation, either version 3 of the License, or
  (at your option) any later version.

  OPM is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU General Public License for more details.

  along with OPM.  If not, see <http://www.gnu.org/licenses/>.
*/

#include <opm/input/eclipse/Deck/FileDeck.hpp>

#include <opm/common/utility/FileSystem.hpp>

#include <opm/input/eclipse/Deck/Deck.hpp>
#include <opm/input/eclipse/Deck/DeckValue.hpp>
#include <opm/input/eclipse/Deck/DeckOutput.hpp>

#include <opm/input/eclipse/Parser/ParserKeywords/D.hpp>
#include <opm/input/eclipse/Parser/ParserKeywords/R.hpp>
#include <opm/input/eclipse/Parser/ParserKeywords/S.hpp>
#include <opm/input/eclipse/Parser/ParserKeywords/T.hpp>

#include <algorithm>
#include <cstddef>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <optional>
#include <stdexcept>
#include <string>
#include <unordered_map>
#include <unordered_set>
#include <vector>

#include <fmt/format.h>

namespace fs = std::filesystem;

namespace {

    const auto rst_keep_in_solution = std::unordered_set<std::string> {
        "RPTRST",
    };

    const auto rst_keep_in_schedule = std::unordered_set<std::string> {
        "VFPPROD", "VFPINJ", "RPTSCHED", "RPTRST", "TUNING", "MESSAGES",
    };

    void INCLUDE(std::ostream& stream, const std::string& fname)
    {
        stream << fmt::format(R"(
INCLUDE
   '{}' /
)", fname);
    }

    void IMPORT(std::ostream& stream, const std::string& fname)
    {
        stream << fmt::format(R"(
IMPORT
   '{}' /
)", fname);
    }

    void touch_file(const fs::path& file)
    {
        if (fs::exists(file)) {
            return;
        }

        if (const auto& parent_path = file.parent_path();
            !fs::is_directory(parent_path))
        {
            fs::create_directories(parent_path);
        }

        std::ofstream {file};
    }

} // Anonymous namespace

namespace Opm {

FileDeck::Index& FileDeck::Index::operator--()
{
    if (this->keyword_index > 0) {
        --this->keyword_index;
    }
    else {
        if (this->file_index == 0) {
            throw std::logic_error("Going beyond start of container");
        }

        --this->file_index;
        this->keyword_index = this->deck->blocks[this->file_index].size() - 1;
    }

    return *this;
}

FileDeck::Index FileDeck::Index::operator--(int)
{
    auto current = *this;

    this->operator--();

    return current;
}

FileDeck::Index& FileDeck::Index::operator++()
{
    if (this->deck->blocks.empty()) {
        throw std::logic_error("Trying to iterate empty container");
    }

    const auto& block = this->deck->blocks[this->file_index];
    if (this->keyword_index < block.size() - 1) {
        ++this->keyword_index;
    }
    else {
        ++this->file_index;
        this->keyword_index = 0;
    }

    return *this;
}

FileDeck::Index FileDeck::Index::operator++(int)
{
    auto current = *this;
    this->operator++();
    return current;
}

FileDeck::Index FileDeck::Index::operator+(std::size_t shift) const
{
    auto sum = *this;

    for (std::size_t arg = 0; arg < shift; ++arg) {
        ++sum;
    }

    return sum;
}

bool FileDeck::Index::operator==(const Index& other) const
{
    return (this->file_index == other.file_index)
        && (this->keyword_index == other.keyword_index)
        ;
}

bool FileDeck::Index::operator!=(const Index& other) const
{
    return !(*this == other);
}

bool FileDeck::Index::operator<(const Index& other) const
{
    if (this->file_index < other.file_index) {
        return true;
    }

    if (this->file_index == other.file_index) {
        return this->keyword_index < other.keyword_index;
    }

    return false;
}

FileDeck::FileDeck(const Deck& deck)
    : input_directory(fs::absolute(deck.getInputPath().empty() ? fs::current_path() : fs::path(deck.getInputPath())))
    , deck_tree(deck.tree())
{
    if (deck.empty()) {
        return;
    }

    std::size_t deck_index = 0;
    while (true) {
        const auto& current_file = deck[deck_index].location().filename;

        FileDeck::Block block(current_file);
        block.load(deck, deck_index);
        if (block.is_binary) {
            this->deck_tree.add_include(this->blocks.back().fname, block.fname);
        }
        deck_index += block.size();
        this->blocks.push_back(std::move(block));

        if (deck_index >= deck.size()) {
            break;
        }
    }

    this->modified_files.insert(this->blocks[0].fname);
}

const DeckKeyword& FileDeck::operator[](const Index& index) const
{
    const auto& file_block = this->blocks.at(index.file_index);
    return file_block.keywords.at(index.keyword_index);
}

void FileDeck::Block::load(const Deck& deck, std::size_t deck_index)
{
    const auto& loc = deck[deck_index].location();
    const auto& current_file = loc.filename;
    this->is_binary = loc.is_binary();
    while (true) {
        this->keywords.push_back(deck[deck_index]);

        ++deck_index;

        if ((deck_index >= deck.size()) ||
            (deck[deck_index].location().filename != current_file))
        {
            break;
        }
    }
}

std::size_t FileDeck::Block::size() const
{
    return this->keywords.size();
}

std::optional<std::size_t>
FileDeck::Block::find(const std::string& keyword,
                      const std::size_t keyword_index) const
{
    const auto iter = std::find_if(this->keywords.begin() + keyword_index, this->keywords.end(),
                                   [&keyword](const DeckKeyword& kw)
                                   { return kw.name() == keyword; });
    if (iter == this->keywords.end()) {
        return {};
    }

    return std::distance(this->keywords.begin(), iter);
}

void FileDeck::erase(const FileDeck::Index& index)
{
    auto& block = this->blocks.at(index.file_index);
    this->modified_files.insert(block.fname);
    block.erase(index);
}

void FileDeck::erase(const Index& begin, const Index& end)
{
    auto current = end;
    while (current != begin) {
        --current;
        this->erase(current);
    }
}

bool FileDeck::Block::empty() const
{
    return this->keywords.empty();
}

void FileDeck::Block::erase(const FileDeck::Index& index)
{
    if (index.keyword_index >= this->keywords.size()) {
        throw std::logic_error("Invalid keyword index in block");
    }

    this->keywords.erase(this->keywords.begin() + index.keyword_index);
}

void FileDeck::Block::insert(const std::size_t keyword_index,
                             const DeckKeyword& keyword)
{
    this->keywords.insert(this->keywords.begin() + keyword_index, keyword);
}

void FileDeck::Block::dump(DeckOutput& out) const
{
    for (const auto& kw : this->keywords) {
        kw.write(out);
        out.write_string(out.fmt.keyword_sep);
    }
}

FileDeck::Block::Block(const std::string& filename)
    : fname(fs::canonical(filename).generic_string())
{}

std::optional<FileDeck::Index>
FileDeck::find(const std::string& keyword, const Index& offset) const
{
    std::size_t file_index = offset.file_index;
    std::size_t keyword_index = offset.keyword_index;

    while (true) {
        if (file_index >= this->blocks.size()) {
            break;
        }

        const auto& file_block = this->blocks[file_index];
        const auto& block_index = file_block.find(keyword, keyword_index);
        if (block_index.has_value()) {
            return std::make_optional<FileDeck::Index>(file_index, block_index.value(), this);
        }

        ++file_index;
        keyword_index = 0;
    }

    return {};
}

std::optional<FileDeck::Index>
FileDeck::find(const std::string& keyword) const
{
    return this->find(keyword, this->start());
}

std::size_t FileDeck::count(const std::string& keyword) const
{
    std::size_t c = 0;

    auto index = this->start();
    while (index != this->stop()) {
        c += (*this)[index].name() == keyword;
        ++index;
    }

    return c;
}

void FileDeck::insert(const Index& index, const DeckKeyword& keyword)
{
    auto& block = this->blocks.at(index.file_index);
    block.insert(index.keyword_index, keyword);

    this->modified_files.insert(block.fname);
}

FileDeck::Index FileDeck::start() const
{
    return FileDeck::Index {0, 0, this};
}

FileDeck::Index FileDeck::stop() const
{
    return FileDeck::Index{this->blocks.size(), 0 , this};
}

// ---------------------------------------------------------------------------

bool FileDeck::DumpContext::has_file(const std::string& fname) const
{
    return this->file_map_.find(fname) != this->file_map_.end();
}

std::ofstream* FileDeck::DumpContext::get_stream(const std::string& deck_name)
{
    auto name_iter = this->file_map_.find(deck_name);
    if (name_iter == this->file_map_.end()) {
        return nullptr;
    }

    auto streamPos = this->stream_map_.find(name_iter->second);
    if (streamPos == this->stream_map_.end()) {
        return nullptr;
    }

    return &streamPos->second;
}

std::ofstream&
FileDeck::DumpContext::open_file(const std::string& deck_name,
                                 const fs::path& output_file)
{
    const auto& [filePos, fileInserted] = this->file_map_
        .try_emplace(deck_name, output_file.generic_string());

    if (fileInserted) {
        if (! fs::is_directory(output_file.parent_path())) {
            fs::create_directories(output_file.parent_path());
        }

        const auto& [streamPos, streamInserted] = this->stream_map_
            .try_emplace(output_file.generic_string(), output_file);

        if (! streamInserted) {
            streamPos->second.open(output_file);
        }
    }

    return this->stream_map_.at(output_file.generic_string());
}

// ---------------------------------------------------------------------------

const std::unordered_set<std::string>& FileDeck::rst_keep_in_solution()
{
    return ::rst_keep_in_solution;
}

void FileDeck::dump(std::ostream& os) const
{
    DeckOutput out(os, 10);
    for (const auto& block : this->blocks) {
        block.dump(out);
    }
}

void FileDeck::dump_inline() const
{
    this->dump(std::cout);
}

std::string
FileDeck::dump_block(const FileDeck::Block& block,
                     const std::string& output_dir,
                     const std::optional<std::string>& data_file,
                     FileDeck::DumpContext& context) const
{
    const auto& deck_name = block.fname;

    if (auto old_stream = context.get_stream(deck_name);
        old_stream != nullptr)
    {
        DeckOutput out(*old_stream, 10);
        block.dump(out);
        return "";
    }

    fs::path output_file;
    if (data_file.has_value()) {
        output_file = fs::path(output_dir) / data_file.value();
    }
    else {
        // Should ideally use fs::relative()
        auto rel_path = fs::proximate(block.fname, this->input_directory);
        output_file = output_dir / rel_path;
    }

    touch_file(output_file);
    output_file = fs::canonical(output_file);

    auto& stream = context.open_file(deck_name, output_file);
    DeckOutput out(stream, 10);
    block.dump(out);

    return output_file.generic_string();
}

void FileDeck::include_block(const std::string& input_file,
                             const std::string& output_file,
                             const std::string& output_dir,
                             FileDeck::DumpContext& context) const
{
    auto current_file = input_file;

    while (true) {
        const auto& parent = this->deck_tree.parent(current_file);

        auto* stream = context.get_stream(parent);
        if (stream != nullptr) {
            // Should ideally use fs::relative()
            INCLUDE(*stream, fs::proximate(output_file, output_dir).generic_string());

            break;
        }

        current_file = parent;
    }
}

void FileDeck::dump(const std::string& output_dir,
                    const std::string& fname,
                    const OutputMode mode) const
{
    if (!fs::is_directory(output_dir)) {
        fs::create_directories(output_dir);
    }

    const auto output_cwd = fs::path(output_dir);

    if (mode == OutputMode::INLINE) {
        std::ofstream os(output_cwd / fname);
        this->dump(os);
        return;
    }

    if (mode == OutputMode::COPY) {
        DumpContext context;
        this->dump_block(this->blocks[0], output_dir, fname, context);

        for (std::size_t block_index = 1; block_index < this->blocks.size(); ++block_index) {
            const auto& block = this->blocks[block_index];
            // For now, originally binary files will be written as GRDECL
            const auto& include_file = this->dump_block(block, output_dir, {}, context);

            if (block.fname != this->deck_tree.root()) {
                this->include_block(block.fname, include_file, output_dir, context);
            }
        }
    }

    if (mode == OutputMode::SHARE) {
       std::ofstream stream{output_cwd / fname};
       this->dump_shared(stream, output_dir);
    }
}

void FileDeck::dump_shared(std::ostream& stream,
                           const std::string& output_dir) const
{
    for (std::size_t block_index = 0; block_index < this->blocks.size(); ++block_index) {
        const auto& block = this->blocks[block_index];

        if ((block_index == 0) ||
            (this->modified_files.count(block.fname) > 0) ||
            this->deck_tree.has_include(block.fname))
        {
            DeckOutput out(stream, 10);
            block.dump( out );
        }
        else {
            // Should ideally use fs::relative()
            std::string include_file = fs::proximate(block.fname, output_dir).generic_string();
            const auto& fname = include_file.find(block.fname) == std::string::npos ? include_file : block.fname;
            block.is_binary ? IMPORT(stream, fname) : INCLUDE(stream, fname);
        }
    }
}

void FileDeck::dump_stdout(const std::string& output_dir,
                           const OutputMode mode) const
{
    if (mode == OutputMode::COPY) {
        throw std::logic_error("dump to stdout can not be combined outputmode COPY");
    }

    if (mode == OutputMode::INLINE) {
        this->dump_inline();
    }
    else if (mode == OutputMode::SHARE) {
        this->dump_shared(std::cout, output_dir);
    }
}

void FileDeck::rst_solution(const std::string& rst_base,
                            const int report_step)
{
    auto index = this->find("SOLUTION").value();
    auto summary_index = this->find("SUMMARY").value();

    ++index;

    while (true) {
        if (::rst_keep_in_solution.count((*this)[index].name()) == 0) {
            this->erase(index);
            --summary_index;
        }
        else {
            ++index;
        }

        if (index == summary_index) {
            break;
        }
    }

    {
        Opm::UnitSystem units{};
        std::vector<DeckValue> values{ DeckValue{rst_base}, DeckValue{report_step} };
        DeckKeyword restart {
            ParserKeywords::RESTART{},
            std::vector<std::vector<DeckValue>>{ values },
            units,
            units
        };

        auto solution = this->find("SOLUTION").value();
        this->insert(++solution, restart);
    }
}

void FileDeck::insert_skiprest()
{
    DeckKeyword skiprest( ParserKeywords::SKIPREST{} );

    const auto schedule = this->find("SCHEDULE");
    auto index = schedule.value();

    this->insert(++index, skiprest);
}

void FileDeck::skip(const int report_step)
{
    int current_report = 0;

    const auto& schedule = this->find("SCHEDULE");
    auto deck_pos = schedule.value();
    while (true) {
        const auto& deck_keyword = (*this)[deck_pos];

        if (deck_keyword.name() == "DATES") {
            current_report += deck_keyword.size();
        }
        else if (deck_keyword.name() == "TSTEP") {
            current_report += deck_keyword[0]
                .getItem<ParserKeywords::TSTEP::step_list>()
                .data_size();
        }

        if (current_report >= report_step) {
            break;
        }

        ++deck_pos;
        if (deck_pos == this->stop()) {
            throw std::logic_error {
                fmt::format("Could not find DATES keyword "
                            "corresponding to report_step {}", report_step)
            };
        }
    }

    auto index = schedule.value() + 1;
    auto end_pos = deck_pos;
    while (index < end_pos) {
        const auto& keyword = (*this)[index];

        if (rst_keep_in_schedule.count(keyword.name()) == 0) {
            auto next_index = index + 1;
            this->erase(index);

            if (next_index.file_index != index.file_index) {
                // Erased the last element on this block, move to the next.
                index = next_index;
            }

            if (index.file_index == end_pos.file_index) {
                // On the last block.
                --end_pos;
            }
        }
        else {
            index++;
        }
    }

    if (current_report == report_step) {
        this->erase(end_pos);
    }
    else {
        using D = ParserKeywords::DATES;

        auto deck_keyword = (*this)[end_pos];
        this->erase(end_pos);
        std::vector<std::vector<DeckValue>> records{};

        current_report -= deck_keyword.size();
        for (std::size_t record_index = report_step - current_report;
             record_index < deck_keyword.size(); ++record_index)
        {
            const auto& record = deck_keyword[record_index];
            records.push_back({DeckValue{record.getItem<D::DAY>().get<int>(0)},
                               DeckValue{record.getItem<D::MONTH>().get<std::string>(0)},
                               DeckValue{record.getItem<D::YEAR>().get<int>(0)}});
        }

        UnitSystem unit_system{};
        this->insert(end_pos, DeckKeyword{ParserKeywords::DATES{},
                    records, unit_system, unit_system});
    }
}

} // namespace Opm
