/**
 * Copyright (c) 2022, 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 <optional>
#include <unordered_map>

#include "lnav.indexing.hh"

#include "bound_tags.hh"
#include "lnav.events.hh"
#include "lnav.exec-phase.hh"
#include "lnav.hh"
#include "lnav_commands.hh"
#include "service_tags.hh"
#include "session_data.hh"
#include "sql_util.hh"
#include "yajlpp/yajlpp_def.hh"

using namespace std::chrono_literals;
using namespace lnav::roles::literals;

/**
 * Observer for loading progress that updates the bottom status bar.
 */
class loading_observer : public logfile_observer {
public:
    lnav::progress_result_t logfile_indexing(const logfile* lf,
                                             file_off_t off,
                                             file_ssize_t total) override
    {
        static sig_atomic_t index_counter = 0;

        if (lnav_data.ld_window == nullptr) {
            return lnav::progress_result_t::ok;
        }

        if (lnav_data.ld_sigint_count.load() > 0) {
            return lnav::progress_result_t::interrupt;
        }

        /* XXX require(off <= total); */
        if (off > (off_t) total) {
            off = total;
        }

        auto retval = lnav::progress_result_t::ok;
        if (((off == total) && (this->lo_last_offset != off))
            || ui_periodic_timer::singleton().time_to_update(index_counter))
        {
            if (off == total) {
                lnav_data.ld_bottom_source.update_loading(0, 0);
            } else {
                lnav_data.ld_bottom_source.update_loading(off, total);
            }
            lnav_data.ld_status[LNS_BOTTOM].set_needs_update();
            if (do_observer_update(lf) == lnav::progress_result_t::interrupt) {
                retval = lnav::progress_result_t::interrupt;
            }
            this->lo_last_offset = off;
        }

        if (!lnav_data.ld_looping) {
            retval = lnav::progress_result_t::interrupt;
        }
        return retval;
    }

    off_t lo_last_offset{0};
};

lnav::progress_result_t
do_observer_update(const logfile* lf)
{
    static auto& exec_phase = injector::get<lnav::exec_phase&>();

    if (lf != nullptr && lnav_data.ld_mode == ln_mode_t::FILES
        && exec_phase.spinning_up())
    {
        const auto& fc = lnav_data.ld_active_files;
        size_t index = 0;

        for (const auto& curr_file : fc.fc_files) {
            if (curr_file.get() != lf) {
                index++;
                continue;
            }
            lnav_data.ld_files_view.set_selection(
                vis_line_t(fc.fc_other_files.size() + index));
            lnav_data.ld_files_view.reload_data();
            lnav_data.ld_files_view.do_update();
        }
    }
    return lnav_data.ld_status_refresher(lnav::func::op_type::interactive);
}

void
rebuild_hist()
{
    auto& lss = lnav_data.ld_log_source;
    auto& hs = lnav_data.ld_hist_source2;
    const auto zoom = lnav_data.ld_zoom_level;

    hs.set_time_slice(ZOOM_LEVELS[zoom]);
    lss.reload_index_delegate();
}

class textfile_callback : public textfile_sub_source::scan_callback {
public:
    void closed_files(
        const std::vector<std::shared_ptr<logfile>>& files) override
    {
        for (const auto& lf : files) {
            log_info("closed text files: %s", lf->get_filename().c_str());
        }
        lnav_data.ld_active_files.close_files(files);
    }

