/*
Copyright (c) 2016 Microsoft Corporation. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.

Authors: Gabriel Ebner, Leonardo de Moura, Sebastian Ullrich
*/
#if defined(LEAN_JSON)
// Remark: gcc 7 produces a warning at json.hpp
// We believe it is a spurious warning
#if defined(__GNUC__) && !defined(__CLANG__)
#pragma GCC diagnostic ignored "-Wuninitialized"
#endif

#include <list>
#include <string>
#include <vector>
#include <algorithm>
#include <clocale>
#include "frontends/lean/module_parser.h"
#include "library/library_task_builder.h"
#include "util/lean_path.h"
#include "util/sexpr/option_declarations.h"
#include "util/timer.h"
#include "library/mt_task_queue.h"
#include "library/st_task_queue.h"
#include "library/attribute_manager.h"
#include "library/tactic/tactic_state.h"
#include "frontends/lean/parser.h"
#include "frontends/lean/info_manager.h"
#include "frontends/lean/interactive.h"
#include "frontends/lean/completion.h"
#include "shell/server.h"

namespace lean {
struct all_messages_msg {
    std::vector<message> m_msgs;

    json to_json_response() const {
        auto msgs = json::array();
        for (auto & msg : m_msgs)
            msgs.push_back(json_of_message(msg));

        json j;
        j["response"] = "all_messages";
        j["msgs"] = msgs;
        return j;
    }
};

region_of_interest::intersection_result region_of_interest::intersects(location const & loc) const {
    if (loc.m_file_name.empty()) return InROI;
    if (!m_open_files || !m_open_files->count(loc.m_file_name)) return NoIntersection;
    auto & visible_lines = m_open_files->at(loc.m_file_name);
    bool above_roi = false;
    for (auto & lr : visible_lines) {
        if (std::max(lr.m_begin_line, loc.m_range.m_begin.first)
            <= std::min(lr.m_end_line, loc.m_range.m_end.first)) {
            return InROI;
        }
        if (loc.m_range.m_begin.first <= lr.m_end_line) above_roi = true;
    }
    if (above_roi) return AboveROI;
    return visible_lines.empty() ? OpenFile : VisibleFile;
}

bool region_of_interest::should_report(location const & loc) const {
    auto isect = intersects(loc);
    switch (m_check_mode) {
        case Nothing: return false;
        case VisibleLines: return isect >= InROI;
        case VisibleLinesAndAbove: case VisibleFiles:
            return isect >= VisibleFile;
        case OpenFiles: return isect >= OpenFile;
        default: return true;
    }
}

optional<unsigned> region_of_interest::get_priority(log_tree::node const & n) const {
    auto isect = intersects(n.get_location());
    optional<unsigned> yes(isect >= InROI ? n.get_detail_level() : n.get_detail_level() + log_tree::MaxLevel);
    optional<unsigned> no;

    switch (m_check_mode) {
        case Nothing: return no;
        case VisibleLines: return (isect >= InROI && n.get_detail_level() < log_tree::CrossModuleLintLevel) ? yes : no;
        case VisibleLinesAndAbove: return isect >= AboveROI ? yes : no;
        case VisibleFiles: return isect >= VisibleFile ? yes : no;
        case OpenFiles: return isect >= OpenFile ? yes : no;
        default: return yes;
    }
}

class server::message_handler {
    server * m_srv;
    log_tree * m_lt;

    mutex m_mutex;
    std::unordered_set<std::string> m_dirty_files;
    bool m_full_refresh_scheduled = false;
    std::unique_ptr<single_timer> m_timer;

public:
    message_handler(server * srv, log_tree * lt, bool use_timer) : m_srv(srv), m_lt(lt) {
        if (use_timer) m_timer.reset(new single_timer);
    }

