/*
 *
 * Conky, a system monitor, based on torsmo
 *
 * Please see COPYING for details
 *
 * Copyright (C) 2010 Pavel Labath et al.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

#ifndef SETTING_HH
#define SETTING_HH

#include <limits>
#include <optional>
#include <string>
#include <type_traits>

#include "../logging.h"
#include "luamm.hh"

namespace conky {

/*
 * Checks settings, and does initial calls to the setters.
 * Should be called after reading the user config.
 * stack on entry: | ... |
 * stack on exit:  | ... |
 */
void set_config_settings(lua::state &l);

/*
 * Calls cleanup functions.
 * Should be called before exit/restart.
 * stack on entry: | ... |
 * stack on exit:  | ... |
 */
void cleanup_config_settings(lua::state &l);

template <typename T, bool is_integral = std::is_integral<T>::value,
          bool floating_point = std::is_floating_point<T>::value,
          bool is_enum = std::is_enum<T>::value>
struct lua_traits {
  // integral is here to force the compiler to evaluate the assert at
  // instantiation time
  static_assert(is_integral && false,
                "Only specializations for enum, string, integral and floating "
                "point types are available");
};

// specialization for integral types
template <typename T>
struct lua_traits<T, true, false, false> {
  static const lua::Type type = lua::TNUMBER;
  typedef lua::integer Type;

  static inline std::pair<Type, bool> convert(lua::state &l, int index,
                                              const std::string &) {
    return {l.tointeger(index), true};
  }
};

// specialization for floating point types
template <typename T>
struct lua_traits<T, false, true, false> {
  static const lua::Type type = lua::TNUMBER;
  typedef lua::number Type;

  static inline std::pair<Type, bool> convert(lua::state &l, int index,
                                              const std::string &) {
    return {l.tonumber(index), true};
  }
};

// specialization for std::string
template <>
struct lua_traits<std::string, false, false, false> {
  static const lua::Type type = lua::TSTRING;
  typedef std::string Type;

  static inline std::pair<Type, bool> convert(lua::state &l, int index,
                                              const std::string &) {
    return {l.tostring(index), true};
  }
};

// specialization for bool
template <>
struct lua_traits<bool, true, false, false> {
  static const lua::Type type = lua::TBOOLEAN;
  typedef bool Type;

  static inline std::pair<Type, bool> convert(lua::state &l, int index,
                                              const std::string &) {
    return {l.toboolean(index), true};
  }
};

// specialization for enums
// to use this, one first has to declare string<->value map
template <typename T>
struct lua_traits<T, false, false, true> {
  static const lua::Type type = lua::TSTRING;
  typedef T Type;

  typedef std::initializer_list<std::pair<std::string, T>> Map;
  static Map map;

  static std::pair<T, bool> convert(lua::state &l, int index,
                                    const std::string &name) {
    std::string val = l.tostring(index);

    for (auto i = map.begin(); i != map.end(); ++i) {
      if (i->first == val) return {i->second, true};
    }

    std::string msg = "invalid value '" + val + "' for setting '" + name +
                      "', valid values are: ";
    for (auto i = map.begin(); i != map.end(); ++i) {
      if (i != map.begin()) msg += ", ";
      msg += "'" + i->first + "'";
    }
    LOG_ERROR("{}", msg);

    return {T(), false};
  }
};

namespace priv {
class config_setting_base {
 private:
  static void process_setting(lua::state &l, bool init);
  static int config__newindex(lua::state *l);
  static void make_conky_config(lua::state &l);

  config_setting_base(const config_setting_base &) = delete;
  config_setting_base &operator=(const config_setting_base &) = delete;

 protected:
  config_setting_base(config_setting_base &&other) noexcept;
  /*
   * Set the setting, if the value is sane
   * stack on entry: | ... potential_new_value old_value |
   * stack on exit:  | ... real_new_value |
   * real_new_value can be the old value if the new value doesn't make sense
   */
  virtual void lua_setter(lua::state &l, bool init) = 0;

  /*
   * Called on exit/restart.
   * stack on entry: | ... new_value |
   * stack on exit:  | ... |
   */
  virtual void cleanup(lua::state &l) { l.pop(); }

 public:
  const std::string name;
  const size_t seq_no;
  std::optional<std::string> deprecation_msg;

  static bool seq_compare(const config_setting_base *a,
                          const config_setting_base *b) {
    return a->seq_no < b->seq_no;
  }

  explicit config_setting_base(std::string name_);
  virtual ~config_setting_base() {}

  /*
   * Set the setting manually.
   * stack on entry: | ... new_value |
   * stack on exit:  | ... |
   */
  void lua_set(lua::state &l);

  friend void conky::set_config_settings(lua::state &l);
  friend void conky::cleanup_config_settings(lua::state &l);
};
}  // namespace priv

/// Mark a config setting as deprecated. Returns the setting by move.
template <typename T>
T deprecated(T setting, const char *msg) {
  setting.deprecation_msg = msg;
  return setting;
}

// If you need some very exotic setting, derive it from this class. Otherwise,
// scroll down.
template <typename T>
class config_setting_template : public priv::config_setting_base {
 public:
  explicit config_setting_template(const std::string &name_)
      : config_setting_base(name_) {}
  config_setting_template(config_setting_template &&) = default;

  // get the value of the setting as a C++ type
  T get(lua::state &l);