    void promote_file(const std::shared_ptr<logfile>& lf) override
    {
        auto* vtab_manager = injector::get<log_vtab_manager*>();
        auto& ftf = lnav_data.ld_files_to_front;

        ftf.remove_if([&lf](const auto& elem) {
            return elem == lf->get_filename()
                || elem == lf->get_open_options().loo_filename;
        });
        if (lnav_data.ld_log_source.insert_file(lf)) {
            this->did_promotion = true;
            log_info("promoting text file to log file: %s (%s)",
                     lf->get_filename().c_str(),
                     lf->get_content_id().c_str());
            auto format = lf->get_format();
            if (format->lf_is_self_describing) {
                auto vt = format->get_vtab_impl();

                if (vt != nullptr) {
                    vtab_manager->register_vtab(vt);
                }
            }
            if (lf->get_open_options().loo_source == logfile_name_source::USER)
            {
                lf->set_include_in_session(true);
            }

            auto iter = session_data.sd_file_states.find(lf->get_filename());
            if (iter != session_data.sd_file_states.end()) {
                log_info("  found visibility state for log file: %d",
                         iter->second.fs_is_visible);

                lnav_data.ld_log_source.find_data(lf) | [&iter](auto ld) {
                    ld->set_visibility(iter->second.fs_is_visible);
                };
            }

            lnav::events::publish(lnav_data.ld_db.in(),
                                  lnav::events::file::format_detected{
                                      lf->get_filename(),
                                      lf->get_format_name().to_string(),
                                  });
        } else {
            this->closed_files({lf});
        }
    }

    void scanned_file(const std::shared_ptr<logfile>& lf) override
    {
        const auto& ftf = lnav_data.ld_files_to_front;

        if (!ftf.empty()
            && (ftf.front() == lf->get_filename()
                || ftf.front() == lf->get_open_options().loo_filename))
        {
            this->front_file = lf;

            lnav_data.ld_files_to_front.pop_front();
        }
    }

    void renamed_file(const std::shared_ptr<logfile>& lf) override
    {
        lnav_data.ld_active_files.regenerate_unique_file_names();
    }

    std::shared_ptr<logfile> front_file;
    bool did_promotion{false};
};

