/**
 * Copyright (c) 2018, 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 "filter_sub_source.hh"

#include "base/attr_line.builder.hh"
#include "base/enum_util.hh"
#include "base/func_util.hh"
#include "base/opt_util.hh"
#include "bound_tags.hh"
#include "config.h"
#include "lnav.hh"
#include "readline_highlighters.hh"
#include "readline_possibilities.hh"
#include "sql_util.hh"

using namespace lnav::roles::literals;

filter_sub_source::
filter_sub_source(std::shared_ptr<readline_curses> editor)
    : fss_editor(editor)
{
    this->fss_editor->set_x(25);
    this->fss_editor->set_width(-1);
    this->fss_editor->set_save_history(!(lnav_data.ld_flags & LNF_SECURE_MODE));
    this->fss_regex_context.set_highlighter(readline_regex_highlighter)
        .set_append_character(0);
    this->fss_editor->add_context(filter_lang_t::REGEX,
                                  this->fss_regex_context);
    this->fss_sql_context.set_highlighter(readline_sqlite_highlighter)
        .set_append_character(0);
    this->fss_editor->add_context(filter_lang_t::SQL, this->fss_sql_context);
    this->fss_editor->set_change_action(
        bind_mem(&filter_sub_source::rl_change, this));
    this->fss_editor->set_perform_action(
        bind_mem(&filter_sub_source::rl_perform, this));
    this->fss_editor->set_abort_action(
        bind_mem(&filter_sub_source::rl_abort, this));
    this->fss_editor->set_display_match_action(
        bind_mem(&filter_sub_source::rl_display_matches, this));
    this->fss_editor->set_display_next_action(
        bind_mem(&filter_sub_source::rl_display_next, this));
    this->fss_match_view.set_sub_source(&this->fss_match_source);
    this->fss_match_view.set_height(0_vl);
    this->fss_match_view.set_show_scrollbar(true);
    this->fss_match_view.set_default_role(role_t::VCR_POPUP);
}

void
filter_sub_source::register_view(textview_curses* tc)
{
    text_sub_source::register_view(tc);
    tc->add_child_view(this->fss_editor.get());
    tc->add_child_view(&this->fss_match_view);
}

bool
filter_sub_source::list_input_handle_key(listview_curses& lv, int ch)
{
    if (this->fss_editing) {
        switch (ch) {
            case KEY_ESCAPE:
            case KEY_CTRL(']'):
                this->fss_editor->abort();
                return true;
            default:
                this->fss_editor->handle_key(ch);
                return true;
        }
    }

    switch (ch) {
        case 'f': {
            auto* top_view = *lnav_data.ld_view_stack.top();
            auto* tss = top_view->get_sub_source();

            tss->toggle_apply_filters();
            top_view->reload_data();
            break;
        }
        case ' ': {
            textview_curses* top_view = *lnav_data.ld_view_stack.top();
            text_sub_source* tss = top_view->get_sub_source();
            filter_stack& fs = tss->get_filters();

            if (fs.empty()) {
                return true;
            }

            auto tf = *(fs.begin() + lv.get_selection());

            fs.set_filter_enabled(tf, !tf->is_enabled());
            tss->text_filters_changed();
            lv.reload_data();
            top_view->reload_data();
            return true;
        }
        case 't': {
            textview_curses* top_view = *lnav_data.ld_view_stack.top();
            text_sub_source* tss = top_view->get_sub_source();
            filter_stack& fs = tss->get_filters();

            if (fs.empty()) {
                return true;
            }

            auto tf = *(fs.begin() + lv.get_selection());

            if (tf->get_type() == text_filter::INCLUDE) {
                tf->set_type(text_filter::EXCLUDE);
            } else {
                tf->set_type(text_filter::INCLUDE);
            }

            tss->text_filters_changed();
            lv.reload_data();
            top_view->reload_data();
            return true;
        }
        case 'D': {
            textview_curses* top_view = *lnav_data.ld_view_stack.top();
            text_sub_source* tss = top_view->get_sub_source();
            filter_stack& fs = tss->get_filters();

            if (fs.empty()) {
                return true;
            }

            auto tf = *(fs.begin() + lv.get_selection());

            fs.delete_filter(tf->get_id());
            lv.reload_data();
            tss->text_filters_changed();
            top_view->reload_data();
            return true;
        }
        case 'i': {
            auto* top_view = *lnav_data.ld_view_stack.top();
            auto* tss = top_view->get_sub_source();
            auto& fs = tss->get_filters();
            auto filter_index = fs.next_index();

            if (!filter_index) {
                lnav_data.ld_filter_help_status_source.fss_error.set_value(
                    "error: too many filters");
                return true;
            }

            auto ef = std::make_shared<empty_filter>(
                text_filter::type_t::INCLUDE, *filter_index);
            fs.add_filter(ef);
            lv.set_selection(vis_line_t(fs.size() - 1));
            lv.reload_data();

            this->fss_editing = true;
            this->tss_view->vc_enabled = false;

            add_view_text_possibilities(this->fss_editor.get(),
                                        filter_lang_t::REGEX,
                                        "*",
                                        top_view,
                                        text_quoting::regex);
            this->fss_editor->set_window(lv.get_window());
            this->fss_editor->set_visible(true);
            this->fss_editor->set_y(
                lv.get_y() + (int) (lv.get_selection() - lv.get_top()));
            this->fss_editor->window_change();
            this->fss_editor->focus(filter_lang_t::REGEX, "", "");
            this->fss_filter_state = true;
            ef->disable();
            return true;
        }
        case 'o': {
            auto* top_view = *lnav_data.ld_view_stack.top();
            auto* tss = top_view->get_sub_source();
            auto& fs = tss->get_filters();
            auto filter_index = fs.next_index();

            if (!filter_index) {
                lnav_data.ld_filter_help_status_source.fss_error.set_value(
                    "error: too many filters");
                return true;
            }

            auto ef = std::make_shared<empty_filter>(
                text_filter::type_t::EXCLUDE, *filter_index);
            fs.add_filter(ef);
            lv.set_selection(vis_line_t(fs.size() - 1));
            lv.reload_data();

            this->fss_editing = true;
            this->tss_view->vc_enabled = false;

            add_view_text_possibilities(this->fss_editor.get(),
                                        filter_lang_t::REGEX,
                                        "*",
                                        top_view,
                                        text_quoting::regex);
            this->fss_editor->set_window(lv.get_window());
            this->fss_editor->set_visible(true);
            this->fss_editor->set_y(
                lv.get_y() + (int) (lv.get_selection() - lv.get_top()));
            this->fss_editor->window_change();
            this->fss_editor->focus(filter_lang_t::REGEX, "", "");
            this->fss_filter_state = true;
            ef->disable();
            return true;
        }
        case '\r':
        case KEY_ENTER: {
            textview_curses* top_view = *lnav_data.ld_view_stack.top();
            text_sub_source* tss = top_view->get_sub_source();
            filter_stack& fs = tss->get_filters();

            if (fs.empty()) {
                return true;
            }

            auto tf = *(fs.begin() + lv.get_selection());

            this->fss_editing = true;
            this->tss_view->vc_enabled = false;

            auto tq = tf->get_lang() == filter_lang_t::SQL
                ? text_quoting::sql
                : text_quoting::regex;
            add_view_text_possibilities(
                this->fss_editor.get(), tf->get_lang(), "*", top_view, tq);
            if (top_view == &lnav_data.ld_views[LNV_LOG]) {
                add_filter_expr_possibilities(
                    this->fss_editor.get(), filter_lang_t::SQL, "*");
            }
            this->fss_editor->set_window(lv.get_window());
            this->fss_editor->set_visible(true);
            this->fss_editor->set_y(
                lv.get_y() + (int) (lv.get_selection() - lv.get_top()));
            this->fss_editor->focus(tf->get_lang(), "");
            this->fss_editor->rewrite_line(0, tf->get_id().c_str());
            this->fss_filter_state = tf->is_enabled();
            tf->disable();
            tss->text_filters_changed();
            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-filters");
            return true;
        }
        default:
            log_debug("unhandled %x", ch);
            break;
    }

    return false;
}

size_t
filter_sub_source::text_line_count()
{
    return (lnav_data.ld_view_stack.top() |
                [](auto tc) -> std::optional<size_t> {
               text_sub_source* tss = tc->get_sub_source();

               if (tss == nullptr) {
                   return std::nullopt;
               }
               auto& fs = tss->get_filters();
               return std::make_optional(fs.size());
           })
        .value_or(0);
}

size_t
filter_sub_source::text_line_width(textview_curses& curses)
{
    textview_curses* top_view = *lnav_data.ld_view_stack.top();
    text_sub_source* tss = top_view->get_sub_source();
    filter_stack& fs = tss->get_filters();
    size_t retval = 0;

    for (auto& filter : fs) {
        retval = std::max(filter->get_id().size() + 8, retval);
    }

    return retval;
}

void
filter_sub_source::text_value_for_line(textview_curses& tc,
                                       int line,
                                       std::string& value_out,
                                       text_sub_source::line_flags_t flags)
{
    auto* top_view = *lnav_data.ld_view_stack.top();
    auto* tss = top_view->get_sub_source();
    auto& fs = tss->get_filters();
    auto tf = *(fs.begin() + line);
    bool selected
        = lnav_data.ld_mode == ln_mode_t::FILTER && line == tc.get_selection();

    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(" ");
    }
    al.append(" ");
    if (tf->is_enabled()) {
        al.append("\u25c6"_ok);
    } else {
        al.append("\u25c7"_comment);
    }
    al.append(" ");
    switch (tf->get_type()) {
        case text_filter::INCLUDE:
            al.append(" ").append(lnav::roles::ok("IN")).append(" ");
            break;
        case text_filter::EXCLUDE:
            if (tf->get_lang() == filter_lang_t::REGEX) {
                al.append(lnav::roles::error("OUT")).append(" ");
            } else {
                al.append("    ");
            }
            break;
        default:
            ensure(0);
            break;
    }

    {
        auto ag = alb.with_attr(VC_ROLE.value(role_t::VCR_NUMBER));
        if (this->fss_editing && line == tc.get_selection()) {
            alb.appendf(FMT_STRING("{:>9}"), "-");
        } else {
            alb.appendf(FMT_STRING("{:>9}"),
                        tss->get_filtered_count_for(tf->get_index()));
        }
    }

    al.append(" hits ").append("|", VC_GRAPHIC.value(ACS_VLINE)).append(" ");

    attr_line_t content{tf->get_id()};
    switch (tf->get_lang()) {
        case filter_lang_t::REGEX:
            readline_regex_highlighter(content, std::nullopt);
            break;
        case filter_lang_t::SQL:
            readline_sqlite_highlighter(content, std::nullopt);
            break;
        case filter_lang_t::NONE:
            break;
    }
    al.append(content);

    if (selected) {
        al.with_attr_for_all(VC_ROLE.value(role_t::VCR_FOCUSED));
    }

    value_out = al.get_string();
}

void
filter_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
filter_sub_source::text_size_for_line(textview_curses& tc,
                                      int line,
                                      text_sub_source::line_flags_t raw)
{
    textview_curses* top_view = *lnav_data.ld_view_stack.top();
    text_sub_source* tss = top_view->get_sub_source();
    filter_stack& fs = tss->get_filters();
    auto tf = *(fs.begin() + line);

    return 8 + tf->get_id().size();
}

void
filter_sub_source::rl_change(readline_curses* rc)
{
    textview_curses* top_view = *lnav_data.ld_view_stack.top();
    text_sub_source* tss = top_view->get_sub_source();
    filter_stack& fs = tss->get_filters();
    if (fs.empty()) {
        return;
    }

    auto iter = fs.begin() + this->tss_view->get_selection();
    auto tf = *iter;
    auto new_value = rc->get_line_buffer();

    switch (tf->get_lang()) {
        case filter_lang_t::NONE:
            break;
        case filter_lang_t::REGEX: {
            if (new_value.empty()) {
                auto sugg = top_view->get_current_search();
                if (top_view->tc_selected_text) {
                    sugg = top_view->tc_selected_text->sti_value;
                }
                if (fs.get_filter(sugg) == nullptr) {
                    this->fss_editor->set_suggestion(sugg);
                }
            } else {
                auto regex_res
                    = lnav::pcre2pp::code::from(new_value, PCRE2_CASELESS);

                if (regex_res.isErr()) {
                    auto pe = regex_res.unwrapErr();
                    lnav_data.ld_filter_help_status_source.fss_error.set_value(
                        "error: %s", pe.get_message().c_str());
                } else {
                    auto& hm = top_view->get_highlights();
                    highlighter hl(regex_res.unwrap().to_shared());
                    auto role = tf->get_type() == text_filter::EXCLUDE
                        ? role_t::VCR_DIFF_DELETE
                        : role_t::VCR_DIFF_ADD;
                    hl.with_role(role);
                    hl.with_attrs(text_attrs{A_BLINK | A_REVERSE});

                    hm[{highlight_source_t::PREVIEW, "preview"}] = hl;
                    top_view->set_needs_update();
                    lnav_data.ld_filter_help_status_source.fss_error.clear();
                }
            }
            break;
        }
        case filter_lang_t::SQL: {
            auto full_sql
                = fmt::format(FMT_STRING("SELECT 1 WHERE {}"), new_value);
            auto_mem<sqlite3_stmt> stmt(sqlite3_finalize);
#ifdef SQLITE_PREPARE_PERSISTENT
            auto retcode = sqlite3_prepare_v3(lnav_data.ld_db.in(),
                                              full_sql.c_str(),
                                              full_sql.size(),
                                              SQLITE_PREPARE_PERSISTENT,
                                              stmt.out(),
                                              nullptr);
#else
            auto retcode = sqlite3_prepare_v2(lnav_data.ld_db.in(),
                                              full_sql.c_str(),
                                              full_sql.size(),
                                              stmt.out(),
                                              nullptr);
#endif
            if (retcode != SQLITE_OK) {
                lnav_data.ld_filter_help_status_source.fss_error.set_value(
                    "error: %s", sqlite3_errmsg(lnav_data.ld_db));
            } else {
                auto set_res = lnav_data.ld_log_source.set_preview_sql_filter(
                    stmt.release());

                if (set_res.isErr()) {
                    lnav_data.ld_filter_help_status_source.fss_error.set_value(
                        "error: %s",
                        set_res.unwrapErr()
                            .to_attr_line()
                            .get_string()
                            .c_str());
                } else {
                    top_view->set_needs_update();
                    lnav_data.ld_filter_help_status_source.fss_error.clear();
                }
            }
            break;
        }
    }
}

void
filter_sub_source::rl_perform(readline_curses* rc)
{
    static const intern_string_t INPUT_SRC = intern_string::lookup("input");

    textview_curses* top_view = *lnav_data.ld_view_stack.top();
    text_sub_source* tss = top_view->get_sub_source();
    filter_stack& fs = tss->get_filters();
    auto iter = fs.begin() + this->tss_view->get_selection();
    auto tf = *iter;
    auto new_value = rc->get_value().get_string();

    if (new_value.empty()) {
        this->rl_abort(rc);
    } else {
        top_view->get_highlights().erase(
            {highlight_source_t::PREVIEW, "preview"});
        switch (tf->get_lang()) {
            case filter_lang_t::NONE:
            case filter_lang_t::REGEX: {
                auto compile_res
                    = lnav::pcre2pp::code::from(new_value, PCRE2_CASELESS);

                if (compile_res.isErr()) {
                    auto ce = compile_res.unwrapErr();
                    auto um = lnav::console::to_user_message(INPUT_SRC, ce);
                    lnav_data.ld_exec_context.ec_error_callback_stack.back()(
                        um);
                    this->rl_abort(rc);
                } else {
                    tf->lf_deleted = true;
                    tss->text_filters_changed();

                    auto pf = std::make_shared<pcre_filter>(
                        tf->get_type(),
                        new_value,
                        tf->get_index(),
                        compile_res.unwrap().to_shared());

                    *iter = pf;
                    tss->text_filters_changed();
                }
                break;
            }
            case filter_lang_t::SQL: {
                auto full_sql
                    = fmt::format(FMT_STRING("SELECT 1 WHERE {}"), new_value);
                auto_mem<sqlite3_stmt> stmt(sqlite3_finalize);
#ifdef SQLITE_PREPARE_PERSISTENT
                auto retcode = sqlite3_prepare_v3(lnav_data.ld_db.in(),
                                                  full_sql.c_str(),
                                                  full_sql.size(),
                                                  SQLITE_PREPARE_PERSISTENT,
                                                  stmt.out(),
                                                  nullptr);
#else
                auto retcode = sqlite3_prepare_v2(lnav_data.ld_db.in(),
                                                  full_sql.c_str(),
                                                  full_sql.size(),
                                                  stmt.out(),
                                                  nullptr);
#endif
                if (retcode != SQLITE_OK) {
                    auto sqlerr = annotate_sql_with_error(
                        lnav_data.ld_db.in(), full_sql.c_str(), nullptr);
                    auto um
                        = lnav::console::user_message::error(
                              "invalid SQL expression")
                              .with_reason(sqlite3_errmsg(lnav_data.ld_db.in()))
                              .with_snippet(lnav::console::snippet::from(
                                  INPUT_SRC, sqlerr));
                    lnav_data.ld_exec_context.ec_error_callback_stack.back()(
                        um);
                    this->rl_abort(rc);
                } else {
                    lnav_data.ld_log_source.set_sql_filter(new_value,
                                                           stmt.release());
                    tss->text_filters_changed();
                }
                break;
            }
        }
    }

    lnav_data.ld_log_source.set_preview_sql_filter(nullptr);
    lnav_data.ld_filter_help_status_source.fss_prompt.clear();
    this->fss_editing = false;
    this->tss_view->vc_enabled = true;
    this->fss_editor->set_visible(false);
    top_view->reload_data();
    this->tss_view->reload_data();
}

void
filter_sub_source::rl_abort(readline_curses* rc)
{
    textview_curses* top_view = *lnav_data.ld_view_stack.top();
    text_sub_source* tss = top_view->get_sub_source();
    filter_stack& fs = tss->get_filters();
    auto iter = fs.begin() + this->tss_view->get_selection();
    auto tf = *iter;

    lnav_data.ld_log_source.set_preview_sql_filter(nullptr);
    lnav_data.ld_filter_help_status_source.fss_prompt.clear();
    lnav_data.ld_filter_help_status_source.fss_error.clear();
    top_view->get_highlights().erase({highlight_source_t::PREVIEW, "preview"});
    top_view->reload_data();
    fs.delete_filter("");
    this->tss_view->reload_data();
    this->fss_editor->set_visible(false);
    this->fss_editing = false;
    this->tss_view->vc_enabled = true;
    this->tss_view->set_needs_update();
    tf->set_enabled(this->fss_filter_state);
    tss->text_filters_changed();
    this->tss_view->reload_data();
}

void
filter_sub_source::rl_display_matches(readline_curses* rc)
{
    const std::vector<std::string>& matches = rc->get_matches();
    unsigned long width = 0;

    if (matches.empty()) {
        this->fss_match_source.clear();
        this->fss_match_view.set_height(0_vl);
        this->tss_view->set_needs_update();
    } else {
        auto current_match = rc->get_match_string();
        attr_line_t al;
        vis_line_t line, selected_line;

        for (const auto& match : matches) {
            if (match == current_match) {
                al.append(match, VC_STYLE.value(text_attrs{A_REVERSE}));
                selected_line = line;
            } else {
                al.append(match);
            }
            al.append(1, '\n');
            width = std::max(width, (unsigned long) match.size());
            line += 1_vl;
        }

        this->fss_match_view.set_selection(selected_line);
        this->fss_match_source.replace_with(al);
        this->fss_match_view.set_height(
            std::min(vis_line_t(matches.size()), 3_vl));
    }

    this->fss_match_view.set_window(this->tss_view->get_window());
    this->fss_match_view.set_y(rc->get_y() + 1);
    this->fss_match_view.set_x(rc->get_x() + rc->get_match_start());
    this->fss_match_view.set_width(width + 3);
    this->fss_match_view.set_needs_update();
    this->fss_match_view.reload_data();
}

void
filter_sub_source::rl_display_next(readline_curses* rc)
{
    textview_curses& tc = this->fss_match_view;

    if (tc.get_top() >= (tc.get_top_for_last_row() - 1)) {
        tc.set_top(0_vl);
    } else {
        tc.shift_top(tc.get_height());
    }
}

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

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