    std::vector<message> get_messages_core(region_of_interest const & roi) {
        std::vector<message> msgs;
        m_lt->for_each([&] (log_tree::node const & n) {
            if (roi.should_report(n.get_location())) {
                for (auto & e : n.get_entries()) {
                    if (auto msg = dynamic_cast<message const *>(e.get())) {
                        if (roi.should_report(msg->get_location()))
                            msgs.push_back(*msg);
                    }
                }
                return true;
            } else {
                return false;
            }
        });
        return msgs;
    }

    void schedule_refresh() {
#if defined(LEAN_MULTI_THREAD)
        if (m_timer) {
            m_full_refresh_scheduled = true;
            m_timer->set(chrono::steady_clock::now() + chrono::milliseconds(100), [&] {
                    unique_lock<mutex> lock(m_mutex);
                    m_full_refresh_scheduled = false;
                    m_dirty_files.clear();
                    auto roi = m_srv->get_roi();
                    lock.unlock();

                    m_srv->send_msg(all_messages_msg{get_messages_core(roi)});
                }, false);
        }
#endif
        if (!m_full_refresh_scheduled) {
            m_dirty_files.clear();
            m_srv->send_msg(all_messages_msg{get_messages_core(m_srv->get_roi())});
        }
    }

    void on_event(std::vector<log_tree::event> const & events) {
        unique_lock<mutex> lock(m_mutex);
        auto roi = m_srv->get_roi();
        for (auto & e : events) {
            switch (e.m_kind) {
                case log_tree::event::EntryAdded:
                case log_tree::event::EntryRemoved:
                    if (auto msg = dynamic_cast<message const *>(e.m_entry.get())) {
                        if (roi.should_report(msg->get_location())) {
                            m_dirty_files.insert(msg->get_file_name());
                        }
                    }
                    break;

                default: break;
            }
        }
        if (!m_dirty_files.empty()) {
            schedule_refresh();
        }
    }

    void on_new_roi() {
        unique_lock<mutex> lock(m_mutex);
        schedule_refresh();
    }
};

struct current_tasks_msg {
    std::vector<json> m_tasks;
    optional<json> m_cur_task;
    bool m_is_running = false;

    json to_json_response() const {
        json j;
        j["response"] = "current_tasks";
        j["is_running"] = m_is_running;
        if (m_cur_task) j["cur_task"] = *m_cur_task;
        j["tasks"] = m_tasks;
        return j;
    }

    static json json_of_task(log_tree::node const & t) {
        json j;
        j["file_name"] = t.get_location().m_file_name;
        auto pos = t.get_location().m_range.m_begin;
        j["pos_line"] = pos.first;
        j["pos_col"] = pos.second;
        auto end_pos = t.get_location().m_range.m_end;
        j["end_pos_line"] = end_pos.first;
        j["end_pos_col"] = end_pos.second;
        j["desc"] = t.get_description();
        return j;
    }
};

class server::tasks_handler {
    server * m_srv;
    log_tree * m_lt;

    mutex m_mutex;
    std::unique_ptr<single_timer> m_timer;

public:
    tasks_handler(server * srv, log_tree * lt, bool use_timer) : m_srv(srv), m_lt(lt) {
        if (use_timer) m_timer.reset(new single_timer);
    }

    void submit_core(unsigned prio, log_tree::node const & n) {
        if (auto prod = n.get_producer()) {
            taskq().submit(prod, prio);
        }
    }

    void resubmit_core() {
        auto roi = m_srv->get_roi();
        m_srv->m_lt.for_each([&] (log_tree::node const & n) {
            if (auto prio = roi.get_priority(n)) {
                submit_core(*prio, n);
                return true;
            } else {
                return false;
            }
        });
    }

    current_tasks_msg mk_tasks_msg() {
        current_tasks_msg msg;
        auto roi = m_srv->get_roi();
        m_lt->for_each([&] (log_tree::node const & n) {
            if (roi.get_priority(n)) {
                if (n.get_producer()) {
                    msg.m_is_running = true;
                    msg.m_tasks.push_back(current_tasks_msg::json_of_task(n));
                    return false;
                } else {
                    return true;
                }
            } else {
                return false;
            }
        });
        return msg;
    }