rebuild_indexes_result_t
rebuild_indexes(std::optional<ui_clock::time_point> deadline)
{
    static auto op = lnav_operation{"rebuild_indexes"};
    static auto& exec_phase = injector::get<lnav::exec_phase&>();
    thread_local size_t rdepth;

    if (rdepth > 0) {
        log_warning("skipping nested rebuild");
        return {0, false, true};
    }

    auto recurse_guard = lnav::recursion_preventer{&rdepth};
    auto op_guard = lnav_opid_guard::internal(op);

    auto& lss = lnav_data.ld_log_source;
    auto& log_view = lnav_data.ld_views[LNV_LOG];
    auto& text_view = lnav_data.ld_views[LNV_TEXT];
    bool scroll_downs[LNV__MAX];
    std::optional<text_time_translator::row_info> scroll_down_sels[LNV__MAX];
    rebuild_indexes_result_t retval;
    bool is_headless = lnav_data.ld_flags.is_set<lnav_flags::headless>();

    for (auto lpc : {LNV_LOG, LNV_TEXT}) {
        auto& view = lnav_data.ld_views[lpc];
        auto* ttt = dynamic_cast<text_time_translator*>(view.get_sub_source());
        if (ttt == nullptr) {
            continue;
        }
        auto sel_opt = view.get_selection();

        if (sel_opt.has_value()) {
            scroll_down_sels[lpc] = ttt->time_for_row(sel_opt.value());
        }
        if (view.is_selectable() && sel_opt.has_value()) {
            auto inner_height = view.get_inner_height();

            if (inner_height > 0_vl) {
                scroll_downs[lpc]
                    = (sel_opt == inner_height - 1_vl) && !is_headless;
            } else {
                scroll_downs[lpc] = !is_headless;
            }
        } else {
            scroll_downs[lpc] = (view.get_top() >= view.get_top_for_last_row())
                && !is_headless;
        }
    }

    // log_trace("rescanning text files");
    {
        auto* tss = &lnav_data.ld_text_source;
        textfile_callback cb;

        auto rescan_res = tss->rescan_files(cb, deadline);
        if (rescan_res.rr_new_data) {
            text_view.reload_data();
            retval.rir_changes += rescan_res.rr_new_data;
        }
        if (!rescan_res.rr_scan_completed) {
            retval.rir_completed = false;
        }

        if (cb.front_file != nullptr) {
            ensure_view(&text_view);

            if (tss->current_file() != cb.front_file) {
                tss->to_front(cb.front_file);
            }
        }
        if (cb.did_promotion) {
            lnav_data.ld_view_stack.set_needs_update();
        }
        if (cb.did_promotion && deadline) {
            // If there's a new log file, extend the deadline so it can be
            // indexed quickly.
            deadline = deadline.value() + 500ms;
        }
    }

    // log_trace("closing files");
    std::vector<std::shared_ptr<logfile>> closed_files;
    for (auto& lf : lnav_data.ld_active_files.fc_files) {
        const char* reason = nullptr;
        if (!lf->in_range()) {
            auto fn = lf->get_filename();
            const auto tr = lf->get_content_time_range();
            const auto& open_opts = lf->get_open_options();
            auto um = lnav::console::user_message::info(
                attr_line_t("file contents are out-of-range of time cutoffs ")
                    .append_quoted(lnav::roles::file(fn)));
            um.with_note(
                attr_line_t("File time range is ")
                    .append_quoted(lnav::to_rfc3339_string(tr.tr_begin))
                    .append(" to ")
                    .append_quoted(lnav::to_rfc3339_string(tr.tr_end)));
            if (open_opts.loo_time_range.has_lower_bound()) {
                um.with_note(attr_line_t("Minimum time cutoff is ")
                                 .append_quoted(lnav::to_rfc3339_string(
                                     open_opts.loo_time_range.tr_begin)));
            }
            if (open_opts.loo_time_range.has_upper_bound()) {
                um.with_note(attr_line_t("Maximum time cutoff is ")
                                 .append_quoted(lnav::to_rfc3339_string(
                                     open_opts.loo_time_range.tr_end)));
            }
            auto stub_map
                = lnav_data.ld_active_files.fc_name_to_stubs->writeAccess();
            stub_map->emplace(lf->get_path_for_key().string(),
                              file_stub_info{
                                  lf->get_filename_as_string(),
                                  lf->get_origin_mtime(),
                                  um,
                              });
            reason = "out-of-range";
        } else if (!lf->exists()) {
            reason = "deleted";
        } else if (lf->is_closed()) {
            reason = "closed";
        }
        if (reason != nullptr) {
            log_info(
                "%s file: %s (%s)",
                reason,
                lf->get_filename().c_str(),
                lf->get_actual_path().value_or(lf->get_filename()).c_str());
            lnav_data.ld_text_source.remove(lf);
            lnav_data.ld_log_source.remove_file(lf);
            closed_files.emplace_back(lf);
            retval.rir_rescan_needed = true;
        }
    }
    if (!closed_files.empty()) {
        lnav_data.ld_active_files.close_files(closed_files);
    }

    // log_trace("rebuilding logs indexes");
    auto result = lss.rebuild_index(deadline);
    if (result != logfile_sub_source::rebuild_result::rr_no_change) {
        size_t new_count = lss.text_line_count();
        auto force
            = result == logfile_sub_source::rebuild_result::rr_full_rebuild;

        if ((!scroll_downs[LNV_LOG]
             || log_view.get_top() > vis_line_t(new_count))
            && force)
        {
            log_debug("no log scroll down");
            scroll_downs[LNV_LOG] = false;
        }

        if (retval.rir_completed && !retval.rir_rescan_needed) {
            std::unordered_map<std::string,
                               std::vector<std::shared_ptr<logfile>>>
                id_to_files;
            auto reload = false;

            for (const auto& lf : lnav_data.ld_active_files.fc_files) {
                if (lf->get_format_ptr() == nullptr) {
                    continue;
                }
                id_to_files[lf->get_content_id()].push_back(lf);
            }

            for (auto& [name, lf] : id_to_files) {
                if (lf.size() == 1) {
                    continue;
                }

                std::sort(lf.begin(),
                          lf.end(),
                          [](const auto& left, const auto& right) {
                              const auto& lst = left->get_stat();
                              const auto& rst = right->get_stat();
                              return lst.st_size < rst.st_size
                                  || (lst.st_size == rst.st_size
                                      && rst.st_mtime < lst.st_mtime);
                          });

                const auto& dupe_name = lf.back()->get_unique_path();
                log_info(
                    "Keeping duplicated file: %s; size=%lld; mtime=%ld; "
                    "path=%s",
                    lf.back()->get_content_id().c_str(),
                    lf.back()->get_stat().st_size,
                    lf.back()->get_stat().st_mtime,
                    lf.back()->get_filename_as_string().c_str());
                lf.pop_back();
                std::for_each(
                    lf.begin(), lf.end(), [&dupe_name, &reload](auto& lf) {
                        if (lf->mark_as_duplicate(dupe_name)) {
                            log_info(
                                "  Hiding copy: size=%lld; mtime=%ld; path=%s",
                                lf->get_stat().st_size,
                                lf->get_stat().st_mtime,
                                lf->get_filename_as_string().c_str());
                            lnav_data.ld_log_source.find_data(lf) |
                                [](auto ld) { ld->set_visibility(false); };
                            reload = true;
                        }
                    });
            }

            if (reload) {
                log_trace(
                    "file visibility changed, calling text_filters_changed");
                lss.text_filters_changed();
            }
        }

        retval.rir_changes += 1;
    }

    if (retval.rir_changes > 0) {
        log_trace("updating top/selections");
        if (exec_phase.interactive()) {
            // XXX find a better place for this
            if (!lnav_data.ld_views[LNV_LOG].get_selection()) {
                lnav_data.ld_views[LNV_LOG].set_selection_to_last_row();
            }
        }
        for (auto lpc : {LNV_LOG, LNV_TEXT}) {
            auto& scroll_view = lnav_data.ld_views[lpc];
            auto* ttt = dynamic_cast<text_time_translator*>(
                scroll_view.get_sub_source());
            if (ttt == nullptr) {
                continue;
            }
            auto sel_opt = scroll_view.get_selection();
            std::optional<text_time_translator::row_info> curr_row_info;

            if (sel_opt.has_value()) {
                curr_row_info = ttt->time_for_row(sel_opt.value());
            }

            if (scroll_downs[lpc] && scroll_down_sels[lpc] == curr_row_info) {
                if (scroll_view.is_selectable() && sel_opt.has_value()) {
                    scroll_view.set_selection_to_last_row();
                } else if (scroll_view.get_top_for_last_row()
                           > scroll_view.get_top())
                {
                    scroll_view.set_top_for_last_row();
                }
            }
            log_debug("  scroll down[%d] = %d (sel=%d) (height=%d)",
                      lpc,
                      scroll_downs[lpc],
                      (int) scroll_view.get_selection().value_or(-1_vl),
                      (int) scroll_view.get_inner_height());
        }
    }

    lnav_data.ld_view_stack.top() | [&closed_files, &retval](auto tc) {
        if (!closed_files.empty() && tc == &lnav_data.ld_views[LNV_TIMELINE]) {
            auto* timeline_source
                = lnav_data.ld_views[LNV_TIMELINE].get_sub_source();
            if (timeline_source != nullptr) {
                timeline_source->text_filters_changed();
            }
        }

        auto* tss = tc->get_sub_source();
        if (lnav_data.ld_filter_status_source.update_filtered(tss)) {
            lnav_data.ld_status[LNS_FILTER].set_needs_update();
        }
        if (retval.rir_changes > 0) {
            lnav_data.ld_scroll_broadcaster(tc);
        }
    };

    if (retval.rir_changes > 0
        && !lnav_data.ld_flags.is_set<lnav_flags::headless>())
    {
        lnav_data.ld_files_view.reload_data();
    }
    // log_trace("done");

    return retval;
}

