/**
 * Copyright (c) 2020, Timothy Stack
 *
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * * Redistributions of source code must retain the above copyright notice, this
 * list of conditions and the following disclaimer.
 * * Redistributions in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer in the documentation
 * and/or other materials provided with the distribution.
 * * Neither the name of Timothy Stack nor the names of its contributors
 * may be used to endorse or promote products derived from this software
 * without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

#include "files_sub_source.hh"

#include "base/ansi_scrubber.hh"
#include "base/attr_line.builder.hh"
#include "base/fs_util.hh"
#include "base/humanize.hh"
#include "base/humanize.network.hh"
#include "base/humanize.time.hh"
#include "base/itertools.enumerate.hh"
#include "base/itertools.hh"
#include "base/opt_util.hh"
#include "base/string_util.hh"
#include "config.h"
#include "lnav.hh"
#include "mapbox/variant.hpp"
#include "sql_util.hh"

using namespace lnav::roles::literals;

namespace files_model {
files_list_selection
from_selection(vis_line_t sel_vis)
{
    auto& fc = lnav_data.ld_active_files;
    int sel = sel_vis;

    {
        safe::ReadAccess<safe_name_to_errors> errs(*fc.fc_name_to_errors);

        if (sel < errs->size()) {
            auto iter = errs->begin();

            std::advance(iter, sel);
            return error_selection::build(
                sel, std::make_pair(iter->first, iter->second.fei_description));
        }

        sel -= errs->size();
    }

    if (sel < fc.fc_other_files.size()) {
        auto iter = fc.fc_other_files.begin();

        std::advance(iter, sel);
        return other_selection::build(sel, iter);
    }

    sel -= fc.fc_other_files.size();

    if (sel < fc.fc_files.size()) {
        auto iter = fc.fc_files.begin();

        std::advance(iter, sel);
        return file_selection::build(sel, iter);
    }

    return no_selection{};
}
}  // namespace files_model

files_sub_source::
files_sub_source()
{
}

bool
files_sub_source::list_input_handle_key(listview_curses& lv, int ch)
{
    switch (ch) {
        case KEY_ENTER:
        case '\r': {
            auto sel = files_model::from_selection(lv.get_selection());

            sel.match(
                [](files_model::no_selection) {},
                [](files_model::error_selection) {},
                [](files_model::other_selection) {},
                [&](files_model::file_selection& fs) {
                    auto& lss = lnav_data.ld_log_source;
                    auto lf = *fs.sb_iter;

                    lf->set_indexing(true);
                    lss.find_data(lf) | [](auto ld) {
                        ld->set_visibility(true);
                        lnav_data.ld_log_source.text_filters_changed();
                    };

                    if (lf->get_format() != nullptr) {
                        auto& log_view = lnav_data.ld_views[LNV_LOG];
                        lss.row_for_time(lf->front().get_timeval()) |
                            [](auto row) {
                                lnav_data.ld_views[LNV_LOG].set_selection(row);
                            };
                        ensure_view(&log_view);
                    } else {
                        auto& tv = lnav_data.ld_views[LNV_TEXT];
                        auto& tss = lnav_data.ld_text_source;

                        tss.to_front(lf);
                        tv.reload_data();
                        ensure_view(&tv);
                    }

                    lv.reload_data();
                    set_view_mode(ln_mode_t::PAGING);
                });

            return true;
        }

        case ' ': {
            auto sel = files_model::from_selection(lv.get_selection());

            sel.match([](files_model::no_selection) {},
                      [](files_model::error_selection) {},
                      [](files_model::other_selection) {},
                      [&](files_model::file_selection& fs) {
                          auto& lss = lnav_data.ld_log_source;
                          auto lf = *fs.sb_iter;

                          lss.find_data(lf) | [](auto ld) {
                              ld->get_file_ptr()->set_indexing(!ld->ld_visible);
                              ld->set_visibility(!ld->ld_visible);
                          };

                          auto top_view = *lnav_data.ld_view_stack.top();
                          auto tss = top_view->get_sub_source();

                          if (tss != nullptr) {
                              if (tss != &lss) {
                                  lss.text_filters_changed();
                                  lnav_data.ld_views[LNV_LOG].reload_data();
                              }
                              tss->text_filters_changed();
                              top_view->reload_data();
                          }

                          lv.reload_data();
                      });
            return true;
        }
        case 'n': {
            execute_command(lnav_data.ld_exec_context, "next-mark search");
            return true;
        }
        case 'N': {
            execute_command(lnav_data.ld_exec_context, "prev-mark search");
            return true;
        }
        case '/': {
            execute_command(lnav_data.ld_exec_context, "prompt search-files");
            return true;
        }
        case 'X': {
            auto sel = files_model::from_selection(lv.get_selection());

            sel.match(
                [](files_model::no_selection) {},
                [&](files_model::error_selection& es) {
                    auto& fc = lnav_data.ld_active_files;

                    fc.fc_file_names.erase(es.sb_iter.first);

                    auto name_iter = fc.fc_file_names.begin();
                    while (name_iter != fc.fc_file_names.end()) {
                        if (name_iter->first == es.sb_iter.first) {
                            name_iter = fc.fc_file_names.erase(name_iter);
                            continue;
                        }

                        auto rp_opt = humanize::network::path::from_str(
                            name_iter->first);

                        if (rp_opt) {
                            auto rp = *rp_opt;

                            if (fmt::to_string(rp.home()) == es.sb_iter.first) {
                                fc.fc_other_files.erase(name_iter->first);
                                name_iter = fc.fc_file_names.erase(name_iter);
                                continue;
                            }
                        }
                        ++name_iter;
                    }

                    fc.fc_name_to_errors->writeAccess()->erase(
                        es.sb_iter.first);
                    fc.fc_invalidate_merge = true;
                    lv.reload_data();
                },
                [](files_model::other_selection) {},
                [](files_model::file_selection) {});
            return true;
        }
    }
    return false;
}

void
files_sub_source::list_input_handle_scroll_out(listview_curses& lv)
{
    set_view_mode(ln_mode_t::PAGING);
    lnav_data.ld_filter_view.reload_data();
}

size_t
files_sub_source::text_line_count()
{
    const auto& fc = lnav_data.ld_active_files;
    auto retval = fc.fc_name_to_errors->readAccess()->size()
        + fc.fc_other_files.size() + fc.fc_files.size();

    return retval;
}

size_t
files_sub_source::text_line_width(textview_curses& curses)
{
    return 512;
}

void
files_sub_source::text_value_for_line(textview_curses& tc,
                                      int line,
                                      std::string& value_out,
                                      text_sub_source::line_flags_t flags)
{
    bool selected = line == tc.get_selection();
    role_t cursor_role = lnav_data.ld_mode == ln_mode_t::FILES
        ? role_t::VCR_CURSOR_LINE
        : role_t::VCR_DISABLED_CURSOR_LINE;
    const auto dim = tc.get_dimensions();
    const auto& fc = lnav_data.ld_active_files;
    auto filename_width
        = std::min(fc.fc_largest_path_length,
                   std::max((size_t) 40, (size_t) dim.second - 30));

    this->fss_curr_line.clear();
    auto& al = this->fss_curr_line;
    attr_line_builder alb(al);

    if (selected) {
        al.append(" ", VC_GRAPHIC.value(ACS_RARROW));
    } else {
        al.append(" ");
    }
    {
        safe::ReadAccess<safe_name_to_errors> errs(*fc.fc_name_to_errors);

        if (line < errs->size()) {
            auto iter = std::next(errs->begin(), line);
            auto path = std::filesystem::path(iter->first);
            auto fn = fmt::to_string(path.filename());

            truncate_to(fn, filename_width);
            al.append("   ");
            {
                auto ag = alb.with_attr(VC_ROLE.value(role_t::VCR_ERROR));

                al.appendf(FMT_STRING("{:<{}}"), fn, filename_width);
            }
            al.append("   ").append(iter->second.fei_description);
            if (selected) {
                al.with_attr_for_all(VC_ROLE.value(cursor_role));
            }

            value_out = al.get_string();
            return;
        }

        line -= errs->size();
    }

    if (line < fc.fc_other_files.size()) {
        auto iter = std::next(fc.fc_other_files.begin(), line);
        auto path = std::filesystem::path(iter->first);
        auto fn = fmt::to_string(path);

        truncate_to(fn, filename_width);
        al.append("   ");
        {
            auto ag = alb.with_attr(VC_ROLE.value(role_t::VCR_FILE));

            al.appendf(FMT_STRING("{:<{}}"), fn, filename_width);
        }
        al.append("   ")
            .appendf(FMT_STRING("{:14}"), iter->second.ofd_format)
            .append("  ")
            .append(iter->second.ofd_description);
        if (selected) {
            al.with_attr_for_all(VC_ROLE.value(cursor_role));
        }
        if (line == fc.fc_other_files.size() - 1) {
            al.with_attr_for_all(VC_STYLE.value(text_attrs{A_UNDERLINE}));
        }

        value_out = al.get_string();
        return;
    }

    line -= fc.fc_other_files.size();

    const auto& lf = fc.fc_files[line];
    auto ld_opt = lnav_data.ld_log_source.find_data(lf);
    auto fn = fmt::to_string(std::filesystem::path(lf->get_unique_path()));

    truncate_to(fn, filename_width);
    al.append(" ");
    if (ld_opt) {
        if (ld_opt.value()->ld_visible) {
            al.append("\u25c6"_ok);
        } else {
            al.append("\u25c7"_comment);
        }
    } else {
        al.append("\u25c6"_comment);
    }
    al.append(" ");
    al.appendf(FMT_STRING("{:<{}}"), fn, filename_width);
    al.append("   ");
    {
        auto ag = alb.with_attr(VC_ROLE.value(role_t::VCR_NUMBER));

        al.appendf(FMT_STRING("{:>8}"),
                   humanize::file_size(lf->get_index_size(),
                                       humanize::alignment::columnar));
    }
    if (selected) {
        al.with_attr_for_all(VC_ROLE.value(cursor_role));
    }

    value_out = al.get_string();
    this->fss_last_line_len = value_out.length();
}

void
files_sub_source::text_attrs_for_line(textview_curses& tc,
                                      int line,
                                      string_attrs_t& value_out)
{
    value_out = this->fss_curr_line.get_attrs();
}

size_t
files_sub_source::text_size_for_line(textview_curses& tc,
                                     int line,
                                     text_sub_source::line_flags_t raw)
{
    return 0;
}

static auto
spinner_index()
{
    auto now = ui_clock::now();

    return std::chrono::duration_cast<std::chrono::milliseconds>(
               now.time_since_epoch())
               .count()
        / 100;
}

bool
files_overlay_source::list_static_overlay(const listview_curses& lv,
                                          int y,
                                          int bottom,
                                          attr_line_t& value_out)
{
    if (y != 0) {
        return false;
    }
    static const char PROG[] = "-\\|/";
    constexpr size_t PROG_SIZE = sizeof(PROG) - 1;

    auto& fc = lnav_data.ld_active_files;
    auto fc_prog = fc.fc_progress;
    safe::WriteAccess<safe_scan_progress> sp(*fc_prog);

    if (!sp->sp_extractions.empty()) {
        const auto& prog = sp->sp_extractions.front();

        value_out.with_ansi_string(fmt::format(
            "{} Extracting " ANSI_COLOR(COLOR_CYAN) "{}" ANSI_NORM
                                                    "... {:>8}/{}",
            PROG[spinner_index() % PROG_SIZE],
            prog.ep_path.filename().string(),
            humanize::file_size(prog.ep_out_size, humanize::alignment::none),
            humanize::file_size(prog.ep_total_size,
                                humanize::alignment::none)));
        return true;
    }
    if (!sp->sp_tailers.empty()) {
        auto first_iter = sp->sp_tailers.begin();

        value_out.with_ansi_string(fmt::format(
            "{} Connecting to " ANSI_COLOR(COLOR_CYAN) "{}" ANSI_NORM ": {}",
            PROG[spinner_index() % PROG_SIZE],
            first_iter->first,
            first_iter->second.tp_message));
        return true;
    }

    return false;
}

bool
files_sub_source::text_handle_mouse(
    textview_curses& tc,
    const listview_curses::display_line_content_t&,
    mouse_event& me)
{
    if (me.is_click_in(mouse_button_t::BUTTON_LEFT, 1, 3)) {
        this->list_input_handle_key(tc, ' ');
    }
    if (me.is_double_click_in(mouse_button_t::BUTTON_LEFT, line_range{4, -1})) {
        this->list_input_handle_key(tc, '\r');
    }

    return false;
}

void
files_sub_source::text_selection_changed(textview_curses& tc)
{
    auto sel = files_model::from_selection(tc.get_selection());
    std::vector<attr_line_t> details;

    sel.match(
        [](files_model::no_selection) {},

        [&details](const files_model::error_selection& es) {
            auto path = std::filesystem::path(es.sb_iter.first);

            details.emplace_back(
                attr_line_t().appendf(FMT_STRING("Full path: {}"), path));
            details.emplace_back(attr_line_t("  ").append(
                lnav::roles::error(es.sb_iter.second)));
        },
        [&details](const files_model::other_selection& os) {
            auto path = std::filesystem::path(os.sb_iter->first);

            details.emplace_back(
                attr_line_t().appendf(FMT_STRING("Full path: {}"), path));
            details.emplace_back(attr_line_t("  ").append("Match Details"_h3));
            for (const auto& msg : os.sb_iter->second.ofd_details) {
                auto msg_al = msg.to_attr_line();

                for (auto& msg_line : msg_al.rtrim().split_lines()) {
                    msg_line.insert(0, 4, ' ');
                    details.emplace_back(msg_line);
                }
            }
        },
        [&details](const files_model::file_selection& fs) {
            static constexpr auto NAME_WIDTH = 17;

            auto lf = *fs.sb_iter;
            auto path = lf->get_filename();
            auto actual_path = lf->get_actual_path();
            auto format = lf->get_format();

            details.emplace_back(
                attr_line_t()
                    .appendf(FMT_STRING("{}"), path)
                    .with_attr_for_all(VC_ROLE.value(role_t::VCR_IDENTIFIER)));
            const auto notes = lf->get_notes();
            if (!notes.empty()) {
                details.emplace_back(
                    attr_line_t("  ").append("Notes"_h2).append(":"));
                for (const auto& [_kind, note_um] : notes) {
                    for (const auto& note_line :
                         note_um.to_attr_line().split_lines())
                    {
                        details.emplace_back(
                            attr_line_t("    ").append(note_line));
                    }
                }
            }
            details.emplace_back(attr_line_t("  ").append("General"_h2));
            if (actual_path) {
                if (path != actual_path.value()) {
                    auto line = attr_line_t()
                                    .append("Actual Path"_h3)
                                    .right_justify(NAME_WIDTH)
                                    .append(": ")
                                    .append(lnav::roles::file(
                                        fmt::to_string(actual_path.value())))
                                    .move();
                    details.emplace_back(line);
                }
            } else {
                details.emplace_back(attr_line_t().append("  Piped"));
            }
            details.emplace_back(
                attr_line_t()
                    .append("MIME Type"_h3)
                    .right_justify(NAME_WIDTH)
                    .append(": ")
                    .append(fmt::to_string(lf->get_text_format())));
            details.emplace_back(
                attr_line_t()
                    .append("Last Modified"_h3)
                    .right_justify(NAME_WIDTH)
                    .append(": ")
                    .append(lnav::to_rfc3339_string(
                        convert_log_time_to_local(lf->get_modified_time()),
                        0,
                        'T')));
            details.emplace_back(
                attr_line_t()
                    .append("Size"_h3)
                    .right_justify(NAME_WIDTH)
                    .append(": ")
                    .append(humanize::file_size(lf->get_index_size(),
                                                humanize::alignment::none)));

            details.emplace_back(attr_line_t()
                                     .append("Lines"_h3)
                                     .right_justify(NAME_WIDTH)
                                     .append(": ")
                                     .append(lnav::roles::number(fmt::format(
                                         FMT_STRING("{:L}"), lf->size()))));
            if (format != nullptr && lf->size() > 0) {
                details.emplace_back(attr_line_t()
                                         .append("Time Range"_h3)
                                         .right_justify(NAME_WIDTH)
                                         .append(": ")
                                         .append(lnav::to_rfc3339_string(
                                             lf->front().get_timeval(), 'T'))
                                         .append(" - ")
                                         .append(lnav::to_rfc3339_string(
                                             lf->back().get_timeval(), 'T')));
                details.emplace_back(
                    attr_line_t()
                        .append("Duration"_h3)
                        .right_justify(NAME_WIDTH)
                        .append(": ")
                        .append(humanize::time::duration::from_tv(
                                    lf->back().get_timeval()
                                    - lf->front().get_timeval())
                                    .to_string()));
            }
            {
                auto line = attr_line_t("  ")
                                .append("Log Format"_h2)
                                .append(": ")
                                .move();

                if (format != nullptr) {
                    line.append(lnav::roles::identifier(
                        format->get_name().to_string()));
                } else {
                    line.append("(none)"_comment);
                }
                details.emplace_back(line);
            }

            if (lf->get_format_ptr() != nullptr) {
                const auto um = lnav::console::user_message::info(
                    attr_line_t("The file contents matched this log format and "
                                "will be shown in the LOG view"));
                um.to_attr_line().rtrim().split_lines()
                    | lnav::itertools::for_each([&details](const auto& al) {
                          details.emplace_back(attr_line_t("    ").append(al));
                      });
            } else {
                const auto um = lnav::console::user_message::info(attr_line_t(
                    "The file contents did not match any log "
                    "formats and can be accessed in the TEXT view"));
                um.to_attr_line().rtrim().split_lines()
                    | lnav::itertools::for_each([&details](const auto& al) {
                          details.emplace_back(attr_line_t("    ").append(al));
                      });
            }

            const auto& match_msgs = lf->get_format_match_messages();
            details.emplace_back(
                attr_line_t("    ").append("Match Details"_h3));
            for (const auto& msg : match_msgs) {
                auto msg_al = msg.to_attr_line();

                for (auto& msg_line : msg_al.rtrim().split_lines()) {
                    msg_line.insert(0, 6, ' ');
                    details.emplace_back(msg_line);
                }
            }

            {
                const auto& meta = lf->get_embedded_metadata();

                if (!meta.empty()) {
                    details.emplace_back(
                        attr_line_t("  ").append("Embedded Metadata:"_h2));
                    for (const auto& [index, meta_pair] :
                         lnav::itertools::enumerate(meta))
                    {
                        details.emplace_back(
                            attr_line_t("  ")
                                .appendf(FMT_STRING("[{}]"), index)
                                .append(" ")
                                .append(lnav::roles::h3(meta_pair.first)));
                        details.emplace_back(
                            attr_line_t("      MIME Type: ")
                                .append(lnav::roles::symbol(fmt::to_string(
                                    meta_pair.second.m_format))));
                        line_range lr{6, -1};
                        details.emplace_back(attr_line_t().with_attr(
                            string_attr{lr, VC_GRAPHIC.value(ACS_HLINE)}));
                        const auto val_sf = string_fragment::from_str(
                            meta_pair.second.m_value);
                        for (const auto val_line : val_sf.split_lines()) {
                            details.emplace_back(
                                attr_line_t("      ").append(val_line));
                        }
                    }
                }
            }
        });

    this->fss_details_source->replace_with(details);
}