 protected:
  /*
   * Convert the value into a C++ type.
   * stack on entry: | ... value |
   * stack on exit:  | ... |
   */
  virtual T getter(lua::state &l) = 0;
};

template <typename T>
T config_setting_template<T>::get(lua::state &l) {
  std::lock_guard<lua::state> guard(l);
  lua::stack_sentry s(l);
  l.checkstack(2);

  l.getglobal("conky");
  l.getfield(-1, "config");
  l.replace(-2);

  l.getfield(-1, name.c_str());
  l.replace(-2);

  return getter(l);
}

/*
 * Declares a setting <name> in the conky.config table.
 * Getter function is used to translate the lua value into C++. It receives the
 * value on the lua stack. It should pop it and return the C++ value. In case
 * the value is nil, it should return a predefined default value. Translation
 * into basic types works with the help of lua_traits template above The
 * lua_setter function is called when someone tries to set the value.  It
 * receives the new and the old value on the stack (old one is on top). It
 * should return the new value for the setting. It doesn't have to be the value
 * the user set, if e.g. the value doesn't make sense. The second parameter is
 * true if the assignment occurs during the initial parsing of the config file,
 * and false afterwards. Some settings obviously cannot be changed (easily?)
 * when conky is running, but some (e.g. x/y position of the window) can.
 */
template <typename T, typename Traits = lua_traits<T>>
class simple_config_setting : public config_setting_template<T> {
  typedef config_setting_template<T> Base;

 public:
  explicit simple_config_setting(const std::string &name_,
                                 const T &default_value_ = T(),
                                 bool modifiable_ = false)
      : Base(name_), default_value(default_value_), modifiable(modifiable_) {}
  simple_config_setting(const char *name_, const T &default_value_ = T(),
                        bool modifiable_ = false)
      : Base(std::string(name_)),
        default_value(default_value_),
        modifiable(modifiable_) {}
  simple_config_setting(simple_config_setting &&) = default;

 protected:
  const T default_value;
  const bool modifiable;

  virtual std::pair<typename Traits::Type, bool> do_convert(lua::state &l,
                                                            int index);
  virtual void lua_setter(lua::state &l, bool init);

  virtual T getter(lua::state &l) {
    lua::stack_sentry s(l, -1);
    auto ret = do_convert(l, -1);
    l.pop();

    // setter function should make sure the value is valid
    assert(ret.second);

    return ret.first;
  }
};

template <typename T, typename Traits>
std::pair<typename Traits::Type, bool>
simple_config_setting<T, Traits>::do_convert(lua::state &l, int index) {
  if (l.isnil(index)) return {default_value, true};

  if (l.type(index) != Traits::type) {
    LOG_ERROR(
        "invalid value of type '{}' for setting '{}', "
        "expected type '{}'",
        l.type_name(l.type(index)), Base::name, l.type_name(Traits::type));
    return {default_value, false};
  }

  return Traits::convert(l, index, Base::name);
}

template <typename T, typename Traits>
void simple_config_setting<T, Traits>::lua_setter(lua::state &l, bool init) {
  lua::stack_sentry s(l, -2);

  bool ok = true;
  if (!init && !modifiable) {
    LOG_ERROR("setting '{}' is not modifiable", Base::name);
    ok = false;
  }

  if (ok && do_convert(l, -2).second) {
    l.pop();
  } else {
    l.replace(-2);
  }
  ++s;
}

template <typename Signed1, typename Signed2>
bool between(Signed1 value, Signed2 min,
             typename std::enable_if<std::is_signed<Signed2>::value,
                                     Signed2>::type max) {
  return value >= min && value <= max;
}

template <typename Signed1, typename Unsigned2>
bool between(Signed1 value, Unsigned2 min,
             typename std::enable_if<std::is_unsigned<Unsigned2>::value,
                                     Unsigned2>::type max) {
  return value >= 0 &&
         static_cast<typename std::make_unsigned<Signed1>::type>(value) >=
             min &&
         static_cast<typename std::make_unsigned<Signed1>::type>(value) <= max;
}

// Just like simple_config_setting, except that in only accepts value in the
// [min, max] range
template <typename T, typename Traits = lua_traits<T>>
class range_config_setting : public simple_config_setting<T, Traits> {
  typedef simple_config_setting<T, Traits> Base;

  const T min;
  const T max;

 public:
  explicit range_config_setting(const std::string &name_,
                                const T &min_ = std::numeric_limits<T>::min(),
                                const T &max_ = std::numeric_limits<T>::max(),
                                const T &default_value_ = T(),
                                bool modifiable_ = false)
      : Base(name_, default_value_, modifiable_), min(min_), max(max_) {
    assert(min <= Base::default_value && Base::default_value <= max);
  }
  range_config_setting(range_config_setting &&) = default;

 protected:
  virtual std::pair<typename Traits::Type, bool> do_convert(lua::state &l,
                                                            int index) {
    auto ret = Base::do_convert(l, index);
    if (ret.second && !between(ret.first, min, max)) {
      LOG_ERROR("value {} is out of range for setting '{}' (expected {}-{})",
                ret.first, Base::name, min, max);
      // we ignore out-of-range values. an alternative would be to clamp them.
      // do we want to do that?
      ret.second = false;
    }
    return ret;
  }
};

}  // namespace conky

#endif /* SETTING_HH */
