// Copyright (c) 2006-2013, Andrey N. Sabelnikov, www.sabelnikov.net
// 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 the Andrey N. Sabelnikov 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 COPYRIGHT HOLDERS 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 COPYRIGHT OWNER  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.
//

#pragma once

#include "misc_log_ex.h"
#include "string_tools.h"
#include <atomic>
#include <condition_variable>
#include <functional>
#include <mutex>
#include <thread>
#include <iostream>
#ifdef __OpenBSD__
#include <stdio.h>
#endif
#include <boost/thread.hpp>
#include <boost/algorithm/string/classification.hpp>
#include <boost/algorithm/string/split.hpp>

#ifdef HAVE_READLINE
  #include "readline_buffer.h"
#endif

#undef MONERO_DEFAULT_LOG_CATEGORY
#define MONERO_DEFAULT_LOG_CATEGORY "console_handler"

namespace epee
{
  class async_stdin_reader
  {
  public:
    async_stdin_reader()
      : m_run(true)
      , m_has_read_request(false)
      , m_read_status(state_init)
    {
#ifdef HAVE_READLINE
      m_readline_buffer.start();
#endif
      m_reader_thread = boost::thread(std::bind(&async_stdin_reader::reader_thread_func, this));
    }

    ~async_stdin_reader()
    {
      try { stop(); }
      catch (...) { /* ignore */ }
    }

#ifdef HAVE_READLINE
    rdln::readline_buffer& get_readline_buffer()
    {
      return m_readline_buffer;
    }
#endif

    // Not thread safe. Only one thread can call this method at once.
    bool get_line(std::string& line)
    {
      if (!start_read())
        return false;

      if (state_eos == m_read_status)
        return false;

      boost::unique_lock<boost::mutex> lock(m_response_mutex);
      while (state_init == m_read_status)
      {
        m_response_cv.wait(lock);
      }

      bool res = false;
      if (state_success == m_read_status)
      {
        line = m_line;
        res = true;
      }

      if (!eos() && m_read_status != state_cancelled)
        m_read_status = state_init;

      return res;
    }

    bool eos() const { return m_read_status == state_eos; }

    void stop()
    {
      if (m_run)
      {
        m_run.store(false, std::memory_order_relaxed);

#if defined(WIN32)
        ::CloseHandle(::GetStdHandle(STD_INPUT_HANDLE));
#endif

        m_request_cv.notify_one();
        m_reader_thread.join();
#ifdef HAVE_READLINE
        m_readline_buffer.stop();
#endif
      }
    }

    void cancel()
    {
      boost::unique_lock<boost::mutex> lock(m_response_mutex);
      m_read_status = state_cancelled;
      m_has_read_request = false;
      m_response_cv.notify_one();
    }

  private:
    bool start_read()
    {
      boost::unique_lock<boost::mutex> lock(m_request_mutex);
      if (!m_run.load(std::memory_order_relaxed) || m_has_read_request)
        return false;

      m_has_read_request = true;
      m_request_cv.notify_one();
      return true;
    }

    bool wait_read()
    {
      boost::unique_lock<boost::mutex> lock(m_request_mutex);
      while (m_run.load(std::memory_order_relaxed) && !m_has_read_request)
      {
        m_request_cv.wait(lock);
      }

      if (m_has_read_request)
      {
        m_has_read_request = false;
        return true;
      }

      return false;
    }

    bool wait_stdin_data()
    {
#if !defined(WIN32)
      #if defined(__OpenBSD__) || defined(__ANDROID__)
      int stdin_fileno = fileno(stdin);
      #else
      int stdin_fileno = ::fileno(stdin);
      #endif

      while (m_run.load(std::memory_order_relaxed))
      {
        if (m_read_status == state_cancelled)
          return false;

        fd_set read_set;
        FD_ZERO(&read_set);
        FD_SET(stdin_fileno, &read_set);

        struct timeval tv;
        tv.tv_sec = 0;
        tv.tv_usec = 100 * 1000;

        int retval = ::select(stdin_fileno + 1, &read_set, NULL, NULL, &tv);
        if (retval < 0)
          return false;
        else if (0 < retval)
          return true;
      }
#else
      while (m_run.load(std::memory_order_relaxed))
      {
        if (m_read_status == state_cancelled)
          return false;

        int retval = ::WaitForSingleObject(::GetStdHandle(STD_INPUT_HANDLE), 100);
        switch (retval)
        {
          case WAIT_FAILED:
            return false;
          case WAIT_OBJECT_0:
            return true;
          default:
            break;
        }
      }
#endif

      return true;
    }