void
rebuild_indexes_repeatedly()
{
    thread_local size_t rdepth;

    if (rdepth > 0) {
        log_warning("skipping nested rebuild");
        return;
    }

    auto recurse_guard = lnav::recursion_preventer{&rdepth};
    for (size_t attempt = 0; attempt < 50; attempt++) {
        auto rebuild_res = rebuild_indexes();
        if (!rebuild_res.rir_completed) {
            log_info("rebuilding indexes did not finish, retrying...");
            continue;
        }
        if (rebuild_res.rir_rescan_needed) {
            log_info("rebuilding indexes needs a rescan...");
            rescan_files(false);
            continue;
        }
        if (rebuild_res.rir_changes == 0) {
            break;
        }
        log_info("continuing to rebuild indexes...");
    }
}

bool
update_active_files(file_collection& new_files)
{
    static loading_observer obs;

    if (lnav_data.ld_active_files.fc_invalidate_merge) {
        lnav_data.ld_active_files.fc_invalidate_merge = false;

        return true;
    }

    const auto was_below_open_file_limit
        = lnav_data.ld_active_files.is_below_open_file_limit();

    for (const auto& lf : new_files.fc_files) {
        lf->set_logfile_observer(&obs);
        lnav_data.ld_text_source.push_back(lf);
    }
    for (const auto& other_pair : new_files.fc_other_files) {
        switch (other_pair.second.ofd_format) {
            case file_format_t::SQLITE_DB:
                attach_sqlite_db(lnav_data.ld_db.in(), other_pair.first);
                break;
            default:
                break;
        }
    }
    lnav_data.ld_active_files.merge(new_files);
    lnav_data.ld_child_pollers.insert(
        lnav_data.ld_child_pollers.begin(),
        std::make_move_iterator(
            lnav_data.ld_active_files.fc_child_pollers.begin()),
        std::make_move_iterator(
            lnav_data.ld_active_files.fc_child_pollers.end()));
    lnav_data.ld_active_files.fc_child_pollers.clear();

    lnav::events::publish(
        lnav_data.ld_db.in(), new_files.fc_files, [](const auto& lf) {
            return lnav::events::file::open{
                lf->get_filename(),
            };
        });

    if (was_below_open_file_limit
        && !lnav_data.ld_active_files.is_below_open_file_limit()
        && !lnav_data.ld_exec_context.ec_msg_callback_stack.empty())
    {
        auto um
            = lnav::console::user_message::error("Unable to open more files")
                  .with_reason(
                      attr_line_t("The file-descriptor limit of ")
                          .append(lnav::roles::number(fmt::to_string(
                              file_collection::get_limits().l_fds)))
                          .append(" is too low to support opening more files"))
                  .with_help(
                      attr_line_t("Use ")
                          .append("ulimit -n"_quoted_code)
                          .append(" to increase the limit before running lnav"))
                  .move();

        lnav_data.ld_exec_context.ec_msg_callback_stack.back()(um);
    }

    return true;
}

