/**
 * 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 "config.h"

#include "lnav.hh"

#include "filter_sub_source.hh"
#include "readline_possibilities.hh"
#include "readline_highlighters.hh"

using namespace std;

filter_sub_source::filter_sub_source()
    : fss_change_wire(*this, &filter_sub_source::rl_change),
      fss_perform_wire(*this, &filter_sub_source::rl_perform),
      fss_abort_wire(*this, &filter_sub_source::rl_abort),
      fss_display_match_wire(*this, &filter_sub_source::rl_display_matches),
      fss_display_next_wire(*this, &filter_sub_source::rl_display_next)
{
    this->filter_context.set_highlighter(readline_regex_highlighter)
        .set_append_character(0);
    this->fss_editor.add_context(LNM_FILTER, this->filter_context);
    this->fss_editor.set_change_action(&this->fss_change_wire);
    this->fss_editor.set_perform_action(&this->fss_perform_wire);
    this->fss_editor.set_abort_action(&this->fss_abort_wire);
    this->fss_editor.set_display_match_action(&this->fss_display_match_wire);
    this->fss_editor.set_display_next_action(&this->fss_display_next_wire);
    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(view_colors::VCR_POPUP);
}

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

    switch (ch) {
        case '\t':
        case KEY_BTAB:
        case 'q':
            lnav_data.ld_mode = LNM_PAGING;
            lnav_data.ld_filter_view.reload_data();
            return true;
        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;
            }

            shared_ptr<text_filter> tf = *(fs.begin() + lv.get_selection());

            fs.set_filter_enabled(tf, !tf->is_enabled());
            tss->text_filters_changed();
            lv.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;
            }

            shared_ptr<text_filter> 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();
            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;
            }

            shared_ptr<text_filter> tf = *(fs.begin() + lv.get_selection());

            fs.delete_filter(tf->get_id());
            tss->text_filters_changed();
            lv.reload_data();
            return true;
        }
        case 'i': {
            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.full()) {
                return true;
            }

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

            this->fss_editing = true;

            add_view_text_possibilities(&this->fss_editor, LNM_FILTER, "*", top_view);
            lnav_data.ld_filter_status_source.tss_prompt.set_value(
                "Enter a regular expression to match lines to filter in:");
            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.set_left(22);
            this->fss_editor.set_width(this->tss_view->get_width() - 8 - 1);
            this->fss_editor.focus(LNM_FILTER, "", "");
            this->fss_filter_state = true;
            ef->disable();
            return true;
        }
        case 'o': {
            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.full()) {
                return true;
            }

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

            this->fss_editing = true;

            add_view_text_possibilities(&this->fss_editor, LNM_FILTER, "*", top_view);
            lnav_data.ld_filter_status_source.tss_prompt.set_value(
                "Enter a regular expression to match lines to filter out:");
            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.set_left(22);
            this->fss_editor.set_width(this->tss_view->get_width() - 8 - 1);
            this->fss_editor.focus(LNM_FILTER, "", "");
            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;
            }

            shared_ptr<text_filter> tf = *(fs.begin() + lv.get_selection());

            this->fss_editing = true;

            add_view_text_possibilities(&this->fss_editor, LNM_FILTER, "*", top_view);
            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.set_left(22);
            this->fss_editor.set_width(this->tss_view->get_width() - 8 - 1);
            this->fss_editor.focus(LNM_FILTER, "", tf->get_id().c_str());
            this->fss_filter_state = tf->is_enabled();
            tf->disable();
            tss->text_filters_changed();
            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) {
        text_sub_source *tss = tc->get_sub_source();
        filter_stack &fs = tss->get_filters();

        return nonstd::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)
{
    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();
    shared_ptr<text_filter> tf = *(fs.begin() + line);
    char hits[32];

    value_out = "    ";
    switch (tf->get_type()) {
        case text_filter::INCLUDE:
            value_out.append(" IN ");
            break;
        case text_filter::EXCLUDE:
            value_out.append("OUT ");
            break;
        default:
            ensure(0);
            break;
    }

    if (this->fss_editing && line == tc.get_selection()) {
        snprintf(hits, sizeof(hits), "%6s hits | ", "-");
    } else {
        snprintf(hits, sizeof(hits), "%6d hits | ", tss->get_filtered_count_for(tf->get_index()));
    }
    value_out.append(hits);

    value_out.append(tf->get_id());
}

void filter_sub_source::text_attrs_for_line(textview_curses &tc, int line,
                                            string_attrs_t &value_out)
{
    auto &vcolors = view_colors::singleton();
    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();
    shared_ptr<text_filter> tf = *(fs.begin() + line);
    bool selected = lnav_data.ld_mode == LNM_FILTER && line == tc.get_selection();
    int bg = selected ? COLOR_WHITE : COLOR_BLACK;

    if (selected) {
        value_out.emplace_back(line_range{0, 1}, &view_curses::VC_GRAPHIC, ACS_RARROW);
    }

    chtype enabled = tf->is_enabled() ? ACS_DIAMOND : ' ';

    line_range lr{2, 3};
    value_out.emplace_back(lr, &view_curses::VC_GRAPHIC, enabled);
    if (tf->is_enabled()) {
        value_out.emplace_back(lr, &view_curses::VC_FOREGROUND,
                               vcolors.ansi_to_theme_color(COLOR_GREEN));
    }

    int fg = tf->get_type() == text_filter::INCLUDE ? COLOR_GREEN : COLOR_RED;
    value_out.emplace_back(line_range{4, 7}, &view_curses::VC_FOREGROUND,
                           vcolors.ansi_to_theme_color(fg));
    value_out.emplace_back(line_range{4, 7}, &view_curses::VC_STYLE, A_BOLD);

    value_out.emplace_back(line_range{8, 14}, &view_curses::VC_STYLE, A_BOLD);
    value_out.emplace_back(line_range{20, 21}, &view_curses::VC_GRAPHIC, ACS_VLINE);

    fg = selected ? COLOR_BLACK : COLOR_WHITE;
    value_out.emplace_back(line_range{0, -1}, &view_curses::VC_FOREGROUND,
                           vcolors.ansi_to_theme_color(fg));
    value_out.emplace_back(line_range{0, -1}, &view_curses::VC_BACKGROUND,
                           vcolors.ansi_to_theme_color(bg));
}

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();
    shared_ptr<text_filter> 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();
    shared_ptr<text_filter> tf = *iter;
    string new_value = rc->get_line_buffer();
    auto_mem<pcre> code;
    const char *errptr;
    int eoff;

    if ((code = pcre_compile(new_value.c_str(),
                             PCRE_CASELESS,
                             &errptr,
                             &eoff,
                             nullptr)) == nullptr) {
        lnav_data.ld_filter_status_source.tss_error
                 .set_value("error: %s", errptr);
    } else {
        textview_curses::highlight_map_t &hm = top_view->get_highlights();
        highlighter hl(code.release());
        int color;

        if (tf->get_type() == text_filter::EXCLUDE) {
            color = COLOR_RED;
        } else {
            color = COLOR_GREEN;
        }
        hl.with_attrs(
            view_colors::ansi_color_pair(COLOR_BLACK, color) | A_BLINK);

        hm[{highlight_source_t::PREVIEW, "preview"}] = hl;
        top_view->set_needs_update();
        lnav_data.ld_filter_status_source.tss_error.clear();
    }
}

void filter_sub_source::rl_perform(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();
    shared_ptr<text_filter> tf = *iter;
    string new_value = rc->get_value();
    auto_mem<pcre> code;
    const char *errptr;
    int eoff;

    if (new_value.empty()) {
        this->rl_abort(rc);
    } else if ((code = pcre_compile(new_value.c_str(),
                                    PCRE_CASELESS,
                                    &errptr,
                                    &eoff,
                                    nullptr)) == nullptr) {
        this->rl_abort(rc);
    } else {
        tf->lf_deleted = true;
        tss->text_filters_changed();

        auto pf = make_shared<pcre_filter>(tf->get_type(),
                                           new_value, tf->get_index(), code.release());

        *iter = pf;
        tss->text_filters_changed();
    }

    lnav_data.ld_filter_status_source.tss_prompt.clear();
    this->fss_editing = false;
    this->fss_editor.set_visible(false);
    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();
    shared_ptr<text_filter> tf = *iter;

    lnav_data.ld_filter_status_source.tss_prompt.clear();
    lnav_data.ld_filter_status_source.tss_error.clear();
    top_view->get_highlights().erase({highlight_source_t::PREVIEW, "preview"});
    top_view->reload_data();
    fs.delete_filter("");
    this->fss_editor.set_visible(false);
    this->fss_editing = false;
    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 {
        string current_match = rc->get_match_string();
        attr_line_t al;
        vis_line_t line, selected_line;

        for (auto &match : matches) {
            if (match == current_match) {
                al.append(match, &view_curses::VC_STYLE, 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_left() + rc->get_match_start());
    this->fss_match_view.set_width(width + 3);
    this->fss_match_view.set_needs_update();
    this->fss_match_view.scroll_selection_into_view();
    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)
{
    lnav_data.ld_mode = LNM_PAGING;
    lnav_data.ld_filter_view.reload_data();
}