    void reader_thread_func()
    {
      while (true)
      {
        if (!wait_read())
          break;

        std::string line;
        bool read_ok = true;
#ifdef HAVE_READLINE
reread:
#endif
        if (wait_stdin_data())
        {
          if (m_run.load(std::memory_order_relaxed))
          {
#ifdef HAVE_READLINE
            switch (m_readline_buffer.get_line(line))
            {
            case rdln::empty:   goto eof;
            case rdln::partial: goto reread;
            case rdln::full:    break;
            }
#else
            if (m_read_status != state_cancelled)
              std::getline(std::cin, line);
#endif
            read_ok = !std::cin.eof() && !std::cin.fail();
          }
        }
        else
        {
          read_ok = false;
        }
        if (std::cin.eof()) {
#ifdef HAVE_READLINE
eof:
#endif
          m_read_status = state_eos;
          m_response_cv.notify_one();
          break;
        }
        else
        {
          boost::unique_lock<boost::mutex> lock(m_response_mutex);
          if (m_run.load(std::memory_order_relaxed))
          {
            m_line = std::move(line);
            m_read_status = read_ok ? state_success : state_error;
          }
          else
          {
            m_read_status = state_cancelled;
          }
          m_response_cv.notify_one();
        }
      }
    }

    enum t_state
    {
      state_init,
      state_success,
      state_error,
      state_cancelled,
      state_eos
    };

  private:
    boost::thread m_reader_thread;
    std::atomic<bool> m_run;
#ifdef HAVE_READLINE
    rdln::readline_buffer m_readline_buffer;
#endif

    std::string m_line;
    bool m_has_read_request;
    t_state m_read_status;

    boost::mutex m_request_mutex;
    boost::mutex m_response_mutex;
    boost::condition_variable m_request_cv;
    boost::condition_variable m_response_cv;
  };


  template<class t_server>
  bool empty_commands_handler(t_server* psrv, const std::string& command)
  {
    return true;
  }


  class async_console_handler
  {
  public:
    async_console_handler()
    {
    }

    template<class t_server, class chain_handler>
    bool run(t_server* psrv, chain_handler ch_handler, std::function<std::string(void)> prompt, const std::string& usage = "")
    {
      return run(prompt, usage, [&](const std::string& cmd) { return ch_handler(psrv, cmd); }, [&] { psrv->send_stop_signal(); });
    }

    template<class chain_handler>
    bool run(chain_handler ch_handler, std::function<std::string(void)> prompt, const std::string& usage = "", std::function<void(void)> exit_handler = NULL)
    {
      return run(prompt, usage, [&](const boost::optional<std::string>& cmd) { return ch_handler(cmd); }, exit_handler);
    }

    void stop()
    {
      m_running = false;
      m_stdin_reader.stop();
    }

    void cancel()
    {
      m_cancel = true;
      m_stdin_reader.cancel();
    }

    void print_prompt()
    {
      std::string prompt = m_prompt();
      if (!prompt.empty())
      {
#ifdef HAVE_READLINE
        std::string color_prompt = "\001\033[1;33m\002" + prompt;
        if (' ' != prompt.back())
          color_prompt += " ";
        color_prompt += "\001\033[0m\002";
        m_stdin_reader.get_readline_buffer().set_prompt(color_prompt);
#else
        epee::set_console_color(epee::console_color_yellow, true);
        std::cout << prompt;
        if (' ' != prompt.back())
          std::cout << ' ';
        epee::reset_console_color();
        std::cout.flush();
#endif
      }
    }

  private:
    template<typename t_cmd_handler>
    bool run(std::function<std::string(void)> prompt, const std::string& usage, const t_cmd_handler& cmd_handler, std::function<void(void)> exit_handler)
    {
      bool continue_handle = true;
      m_prompt = prompt;
      while(continue_handle)
      {
        try
        {
          if (!m_running)
          {
            break;
          }
          print_prompt();

          std::string command;
          bool get_line_ret = m_stdin_reader.get_line(command);
          if (!m_running)
            break;
          if (m_stdin_reader.eos())
          {
            MGINFO("EOF on stdin, exiting");
            std::cout << std::endl;
            break;
          }

          if (m_cancel)
          {
            MDEBUG("Input cancelled");
            cmd_handler(boost::none);
            m_cancel = false;
            continue;
          }
          if (!get_line_ret)
          {
            MERROR("Failed to read line.");
          }

          string_tools::trim(command);

          LOG_PRINT_L2("Read command: " << command);
          if(cmd_handler(command))
          {
            continue;
          }
          else if(0 == command.compare("exit") || 0 == command.compare("q"))
          {
            continue_handle = false;
          }
          else
          {
#ifdef HAVE_READLINE
            rdln::suspend_readline pause_readline;
#endif
            std::cout << "unknown command: " << command << std::endl;
            std::cout << usage;
          }
        }
        catch (const std::exception &ex)
        {
          LOG_ERROR("Exception at [console_handler], what=" << ex.what());
        }
      }
      if (exit_handler)
        exit_handler();
      return true;
    }