bool
rescan_files(bool req)
{
    auto& mlooper = injector::get<main_looper&, services::main_t>();
    bool done = false;
    auto delay = 0ms;

    do {
        auto fc = lnav_data.ld_active_files.rescan_files(req);
        bool all_synced = true;

        update_active_files(fc);
        mlooper.get_port().process_for(delay);
        for (const auto& pair : lnav_data.ld_active_files.fc_other_files) {
            if (pair.second.ofd_format != file_format_t::REMOTE) {
                continue;
            }

            if (lnav_data.ld_active_files.fc_name_to_stubs->readAccess()->count(
                    pair.first))
            {
                continue;
            }

            if (lnav_data.ld_active_files.fc_synced_files.count(pair.first)
                == 0)
            {
                all_synced = false;
            }
        }
        if (!lnav_data.ld_active_files.fc_name_to_stubs->readAccess()->empty())
        {
            return false;
        }
        if (!all_synced) {
            delay = 30ms;
        }
        done = fc.fc_file_names.empty() && all_synced;
        if (!done && !lnav_data.ld_flags.is_set<lnav_flags::headless>()) {
            lnav_data.ld_files_view.set_needs_update();
            lnav_data.ld_files_view.do_update();
            lnav_data.ld_status_refresher(lnav::func::op_type::interactive);
        }
    } while (!done && lnav_data.ld_looping);
    return true;
}