    void schedule_refresh() {
#if defined(LEAN_MULTI_THREAD)
        if (m_timer) {
            m_timer->set(chrono::steady_clock::now() + chrono::milliseconds(200), [&] {
                m_srv->send_msg(mk_tasks_msg());
            }, false);
        }
#endif
    }

    void on_event(std::vector<log_tree::event> const & events) {
        optional<region_of_interest> roi;
        bool need_refresh = false;
        for (auto & e : events) {
            switch (e.m_kind) {
                case log_tree::event::ProducerSet:
                    if (!roi) roi = m_srv->get_roi();
                    if (auto prio = roi->get_priority(e.m_node)) {
                        submit_core(*prio, e.m_node);
                        need_refresh = true;
                    }
                    break;
                case log_tree::event::StateChanged:
                    if (!roi) roi = m_srv->get_roi();
                    if (roi->get_priority(e.m_node))
                        need_refresh = true;
                    break;

                default:
                    break;
            }
        }
        if (need_refresh) {
            unique_lock<mutex> lock(m_mutex);
            schedule_refresh();
        }
    }

    void on_new_roi() {
        resubmit_core();
        unique_lock<mutex> lock(m_mutex);
        schedule_refresh();
    }
};

server::server(unsigned num_threads, search_path const & path, environment const & initial_env, io_state const & ios) :
        m_path(path), m_initial_env(initial_env), m_ios(ios) {
    m_ios.set_regular_channel(std::make_shared<stderr_channel>());
    m_ios.set_diagnostic_channel(std::make_shared<stderr_channel>());

    m_msg_handler.reset(new message_handler(this, &m_lt, num_threads > 0));
    m_tasks_handler.reset(new tasks_handler(this, &m_lt, num_threads > 0));

    m_lt.add_listener([&] (std::vector<log_tree::event> const & evs) {
        m_msg_handler->on_event(evs);
        m_tasks_handler->on_event(evs);
    });

    scope_global_ios scoped_ios(m_ios);
#if defined(LEAN_MULTI_THREAD)
    if (num_threads == 0) {
        m_tq.reset(new st_task_queue);
    } else {
        m_tq.reset(new mt_task_queue(num_threads));
    }
#else
    m_tq.reset(new st_task_queue());
#endif

    set_task_queue(m_tq.get());
    m_mod_mgr.reset(new module_mgr(this, m_lt.get_root(), m_path, m_initial_env, m_ios));
    m_mod_mgr->set_server_mode(true);
    m_mod_mgr->set_save_olean(false);
}

server::~server() {
    m_mod_mgr->cancel_all();
    cancel(m_bg_task_ctok);
    m_tq->evacuate();
}

struct server::cmd_req {
    unsigned m_seq_num = static_cast<unsigned>(-1);
    std::string m_cmd_name;
    json m_payload;
};

struct server::cmd_res {
    cmd_res() {}
    cmd_res(unsigned seq_num, json const & payload) : m_seq_num(seq_num), m_payload(payload) {}
    cmd_res(unsigned seq_num, std::string const & error_msg) : m_seq_num(seq_num), m_error_msg(error_msg) {}

    unsigned m_seq_num = static_cast<unsigned>(-1);
    json m_payload;
    optional<std::string> m_error_msg;

    json to_json_response() const {
        json j;
        if (m_error_msg) {
            j["response"] = "error";
            j["message"] = *m_error_msg;
        } else {
            j = m_payload;
            j["response"] = "ok";
        }
        j["seq_num"] = m_seq_num;
        return j;
    }
};

struct unrelated_error_msg {
    std::string m_msg;