  private:
    async_stdin_reader m_stdin_reader;
    std::atomic<bool> m_running = {true};
    std::atomic<bool> m_cancel = {false};
    std::function<std::string(void)> m_prompt;
  };


  template<class t_server, class t_handler>
  bool start_default_console(t_server* ptsrv, t_handler handlr, std::function<std::string(void)> prompt, const std::string& usage = "")
  {
    std::shared_ptr<async_console_handler> console_handler = std::make_shared<async_console_handler>();
    boost::thread([=](){console_handler->run<t_server, t_handler>(ptsrv, handlr, prompt, usage);}).detach();
    return true;
  }

  template<class t_server, class t_handler>
  bool start_default_console(t_server* ptsrv, t_handler handlr, const std::string& prompt, const std::string& usage = "")
  {
    return start_default_console(ptsrv, handlr, [prompt](){ return prompt; }, usage);
  }

  template<class t_server>
  bool start_default_console(t_server* ptsrv, const std::string& prompt, const std::string& usage = "")
  {
    return start_default_console(ptsrv, empty_commands_handler<t_server>, prompt, usage);
  }

  template<class t_server, class t_handler>
    bool no_srv_param_adapter(t_server* ptsrv, const std::string& cmd, t_handler handlr)
    {
      return handlr(cmd);
    }

  template<class t_server, class t_handler>
  bool run_default_console_handler_no_srv_param(t_server* ptsrv, t_handler handlr, std::function<std::string(void)> prompt, const std::string& usage = "")
  {
    async_console_handler console_handler;
    return console_handler.run(ptsrv, std::bind<bool>(no_srv_param_adapter<t_server, t_handler>, std::placeholders::_1, std::placeholders::_2, handlr), prompt, usage);
  }

  template<class t_server, class t_handler>
  bool run_default_console_handler_no_srv_param(t_server* ptsrv, t_handler handlr, const std::string& prompt, const std::string& usage = "")
  {
    return run_default_console_handler_no_srv_param(ptsrv, handlr, [prompt](){return prompt;},usage);
  }

  template<class t_server, class t_handler>
  bool start_default_console_handler_no_srv_param(t_server* ptsrv, t_handler handlr, std::function<std::string(void)> prompt, const std::string& usage = "")
  {
    boost::thread( boost::bind(run_default_console_handler_no_srv_param<t_server, t_handler>, ptsrv, handlr, prompt, usage) );
    return true;
  }

  template<class t_server, class t_handler>
  bool start_default_console_handler_no_srv_param(t_server* ptsrv, t_handler handlr, const std::string& prompt, const std::string& usage = "")
  {
    return start_default_console_handler_no_srv_param(ptsrv, handlr, [prompt](){return prompt;}, usage);
  }

  /*template<class a>
  bool f(int i, a l)
  {
    return true;
  }*/
  /*
  template<class chain_handler>
  bool default_console_handler2(chain_handler ch_handler, const std::string usage)
  */


  /*template<class t_handler>
  bool start_default_console2(t_handler handlr, const std::string& usage = "")
  {
    //std::string usage_local = usage;
    boost::thread( boost::bind(default_console_handler2<t_handler>, handlr, usage) );
    //boost::function<bool ()> p__ = boost::bind(f<t_handler>, 1, handlr);
    //boost::function<bool ()> p__ = boost::bind(default_console_handler2<t_handler>, handlr, usage);
    //boost::thread tr(p__);
    return true;
  }*/

  class command_handler {
  public:
    typedef boost::function<bool (const std::vector<std::string> &)> callback;
    typedef boost::function<bool (void)> empty_callback;
    typedef std::map<std::string, std::pair<callback, std::pair<std::string, std::string>>> lookup;

    command_handler():
      m_unknown_command_handler([](const std::vector<std::string>&){return false;}),
      m_empty_command_handler([](){return true;}),
      m_cancel_handler([](){return true;})
    {
    }

    std::string get_usage()
    {
      std::stringstream ss;

      for(auto& x:m_command_handlers)
      {
        ss << x.second.second.first << ENDL;
      }
      return ss.str();
    }

    std::pair<std::string, std::string> get_documentation(const std::vector<std::string>& cmd)
    {
      if(cmd.empty())
        return std::make_pair("", "");
      auto it = m_command_handlers.find(cmd.front());
      if(it == m_command_handlers.end())
        return std::make_pair("", "");
      return it->second.second;
    }

    std::vector<std::string> get_command_list(const std::vector<std::string>& keywords = std::vector<std::string>())
    {
      std::vector<std::string> list;
      list.reserve(m_command_handlers.size());
      for(auto const& x:m_command_handlers)
      {
        bool take = true;
        for(auto const& y:keywords)
        {
          bool in_usage = x.second.second.first.find(y) != std::string::npos;
          bool in_description = x.second.second.second.find(y) != std::string::npos;
          if (!(in_usage || in_description))
          {
            take = false;
            break;
          }
        }
        if (take)
        {
          list.push_back(x.first);
        }
      }
      return list;
    }

    void set_handler(const std::string& cmd, const callback& hndlr, const std::string& usage = "", const std::string& description = "")
    {
      lookup::mapped_type & vt = m_command_handlers[cmd];
      vt.first = hndlr;
      vt.second.first = description.empty() ? cmd : usage;
      vt.second.second = description.empty() ? usage : description;
#ifdef HAVE_READLINE
      rdln::readline_buffer::add_completion(cmd);
#endif
    }

    void set_unknown_command_handler(const callback& hndlr)
    {
      m_unknown_command_handler = hndlr;
    }

    void set_empty_command_handler(const empty_callback& hndlr)
    {
      m_empty_command_handler = hndlr;
    }

    void set_cancel_handler(const empty_callback& hndlr)
    {
      m_cancel_handler = hndlr;
    }

    bool process_command_vec(const std::vector<std::string>& cmd)
    {
      if(!cmd.size() || (cmd.size() == 1 && !cmd[0].size()))
        return m_empty_command_handler();
      auto it = m_command_handlers.find(cmd.front());
      if(it == m_command_handlers.end())
        return m_unknown_command_handler(cmd);
      std::vector<std::string> cmd_local(cmd.begin()+1, cmd.end());
      return it->second.first(cmd_local);
    }

    bool process_command_str(const boost::optional<std::string>& cmd)
    {
      if (!cmd)
        return m_cancel_handler();
      std::vector<std::string> cmd_v;
      boost::split(cmd_v,*cmd,boost::is_any_of(" "), boost::token_compress_on);
      return process_command_vec(cmd_v);
    }
  private:
    lookup m_command_handlers;
    callback m_unknown_command_handler;
    empty_callback m_empty_command_handler;
    empty_callback m_cancel_handler;
  };

  /************************************************************************/
  /*                                                                      */
  /************************************************************************/
  class console_handlers_binder : public command_handler
  {
    typedef command_handler::callback console_command_handler;
    typedef command_handler::lookup command_handlers_map;
    std::unique_ptr<boost::thread> m_console_thread;
    async_console_handler m_console_handler;
  public:
    ~console_handlers_binder() {
      try
      {
        stop_handling();
        if (m_console_thread.get() != nullptr)
        {
          m_console_thread->join();
        }
      }
      catch (const std::exception &e) { /* ignore */ }
    }

    bool start_handling(std::function<std::string(void)> prompt, const std::string& usage_string = "", std::function<void(void)> exit_handler = NULL)
    {
      m_console_thread.reset(new boost::thread(boost::bind(&console_handlers_binder::run_handling, this, prompt, usage_string, exit_handler)));
      return true;
    }
    bool start_handling(const std::string &prompt, const std::string& usage_string = "", std::function<void(void)> exit_handler = NULL)
    {
      return start_handling([prompt](){ return prompt; }, usage_string, exit_handler);
    }

    void stop_handling()
    {
      m_console_handler.stop();
    }

    bool run_handling(std::function<std::string(void)> prompt, const std::string& usage_string, std::function<void(void)> exit_handler = NULL)
    {
      return m_console_handler.run(std::bind(&console_handlers_binder::process_command_str, this, std::placeholders::_1), prompt, usage_string, exit_handler);
    }

    void print_prompt()
    {
      m_console_handler.print_prompt();
    }

    void cancel_input()
    {
      m_console_handler.cancel();
    }
  };

  ///* work around because of broken boost bind */
  //template<class t_server>
  //class srv_console_handlers_binder: public command_handler
  //{
  //  async_console_handler m_console_handler;
  //public:
  //  bool start_handling(t_server* psrv, const std::string& prompt, const std::string& usage_string = "")
  //  {
  //    boost::thread(boost::bind(&srv_console_handlers_binder<t_server>::run_handling, this, psrv, prompt, usage_string)).detach();
  //    return true;
  //  }

  //  bool run_handling(t_server* psrv, const std::string& prompt, const std::string& usage_string)
  //  {
  //    return m_console_handler.run(psrv, boost::bind(&srv_console_handlers_binder<t_server>::process_command_str, this, _1, _2), prompt, usage_string);
  //  }

  //  void stop_handling()
  //  {
  //    m_console_handler.stop();
  //  }
  //private:
  //  bool process_command_str(t_server* /*psrv*/, const std::string& cmd)
  //  {
  //    return console_handlers_binder::process_command_str(cmd);
  //  }
  //};
}