    json to_json_response() const {
        json j;
        j["response"] = "error";
        j["message"] = m_msg;
        return j;
    }
};

// Debugging functions for use in GDB.
server * g_server = nullptr;
void server_dump_log_tree() {
    g_server->get_log_tree().print_to(std::cerr);
}
void server_print_roi() {
    auto roi = g_server->get_roi();
    std::cerr << "mode: " << roi.m_check_mode << std::endl;
    for (auto & f : *roi.m_open_files) {
        std::cerr << f.first << std::endl;
        for (auto & lr : f.second) {
            std::cerr << " " << lr.m_begin_line << "-" << lr.m_end_line << std::endl;
        }
    }
}

void server::run() {
    flet<server *> _(g_server, this);

    scope_global_ios scoped_ios(m_ios);

    /* Leo: we use std::setlocale to make sure decimal period is displayed as ".".
       We added this hack because the json library code used for ensuring this property
       was crashing when compiling Lean on Windows with mingw. */
#if !defined(LEAN_EMSCRIPTEN)
    std::setlocale(LC_NUMERIC, "C");
#endif

    while (true) {
        try {
            std::string req_string;
            std::getline(std::cin, req_string);
            if (std::cin.eof()) return;

            json req = json::parse(req_string);

            handle_request(req);
        } catch (std::exception & ex) {
            send_msg(unrelated_error_msg{ex.what()});
        }
    }
}

void server::handle_request(json const & jreq) {
    cmd_req req;
    req.m_seq_num = jreq.at("seq_num");
    try {
        req.m_cmd_name = jreq.at("command");
        req.m_payload = jreq;
        handle_request(req);
    } catch (std::exception & ex) {
        send_msg(cmd_res(req.m_seq_num, std::string(ex.what())));
    } catch (interrupted) {
        send_msg(cmd_res(req.m_seq_num, std::string("interrupted")));
    } catch (...) {
        send_msg(cmd_res(req.m_seq_num, std::string("unknown exception")));
    }
}

void server::handle_request(server::cmd_req const & req) {
    std::string command = req.m_cmd_name;

    if (command == "sync") {
        send_msg(handle_sync(req));
    } else if (command == "complete") {
        handle_async_response(req, handle_complete(req));
    } else if (command == "info") {
        handle_async_response(req, handle_info(req));
    } else if (command == "search") {
        send_msg(handle_search(req));
    } else if (command == "roi") {
        send_msg(handle_roi(req));
    } else if (command == "sleep") {
        chrono::milliseconds small_delay(1000);
        this_thread::sleep_for(small_delay);
    } else if (command == "long_sleep") {
        chrono::milliseconds small_delay(10000);
        this_thread::sleep_for(small_delay);
    } else if (command == "sync_output") {
        taskq().wait_for_finish(this->m_lt.get_root().wait_for_finish());
    } else {
        send_msg(cmd_res(req.m_seq_num, std::string("unknown command")));
    }
}

void server::handle_async_response(server::cmd_req const & req, task<cmd_res> const & res) {
    taskq().submit(task_builder<unit>([this, req, res] {
        try {
            auto msg = get(res);
            msg.m_seq_num = req.m_seq_num;
            send_msg(msg);
        } catch (throwable & ex) {
            send_msg(cmd_res(req.m_seq_num, std::string(ex.what())));
        } catch (interrupted) {
            send_msg(cmd_res(req.m_seq_num, std::string("interrupted")));
        } catch (...) {
            send_msg(cmd_res(req.m_seq_num, std::string("unknown exception")));
        }
        return unit{};
    }).depends_on(res).build());
}

server::cmd_res server::handle_sync(server::cmd_req const & req) {
    std::string new_file_name = req.m_payload.at("file_name");
    std::string new_content;
    if (req.m_payload.count("content")) {
        new_content = req.m_payload.at("content");
    } else {
        new_content = load_module(new_file_name, /* can_use_olean */ false)->m_contents;
    }

    auto mtime = time(nullptr);

    bool needs_invalidation = true;

    auto & ef = m_open_files[new_file_name];
    if (ef.m_content != new_content) {
        ef.m_content = new_content;
        ef.m_mtime = mtime;
        needs_invalidation = true;
    } else {
        needs_invalidation = false;
    }

    json res;
    if (needs_invalidation) {
        m_mod_mgr->invalidate(new_file_name);
        res["message"] = "file invalidated";
    } else {
        res["message"] = "file unchanged";
    }

    return { req.m_seq_num, res };
}

optional<module_parser_result> get_closest_snapshot(std::shared_ptr<module_info const> const & mod_info, pos_info p) {
    auto res = mod_info->m_snapshots;

    while (res && res->m_next) {
        if (auto next = peek(res->m_next)) {
            if (next->m_range.m_end < p) {
                res = next;
            } else {
                break;
            }
        } else {
            break;
        }
    }

    return res;
}

void parse_breaking_at_pos(module_id const & mod_id, std::shared_ptr<module_info const> mod_info, pos_info pos,
                           bool complete = false) {
    if (auto snap = get_closest_snapshot(mod_info, pos)) {
        // ignore messages from reparsing
        log_tree null;
        scope_log_tree scope_lt(null.get_root());
        snap->m_lt = logtree();
        snap->m_cancel = global_cancellation_token();
        snap->m_next = nullptr;

        auto p = std::make_shared<module_parser>(mod_id, mod_info->m_contents, environment(), mk_dummy_loader());
        p->save_info(false);
        p->use_separate_tasks(false);
        p->break_at_pos(pos, complete);

        p->resume(*snap, {});
    }
}

json server::autocomplete(std::shared_ptr<module_info const> const & mod_info, bool skip_completions,
                          pos_info const & pos0) {
    auto pos = pos0;
    if (pos.second == 0)
        pos.first--;
    pos.second--;
    json j;

    if (auto snap = get_closest_snapshot(mod_info, pos)) {
        try {
            parse_breaking_at_pos(mod_info->m_id, mod_info, pos, true);
        } catch (break_at_pos_exception & e) {
            report_completions(snap->m_snapshot_at_end->m_env, snap->m_snapshot_at_end->m_options,
                               pos0, skip_completions, m_path, mod_info->m_id.c_str(),
                               e, j);
        } catch (throwable & ex) {}
    }
    return j;
}

task<server::cmd_res> server::handle_complete(cmd_req const & req) {
    cancel(m_bg_task_ctok);
    m_bg_task_ctok = mk_cancellation_token();

    std::string fn = req.m_payload.at("file_name");
    pos_info pos = {req.m_payload.at("line"), req.m_payload.at("column")};
    bool skip_completions = false;
    if (req.m_payload.count("skip_completions"))
        skip_completions = req.m_payload.at("skip_completions");

    auto mod_info = m_mod_mgr->get_module(fn);

    return task_builder<cmd_res>([=] { return cmd_res(req.m_seq_num, autocomplete(mod_info, skip_completions, pos)); })
        .wrap(library_scopes(log_tree::node()))
        .set_cancellation_token(m_bg_task_ctok)
        .build();
}

static void get_info_managers(log_tree::node const & n, std::vector<info_manager> & infoms) {
    n.for_each([&] (log_tree::node const & c) {
        for (auto & e : c.get_entries()) {
            if (auto infom = dynamic_cast<info_manager const *>(e.get())) {
                infoms.push_back(*infom);
            }
        }
        return true;
    });
}

std::vector<info_manager> get_info_managers(log_tree const & t) {
    std::vector<info_manager> infoms;
    get_info_managers(t.get_root(), infoms);
    return infoms;
}

json server::info(std::shared_ptr<module_info const> const & mod_info, pos_info const & pos) {
    json j;
    try {
        parse_breaking_at_pos(mod_info->m_id, mod_info, pos);
    } catch (break_at_pos_exception & e) {
        auto opts = m_ios.get_options();
        auto env = m_initial_env;
        if (auto snap = get_closest_snapshot(mod_info, e.m_token_info.m_pos)) {
            env = snap->m_snapshot_at_end->m_env;
            opts = snap->m_snapshot_at_end->m_options;
        }
        report_info(env, opts, m_ios, m_path, *mod_info, get_info_managers(m_lt), pos, e, j);
    } catch (throwable & ex) {}

    return j;
}

task<server::cmd_res> server::handle_info(server::cmd_req const & req) {
    cancel(m_bg_task_ctok);
    m_bg_task_ctok = mk_cancellation_token();

    std::string fn = req.m_payload.at("file_name");
    pos_info pos = {req.m_payload.at("line"), req.m_payload.at("column")};

    auto mod_info = m_mod_mgr->get_module(fn);

    return task_builder<cmd_res>([=] {
        return cmd_res(req.m_seq_num, info(mod_info, pos));
    }).wrap(library_scopes(log_tree::node()))
      .set_cancellation_token(m_bg_task_ctok).build();
}

server::cmd_res server::handle_search(server::cmd_req const & req) {
    std::string query = req.m_payload.at("query");

    std::vector<pair<std::string, environment>> envs_to_search;
    for (auto & mod : m_mod_mgr->get_all_modules()) {
        envs_to_search.emplace_back(mod->m_id, mod->get_latest_env());
    }

    std::vector<json> results;
    search_decls(query, envs_to_search, m_ios.get_options(), results);

    json j;
    j["results"] = results;

    return cmd_res(req.m_seq_num, j);
}

std::shared_ptr<module_info> server::load_module(module_id const & id, bool can_use_olean) {
    if (m_open_files.count(id)) {
        auto & ef = m_open_files[id];
        return std::make_shared<module_info>(id, ef.m_content, module_src::LEAN, ef.m_mtime);
    }
    return m_fs_vfs.load_module(id, can_use_olean);
}

template <class Msg>
void server::send_msg(Msg const & m) {
    json j = m.to_json_response();
    unique_lock<mutex> _(m_out_mutex);
    std::cout << j << std::endl;
}

region_of_interest server::get_roi() {
    unique_lock<mutex> _(m_roi_mutex);
    return m_roi;
}

static region_of_interest::checking_mode parse_checking_mode(std::string const & j) {
    if (j == "nothing") return region_of_interest::Nothing;
    if (j == "visible-lines") return region_of_interest::VisibleLines;
    if (j == "visible-lines-and-above") return region_of_interest::VisibleLinesAndAbove;
    if (j == "visible-files") return region_of_interest::VisibleFiles;
    if (j == "open-files") return region_of_interest::OpenFiles;
    throw exception(sstream() << "unknown checking mode: " << j);
}

server::cmd_res server::handle_roi(server::cmd_req const & req) {
    region_of_interest new_roi;
    new_roi.m_check_mode = parse_checking_mode(req.m_payload.at("mode"));
    auto open_files = std::make_shared<std::unordered_map<std::string, std::vector<line_range>>>();
    new_roi.m_open_files = open_files;

    for (auto & f : req.m_payload.at("files")) {
        std::string fn = f.at("file_name");
        std::vector<line_range> ranges;
        for (auto & r : f.at("ranges")) {
            unsigned begin_line = r.at("begin_line");
            unsigned end_line = r.at("end_line");
            ranges.push_back({begin_line, end_line});
        }
        (*open_files)[fn] = ranges;
    }

    for (auto & f : *new_roi.m_open_files) {
        try { m_mod_mgr->get_module(f.first); } catch (...) {}
    }

    {
        unique_lock<mutex> _(m_roi_mutex);
        m_roi = new_roi;
    }
    m_tasks_handler->on_new_roi();
    m_msg_handler->on_new_roi();

    return cmd_res(req.m_seq_num, json());
}

void initialize_server() {
}

void finalize_server() {
}

}
#endif
