/*
;    Project:       Open Vehicle Monitor System
;    Date:          14th March 2017
;
;    Changes:
;    1.0  Initial release
;
;    (C) 2011       Michael Stegen / Stegen Electronics
;    (C) 2011-2017  Mark Webb-Johnson
;    (C) 2011        Sonny Chen @ EPRO/DX
;
; Permission is hereby granted, free of charge, to any person obtaining a copy
; of this software and associated documentation files (the "Software"), to deal
; in the Software without restriction, including without limitation the rights
; to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
; copies of the Software, and to permit persons to whom the Software is
; furnished to do so, subject to the following conditions:
;
; The above copyright notice and this permission notice shall be included in
; all copies or substantial portions of the Software.
;
; THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
; IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
; FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
; AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
; LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
; OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
; THE SOFTWARE.
*/

#include "ovms_log.h"
static const char *TAG = "config";

#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <string.h>
#include <sstream>
#include <dirent.h>
#include "crypt_base64.h"
#include "ovms_config.h"
#include "ovms_command.h"
#include "ovms_script.h"
#include "ovms_events.h"
#include "ovms_utils.h"
#include "ovms_boot.h"
#include "ovms_semaphore.h"

#ifdef CONFIG_OVMS_SC_ZIP
#include "zip_archive.h"
#endif // CONFIG_OVMS_SC_ZIP

#define OVMS_CONFIGPATH "/store/ovms_config"
#define OVMS_MAXVALSIZE 2500
//#define OVMS_PERSIST_METADATA


OvmsConfig MyConfig __attribute__ ((init_priority (1400)));

void store_mount(int verbosity, OvmsWriter* writer, OvmsCommand* cmd, int argc, const char* const* argv)
  {
  MyConfig.mount();
  writer->puts("Mounted STORE");
  }

void store_unmount(int verbosity, OvmsWriter* writer, OvmsCommand* cmd, int argc, const char* const* argv)
  {
  MyConfig.unmount();
  writer->puts("Unmounted STORE");
  }

int config_validate(OvmsWriter* writer, OvmsCommand* cmd, int argc, const char* const* argv, bool complete)
  {
  if (!MyConfig.ismounted())
    return -1;
  // argv[0] is the <param>
  if (argc == 1)
    return MyConfig.m_map.Validate(writer, argc, argv[0], complete);
  // argv[1] is the <instance>
  if (argc == 2)
    {
    OvmsConfigParam* const* p = MyConfig.m_map.FindUniquePrefix(argv[0]);
    if (!p)	// <param> was not valid, so can't check <instance>
      return -1;
    return (*p)->m_map.Validate(writer, argc, argv[1], complete);
    }
  // argv[2] is the value, which we can't validate
  return -1;
  }

void config_list(int verbosity, OvmsWriter* writer, OvmsCommand* cmd, int argc, const char* const* argv)
  {
  if (!MyConfig.ismounted()) return;

  if (argc == 0)
    {
    // Show all parameters
    for (ConfigMap::iterator it=MyConfig.m_map.begin(); it!=MyConfig.m_map.end(); ++it)
      {
      writer->printf("%-20s %s\n", it->first.c_str(), it->second->GetTitle());
      }
    }
  else
    {
    // Show all instances for a particular parameter
    OvmsConfigParam *p = MyConfig.CachedParam(argv[0]);
    if (p)
      {
      writer->printf("%s (%s %s)\n",p->GetName().c_str(),
        (p->Readable()?"readable":"protected"),
        (p->Writable()?"writeable":"read-only"));
      for (ConfigParamMap::iterator it=p->m_map.begin(); it!=p->m_map.end(); ++it)
        {
        if (p->Readable())
          { writer->printf("  %s: %s\n",it->first.c_str(), it->second.c_str()); }
        else
          { writer->printf("  %s\n",it->first.c_str()); }
        }
      }
    else
      {
      writer->printf("%s not found\n", argv[0]);
      }
    }
  }

void config_set(int verbosity, OvmsWriter* writer, OvmsCommand* cmd, int argc, const char* const* argv)
  {
  if (!MyConfig.ismounted()) return;

  OvmsConfigParam *p = MyConfig.CachedParam(argv[0]);
  if (p==NULL)
    {
    writer->puts("Error: parameter not found");
    return;
    }

  if (!p->Writable())
    {
    writer->puts("Error: parameter is not writeable");
    return;
    }

  p->SetValue(argv[1],argv[2]);
  writer->puts("Parameter has been set.");
  }

void config_rm(int verbosity, OvmsWriter* writer, OvmsCommand* cmd, int argc, const char* const* argv)
  {
  if (!MyConfig.ismounted()) return;

  OvmsConfigParam *p = MyConfig.CachedParam(argv[0]);
  if (p==NULL)
    {
    writer->puts("Error: parameter not found");
    return;
    }

  if (!p->Writable())
    {
    writer->puts("Error: parameter is not writeable");
    return;
    }

  if (p->DeleteInstance(argv[1]))
    {
    writer->printf("Instance %s has been removed.\n", argv[1]);
    return;
    }
  if (strcmp(argv[1], "*") != 0)
    return;
  MyConfig.DeregisterParam(argv[0]);
  writer->printf("Parameter %s has been removed.\n", argv[0]);
  }

#ifdef CONFIG_OVMS_SC_ZIP
void config_backup(int verbosity, OvmsWriter* writer, OvmsCommand* cmd, int argc, const char* const* argv)
  {
  if (!MyConfig.ismounted())
    {
    writer->puts("Error: config store not mounted");
    return;
    }

  // check path:
  if (MyConfig.ProtectedPath(argv[0]))
    {
    writer->printf("Error: path '%s' is protected\n", argv[0]);
    return;
    }

  // get password:
  std::string password;
  if (argc >= 2)
    password = argv[1];
  else
    password = MyConfig.GetParamValue("password", "module");

  MyConfig.Backup(argv[0], password, writer, verbosity);
  }

void config_restore(int verbosity, OvmsWriter* writer, OvmsCommand* cmd, int argc, const char* const* argv)
  {
  if (!MyConfig.ismounted())
    {
    writer->puts("Error: config store not mounted");
    return;
    }

  // check path:
  if (MyConfig.ProtectedPath(argv[0]))
    {
    writer->printf("Error: path '%s' is protected\n", argv[0]);
    return;
    }

  // get password:
  std::string password;
  if (argc >= 2)
    password = argv[1];
  else
    password = MyConfig.GetParamValue("password", "module");

  MyConfig.Restore(argv[0], password, writer, verbosity);
  }
#endif // CONFIG_OVMS_SC_ZIP

#ifdef CONFIG_OVMS_SC_JAVASCRIPT_DUKTAPE

static duk_ret_t DukOvmsConfigParams(duk_context *ctx)
  {
  if (!MyConfig.ismounted()) return 0;

  duk_idx_t arr_idx = duk_push_array(ctx);
  int count = 0;
  for (ConfigMap::iterator it=MyConfig.m_map.begin(); it!=MyConfig.m_map.end(); ++it)
    {
    duk_push_string(ctx, it->first.c_str());
    duk_put_prop_index(ctx, arr_idx, count++);
    }

  return 1;
  }

static duk_ret_t DukOvmsConfigInstances(duk_context *ctx)
  {
  const char *param = duk_to_string(ctx,0);

  if (!MyConfig.ismounted()) return 0;

  OvmsConfigParam *p = MyConfig.CachedParam(param);
  if (p)
    {
    if (! p->Readable()) return 0;  // Parameter is protected, and not readable

    duk_idx_t arr_idx = duk_push_array(ctx);
    int count = 0;
    for (ConfigParamMap::iterator it=p->m_map.begin(); it!=p->m_map.end(); ++it)
      {
      duk_push_string(ctx, it->first.c_str());
      duk_put_prop_index(ctx, arr_idx, count++);
      }
    return 1;
    }

  return 0;
  }

static duk_ret_t DukOvmsConfigGetValues(duk_context *ctx)
  {
  const char *param = duk_to_string(ctx,0);
  std::string prefix = duk_opt_string(ctx,1,"");

  if (!MyConfig.ismounted()) return 0;

  OvmsConfigParam *p = MyConfig.CachedParam(param);
  if (p)
    {
    if (! p->Readable()) return 0;  // Parameter is protected, and not readable

    duk_idx_t obj_idx = duk_push_object(ctx);
    for (ConfigParamMap::iterator it=p->m_map.lower_bound(prefix); it!=p->m_map.end(); ++it)
      {
      if (!startsWith(it->first, prefix)) break;
      duk_push_string(ctx, it->second.c_str());
      duk_put_prop_string(ctx, obj_idx, it->first.substr(prefix.length()).c_str());
      }
    return 1;
    }

  return 0;
  }

static duk_ret_t DukOvmsConfigSetValues(duk_context *ctx)
  {
  const char *param = duk_to_string(ctx,0);
  std::string prefix = duk_opt_string(ctx,1,"");
  duk_require_object(ctx,2);

  if (!duk_is_object(ctx,2)) return 0;
  if (!MyConfig.ismounted()) return 0;

  OvmsConfigParam *p = MyConfig.CachedParam(param);
  if (p)
    {
    if (! p->Writable()) return 0;  // Parameter is not writeable

    ConfigParamMap pmap = p->m_map;
    std::string key, val;
    duk_enum(ctx, 2, 0);
    while (duk_next(ctx, -1, true))
      {
      key = duk_to_string(ctx, -2);
      val = duk_to_string(ctx, -1);
      duk_pop_2(ctx);
      pmap[prefix+key] = val;
      }
    duk_pop(ctx);
    p->m_map.clear();
    p->m_map = std::move(pmap);
    p->Save();
    }

  return 0;
  }

static duk_ret_t DukOvmsConfigGet(duk_context *ctx)
  {
  const char *param = duk_to_string(ctx,0);
  const char *instance = duk_to_string(ctx,1);
  const char *defvalue = duk_to_string(ctx,2);

  if (!MyConfig.ismounted()) return 0;
  OvmsConfigParam *p = MyConfig.CachedParam(param);
  if (p)
    {
    if (! p->Readable()) return 0;  // Parameter is protected, and not readable
    if (p->IsDefined(instance))
      {
      std::string v = p->GetValue(instance);
      duk_push_string(ctx, v.c_str());
      }
    else
      {
      duk_push_string(ctx, defvalue);
      }
    return 1;
    }
  else
    {
    return 0;
    }
  return 0;
  }

static duk_ret_t DukOvmsConfigSet(duk_context *ctx)
  {
  const char *param = duk_to_string(ctx,0);
  const char *instance = duk_to_string(ctx,1);
  const char *value = duk_to_string(ctx,2);

  if (!MyConfig.ismounted()) return 0;

  OvmsConfigParam *p = MyConfig.CachedParam(param);
  if (p)
    {
    if (! p->Writable()) return 0;  // Parameter is not writeable
    p->SetValue(instance, value);
    }
  return 0;
  }

static duk_ret_t DukOvmsConfigDelete(duk_context *ctx)
  {
  const char *param = duk_to_string(ctx,0);
  const char *instance = duk_to_string(ctx,1);

  if (!MyConfig.ismounted()) return 0;

  OvmsConfigParam *p = MyConfig.CachedParam(param);
  if (p)
    {
    if (! p->Writable()) return 0;  // Parameter is not writeable
    p->DeleteInstance(instance);
    }
  return 0;
  }

#endif // #ifdef CONFIG_OVMS_SC_JAVASCRIPT_DUKTAPE

OvmsConfig::OvmsConfig()
  {
  ESP_LOGI(TAG, "Initialising CONFIG (1400)");

  m_mounted = false;

  OvmsCommand* cmd_store = MyCommandApp.RegisterCommand("store","STORE framework");
  cmd_store->RegisterCommand("mount","Mount STORE",store_mount);
  cmd_store->RegisterCommand("unmount","Unmount STORE",store_unmount);

  OvmsCommand* cmd_config = MyCommandApp.RegisterCommand("config","CONFIG framework");
  cmd_config->RegisterCommand("list","Show configuration parameters/instances",config_list,"[<param>]",0,1, true, config_validate);
  cmd_config->RegisterCommand("set","Set parameter:instance=value",config_set,"<param> <instance> <value>",3,3, true, config_validate);
  cmd_config->RegisterCommand("rm","Remove parameter:instance",config_rm,"<param> {<instance> | *}",2,2, true, config_validate);

#ifdef CONFIG_OVMS_SC_ZIP
  cmd_config->RegisterCommand("backup", "Backup to file", config_backup,
    "<zipfile> [password=module password]\n"
    "Backup system configuration & scripts into password protected ZIP file.\n"
    "Note: user files or directories in /store will not be included.\n"
    "<password> defaults to the current module password, set to \"\" to disable encryption.\n"
    "Hint: use 7z to unzip/create backup ZIPs on a PC.", 1, 2);
  cmd_config->RegisterCommand("restore", "Restore from file", config_restore,
    "<zipfile> [password=module password]\n"
    "Restore system configuration & scripts from password protected ZIP file.\n"
    "Note: user files or directories in /store will not be touched.\n"
    "The module will perform a reboot after successful restore.\n"
    "<password> defaults to the current module password.\n"
    "Note: You need to supply the password used for the backup creation.\n"
    "The default password is not available after flash is erased.", 1, 2);
#endif // CONFIG_OVMS_SC_ZIP

  RegisterParam("password", "Password store", true, false);
  RegisterParam("module", "Module configuration", true, true);
  RegisterParam("usr", "Custom plugin configuration", true, true);

  #ifdef CONFIG_OVMS_SC_JAVASCRIPT_DUKTAPE
  DuktapeObjectRegistration* dto = new DuktapeObjectRegistration("OvmsConfig");
  dto->RegisterDuktapeFunction(DukOvmsConfigParams, 0, "Params");
  dto->RegisterDuktapeFunction(DukOvmsConfigInstances, 1, "Instances");
  dto->RegisterDuktapeFunction(DukOvmsConfigGet, 3, "Get");
  dto->RegisterDuktapeFunction(DukOvmsConfigSet, 3, "Set");
  dto->RegisterDuktapeFunction(DukOvmsConfigDelete, 2, "Delete");
  dto->RegisterDuktapeFunction(DukOvmsConfigGetValues, 2, "GetValues");
  dto->RegisterDuktapeFunction(DukOvmsConfigSetValues, 3, "SetValues");
  MyDuktape.RegisterDuktapeObject(dto);
  #endif // #ifdef CONFIG_OVMS_SC_JAVASCRIPT_DUKTAPE
  }

OvmsConfig::~OvmsConfig()
  {
  }

esp_err_t OvmsConfig::mount()
  {
//  if (!spiffs_is_registered)
//    vfs_spiffs_register();

//  if (!spiffs_is_mounted)
//    spiffs_mount();

  if (m_mounted)
    return ESP_OK;

  memset(&m_store_fat,0,sizeof(esp_vfs_fat_sdmmc_mount_config_t));
  m_store_fat.format_if_mount_failed = true;
  m_store_fat.max_files = 5;
  esp_vfs_fat_spiflash_mount("/store", "store", &m_store_fat, &m_store_wlh);
  m_mounted = true;

  struct stat ds;
  if (stat(OVMS_CONFIGPATH, &ds) != 0)
    {
    ESP_LOGI(TAG, "Initialising OVMS CONFIG within STORE");
    mkdir(OVMS_CONFIGPATH,0);
    }

  DIR *dir;
  struct dirent *dp;
  if ((dir = opendir(OVMS_CONFIGPATH)) == NULL)
    {
    ESP_LOGE(TAG, "Error: Cannot open config store directory");
    return ESP_ERR_NOT_FOUND;
    }
  while ((dp = readdir(dir)) != NULL)
    {
    // Register the param in case this was not already done
    if (CachedParam(dp->d_name) == NULL)
      RegisterParam(dp->d_name, "", true, false);
    }
  closedir(dir);

  // load & upgrade params:
  for (ConfigMap::iterator it=MyConfig.m_map.begin(); it!=MyConfig.m_map.end(); ++it)
    {
    it->second->Load();
    }
  upgrade();

  MyEvents.SignalEvent("config.mounted", NULL);
  return ESP_OK;
  }

esp_err_t OvmsConfig::unmount()
  {
//  if (spiffs_is_mounted)
//    spiffs_unmount(0);

  if (m_mounted)
    {
    esp_vfs_fat_spiflash_unmount("/store", m_store_wlh);
    m_mounted = false;
    MyEvents.SignalEvent("config.unmounted", NULL);
    }

  return ESP_OK;
  }

bool OvmsConfig::ismounted()
  {
  return m_mounted;
  }

void OvmsConfig::upgrade()
  {
  // Migrate password/changed → module/init:
  if (GetParamValueBool("password", "changed") == true)
    {
    SetParamValue("module", "init", "done");
    DeleteInstance("password", "changed");
    }

  // Migrate vehicle.require.* signals to config:
  if (GetParamValueInt("module", "cfgversion") < 2018112200)
    {
    std::string vt = GetParamValue("auto", "vehicle.type");
    if (vt=="FT5E" || vt=="KS" || vt=="MI" || vt=="RT" || vt=="TGTC" || vt=="VA" || vt=="ZEVA")
      {
      SetParamValueBool("modem", "enable.gps", true);
      SetParamValueBool("modem", "enable.gpstime", true);
      }
    }

  // Migrate server.v2 password (from server.v2 to password)
  if (GetParamValueInt("module", "cfgversion") < 2020053100)
    {
    if (IsDefined("server.v2","password"))
      {
      std::string p = GetParamValue("server.v2", "password");
      SetParamValue("password","server.v2",p);
      DeleteInstance("server.v2","password");
      }
    }

  // Migrate vehicle IDs VWUP.T26/.OBD back to VWUP
  std::string vt = GetParamValue("auto", "vehicle.type");
  if (vt == "VWUP.T26" || vt == "VWUP.OBD")
    {
    SetParamValue("auto", "vehicle.type", "VWUP");
    }
  // Move obsolete VWUP "xut" instances to "xvu":
  if (CachedParam("xut"))
    {
    RegisterParam("xvu", "VW e-Up", true, true);
    for (const auto& instance : { "canwrite", "modelyear", "cc_temp" })
      {
      if (!IsDefined("xvu", instance) && IsDefined("xut", instance))
        SetParamValue("xvu", instance, GetParamValue("xut", instance));
      }
    DeregisterParam("xut");
    }
  // Remove obsolete VWUP "vwup" param:
  if (CachedParam("vwup"))
    {
    DeregisterParam("vwup");
    }

  // Done, set config version:
  SetParamValueInt("module", "cfgversion", 2020053100);
  }

void OvmsConfig::RegisterParam(std::string name, std::string title, bool writable, bool readable)
  {
  auto k = m_map.find(name);
  if (k == m_map.end())
    {
    OvmsConfigParam* p = new OvmsConfigParam(name, title, writable, readable);
    m_map[name] = p;
    }
  else
    {
    k->second->SetTitle(title);
    k->second->SetAccess(writable, readable);
    }
  }

void OvmsConfig::DeregisterParam(std::string name)
  {
  auto k = m_map.find(name);
  if (k != m_map.end())
    {
    k->second->DeleteParam();
    delete k->second;
    m_map.erase(k);
    }
  }

void OvmsConfig::SetParamValue(std::string param, std::string instance, std::string value)
  {
  OvmsConfigParam *p = CachedParam(param);
  if (p)
    {
    p->SetValue(instance,value);
    }
  }

void OvmsConfig::SetParamValueBinary(std::string param, std::string instance, std::string value, BinaryEncoding_t encoding /*=Encoding_HEX*/)
  {
  std::string encval;
  if (encoding == Encoding_BASE64)
    {
    encval = base64encode(value);
    }
  else // Encoding_HEX
    {
    encval = hexencode(value);
    }
  SetParamValue(param, instance, encval);
  }

void OvmsConfig::SetParamValueInt(std::string param, std::string instance, int value)
  {
  std::ostringstream ss;
  ss << value;
  SetParamValue(param, instance, std::string(ss.str()));
  }

void OvmsConfig::SetParamValueFloat(std::string param, std::string instance, float value)
  {
  std::ostringstream ss;
  ss << value;
  SetParamValue(param, instance, std::string(ss.str()));
  }

void OvmsConfig::SetParamValueBool(std::string param, std::string instance, bool value)
  {
  SetParamValue(param, instance, std::string(value ? "yes" : "no"));
  }

void OvmsConfig::DeleteInstance(std::string param, std::string instance)
  {
  OvmsConfigParam *p = CachedParam(param);
  if (p)
    {
    p->DeleteInstance(instance);
    }
  }

std::string OvmsConfig::GetParamValue(std::string param, std::string instance, std::string defvalue)
  {
  OvmsConfigParam *p = CachedParam(param);
  if (p && p->IsDefined(instance))
    {
    return p->GetValue(instance);
    }
  else
    {
    return defvalue;
    }
  }

std::string OvmsConfig::GetParamValueBinary(std::string param, std::string instance, std::string defvalue, BinaryEncoding_t encoding /*=Encoding_HEX*/)
  {
  std::string encval = GetParamValue(param,instance);
  size_t len = encval.length();
  if (len == 0) return defvalue;
  if (encoding == Encoding_BASE64)
    {
    return base64decode(encval);
    }
  else // Encoding_HEX
    {
    std::string value = hexdecode(encval);
    if (value.empty())
      {
      ESP_LOGE(TAG, "Invalid non-hex value for config param %s instance %s",
        param.c_str(), instance.c_str());
      return defvalue;
      }
    return value;
    }
  }

int OvmsConfig::GetParamValueInt(std::string param, std::string instance, int defvalue)
  {
  std::string value = GetParamValue(param,instance);
  if (value.length() == 0) return defvalue;
  return atoi(value.c_str());
  }

float OvmsConfig::GetParamValueFloat(std::string param, std::string instance, float defvalue)
  {
  std::string value = GetParamValue(param,instance);
  if (value.length() == 0) return defvalue;
  return atof(value.c_str());
  }

bool OvmsConfig::GetParamValueBool(std::string param, std::string instance, bool defvalue)
  {
  std::string value = GetParamValue(param,instance);
  if (value.length() == 0) return defvalue;
  return strtobool(value);
  }

bool OvmsConfig::IsDefined(std::string param, std::string instance)
  {
  OvmsConfigParam *p = CachedParam(param);
  if (p == NULL) return false;
  return p->IsDefined(instance);
  }

OvmsConfigParam* OvmsConfig::CachedParam(std::string param)
  {
  if (!m_mounted) return NULL;
  OvmsConfigParam* const* p = m_map.FindUniquePrefix(param.c_str());
  if (!p)
    return NULL;
  return *p;
  }

bool OvmsConfig::ProtectedPath(std::string path)
  {
#ifdef CONFIG_OVMS_DEV_CONFIGVFS
  return false;
#else
  return (path.find(OVMS_CONFIGPATH) != std::string::npos);
#endif // #ifdef CONFIG_OVMS_DEV_CONFIGVFS
  }

/**
 * GetParamMap: get map (copy) of param instances
 */
ConfigParamMap OvmsConfig::GetParamMap(std::string param)
  {
  ConfigParamMap pmap;
  if (!CachedParam(param))
    RegisterParam(param, "", true, false);
  OvmsConfigParam* p = CachedParam(param);
  if (p)
    pmap = p->GetMap();
  return pmap;
  }

/**
 * SetParamMap: replace all param instances
 * - Note: items will be removed from source map, map is empty afterwards
 */
void OvmsConfig::SetParamMap(std::string param, ConfigParamMap& map)
  {
  if (!CachedParam(param))
    RegisterParam(param, "", true, false);
  OvmsConfigParam* p = CachedParam(param);
  if (p)
    p->SetMap(map);
  }

#ifdef CONFIG_OVMS_SC_ZIP

/**
 * Backup:
 */

static struct
  {
  const char* name;
  bool optional;
  }
  backup_dir[] =
  {
    { "ovms_config", false },
    { "events", true },
    { "scripts", true },
    { "obd2ecu", true },
    { "dbc", true },
    { "plugin", true },
    { "tls", true },
    { "trustedca", true },
    { "usr", true },
    { NULL, false }
  };

bool OvmsConfig::Backup(std::string path, std::string password, OvmsWriter* writer /*=NULL*/, int verbosity /*=1024*/)
  {
  if (writer)
    writer->printf("Creating config backup '%s'...\n", path.c_str());
  else
    ESP_LOGD(TAG, "Backup: creating '%s'...", path.c_str());

  OvmsMutexLock store_lock(&m_store_lock);
  bool ok = true;

  ZipArchive zip(path, password, ZIP_CREATE|ZIP_TRUNCATE);
  if (ok) ok = zip.chdir("/store");
  for (int i = 0; ok && backup_dir[i].name; i++)
    {
    if (writer && verbosity >= COMMAND_RESULT_NORMAL)
      writer->printf("..add '%s'\n", backup_dir[i].name);
    else if (!writer)
      ESP_LOGD(TAG, "Backup '%s': add '%s'", path.c_str(), backup_dir[i].name);
    ok = zip.add(backup_dir[i].name, backup_dir[i].optional);
    }
  if (ok) ok = zip.close();

  if (!ok)
    {
    if (writer)
      writer->printf("Error: zip failed: %s\n", zip.strerror());
    else
      ESP_LOGE(TAG, "Backup '%s': zip failed: %s", path.c_str(), zip.strerror());
    }
  else
    {
    if (writer)
      writer->puts("Done.");
    else
      ESP_LOGI(TAG, "Backup '%s' done", path.c_str());
    }

  return ok;
  }

/**
 * Restore:
 */

static bool install_dir(std::string src, std::string dst)
  {
  if (!path_exists(src))
    {
    rmtree(dst);
    return true;
    }
  std::string dstbak = dst + ".old";
  rmtree(dstbak);
  if (mkpath(dst) != 0) // ensure dst exists
    return false;
  if (rename(dst.c_str(), dstbak.c_str()) == 0)
    {
    if (rename(src.c_str(), dst.c_str()) == 0)
      {
      rmtree(dstbak);
      return true;
      }
    rename(dstbak.c_str(), dst.c_str());
    }
  return false;
  }

bool OvmsConfig::Restore(std::string path, std::string password, OvmsWriter* writer /*=NULL*/, int verbosity /*=1024*/)
  {
  if (writer)
    writer->printf("Restoring config from '%s'...\n", path.c_str());
  else
    ESP_LOGD(TAG, "Restore: reading '%s'...", path.c_str());

  // Lock config store:
  if (!m_store_lock.Lock(pdMS_TO_TICKS(5000)))
    {
    if (writer)
      writer->puts("Error: config store currently in use by another process");
    else
      ESP_LOGE(TAG, "Restore: timeout waiting for config lock");
    return false;
    }

  // Signal & wait for components to shutdown:
    {
    OvmsSemaphore eventdone;
    auto callback = [](const char* event, void* data) { ((OvmsSemaphore*)data)->Give(); };
    MyEvents.SignalEvent("config.restore", &eventdone, callback);
    eventdone.Take();
    }

  bool ok = true;

  // unzip into restore directory:
  // (Note: all paths beginning with "/store/ovms_config" are protected)
  std::string tempdir = "/store/ovms_config_restore";

  if (rmtree(tempdir) != 0 || mkpath(tempdir) != 0)
    {
    if (writer)
      writer->printf("Error: prepare failed: %s\n", strerror(errno));
    else
      ESP_LOGE(TAG, "Restore '%s': prepare failed: %s", path.c_str(), strerror(errno));
    m_store_lock.Unlock();
    return false;
    }

  ZipArchive zip(path, password, ZIP_RDONLY);
  if (ok) ok = zip.chdir(tempdir);
  for (int i = 0; ok && backup_dir[i].name; i++)
    {
    if (writer && verbosity >= COMMAND_RESULT_NORMAL)
      writer->printf("..extract '%s'\n", backup_dir[i].name);
    else if (!writer)
      ESP_LOGD(TAG, "Restore '%s': extract '%s'", path.c_str(), backup_dir[i].name);
    ok = zip.extract(backup_dir[i].name, backup_dir[i].optional);
    }
  if (ok) ok = zip.close();

  if (!ok)
    {
    if (writer)
      writer->printf("Error: unzip failed: %s%s\n", zip.strerror(),
        password.empty() ? " (password required?)" : "");
    else
      ESP_LOGE(TAG, "Restore '%s': unzip failed: %s%s", path.c_str(), zip.strerror(),
        password.empty() ? " (password required?)" : "");
    rmtree(tempdir);
    m_store_lock.Unlock();
    return false;
    }

  // replace config by restored version:

  if (writer)
    writer->puts("Installing...");
  else
    ESP_LOGD(TAG, "Restore '%s': installing...", path.c_str());

  std::string dstbase = "/store/";
  for (int i = 0; backup_dir[i].name; i++)
    {
    if (writer && verbosity >= COMMAND_RESULT_NORMAL)
      writer->printf("..install '%s'\n", backup_dir[i].name);
    else if (!writer)
      ESP_LOGD(TAG, "Restore '%s':  install '%s'", path.c_str(), backup_dir[i].name);
    if (!install_dir(tempdir + "/" + backup_dir[i].name, dstbase + backup_dir[i].name))
      {
      ok = false;
      if (!backup_dir[i].optional)
        {
        if (writer)
          writer->printf("Error: install '%s' failed: %s\n", backup_dir[i].name, strerror(errno));
        else
          ESP_LOGE(TAG, "Restore '%s': install '%s' failed: %s", path.c_str(), backup_dir[i].name, strerror(errno));
        break;
        }
      else
        {
        if (writer)
          writer->printf("Warning: install '%s' failed: %s\n", backup_dir[i].name, strerror(errno));
        else
          ESP_LOGW(TAG, "Restore '%s': install '%s' failed: %s", path.c_str(), backup_dir[i].name, strerror(errno));
        }
      }
    }

  // cleanup & reboot:

  rmtree(tempdir);

  if (!ok)
    {
    m_store_lock.Unlock();
    return false;
    }

  if (writer)
    writer->puts("Done, rebooting now...");
  else
    ESP_LOGI(TAG, "Restore '%s': done, rebooting...", path.c_str());

  vTaskDelay(1000/portTICK_PERIOD_MS);
  MyBoot.Restart();

  return true;
  }

#endif // CONFIG_OVMS_SC_ZIP

void OvmsConfig::SupportSummary(OvmsWriter* writer)
  {
  writer->puts("\nConfiguration");

  for (ConfigMap::iterator mi=m_map.begin(); mi!=m_map.end(); ++mi)
    {
    writer->printf("  [%s]\n",mi->first.c_str());
    OvmsConfigParam* p = mi->second;
    for (ConfigParamMap::iterator it=p->m_map.begin(); it!=p->m_map.end(); ++it)
      {
      if (p->Readable())
        { writer->printf("    %s: %s\n",it->first.c_str(), it->second.c_str()); }
      else
        { writer->printf("    %s: **redacted**\n",it->first.c_str()); }
      }
    }
  }

OvmsConfigParam::OvmsConfigParam(std::string name, std::string title, bool writable, bool readable)
  {
  m_name = name;
  m_title = title;
  m_writable = writable;
  m_readable = readable;
  m_loaded = false;

  if (MyConfig.ismounted())
    {
    LoadConfig();
    }
  }

OvmsConfigParam::~OvmsConfigParam()
  {
  }

void OvmsConfigParam::LoadConfig()
  {
  if (m_loaded) return;  // Protected against loading more than once

  OvmsMutexLock store_lock(&MyConfig.m_store_lock);

  std::string path(OVMS_CONFIGPATH);
  path.append("/");
  path.append(m_name);
  // ESP_LOGI(TAG, "Trying %s",path.c_str());
  FILE* f = fopen(path.c_str(), "r");
  if (f)
    {
    char* buf = new char[OVMS_MAXVALSIZE];
    while (fgets(buf, OVMS_MAXVALSIZE, f))
      {
      buf[strlen(buf)-1] = 0; // Remove trailing newline
#ifdef OVMS_PERSIST_METADATA
      // check for meta data:
      if (buf[0] == '#')
        {
        if (strncmp(buf, "#access=", 8) == 0)
          {
          m_readable = (strchr(buf+8, 'r') != NULL);
          m_writable = (strchr(buf+8, 'w') != NULL);
          continue;
          }
        else if (strncmp(buf, "#title=", 7) == 0)
          {
          m_title = buf+7;
          continue;
          }
        }
#endif // OVMS_PERSIST_METADATA
      // read instance:
      char *p = index(buf,char(9));
      if (p == NULL) p = index(buf,' ');
      if (p)
        {
        *p = 0; // Null terminate the key
        p++;    // and point to the value
        m_map[std::string(buf)] = std::string(p);
        // ESP_LOGI(TAG, "Loaded %s/%s=%s", m_name.c_str(), buf, p);
        }
      }
    delete[] buf;
    fclose(f);
    }
  m_loaded = true;
  }

void OvmsConfigParam::SetValue(std::string instance, std::string value)
  {
  if (m_map.find(instance) == m_map.end() || m_map[instance] != value)
    {
    m_map[instance] = value;
    RewriteConfig();
    MyEvents.SignalEvent("config.changed", this);
    }
  }

void OvmsConfigParam::DeleteParam()
  {
  OvmsMutexLock store_lock(&MyConfig.m_store_lock);

  std::string path(OVMS_CONFIGPATH);
  path.append("/");
  path.append(m_name);
  unlink(path.c_str());
  MyEvents.SignalEvent("config.changed", this);
  }

bool OvmsConfigParam::DeleteInstance(std::string instance)
  {
  bool ret = false;
  auto k = m_map.find(instance);
  if (k != m_map.end())
    {
    m_map.erase(k);
    RewriteConfig();
    ret = true;
    }
  MyEvents.SignalEvent("config.changed", this);
  return ret;
  }

std::string OvmsConfigParam::GetValue(std::string instance)
  {
  auto k = m_map.find(instance);
  if (k == m_map.end())
    return std::string("");
  else
    return k->second;
  }

bool OvmsConfigParam::IsDefined(std::string instance)
  {
  if (instance.empty())
    return !m_map.empty();
  auto k = m_map.find(instance);
  if (k == m_map.end())
    return false;
  else
    return true;
  }

bool OvmsConfigParam::Writable()
  {
  return m_writable;
  }

bool OvmsConfigParam::Readable()
  {
  return m_readable;
  }

void OvmsConfigParam::SetAccess(bool writable, bool readable)
  {
  m_writable = writable;
  m_readable = readable;
  }

std::string OvmsConfigParam::GetName()
  {
  return m_name;
  }

void OvmsConfigParam::RewriteConfig()
  {
  OvmsMutexLock store_lock(&MyConfig.m_store_lock);

  std::string path(OVMS_CONFIGPATH);
  path.append("/");
  path.append(m_name);
  FILE* f = fopen(path.c_str(), "w");
  if (!f)
    ESP_LOGE(TAG, "RewriteConfig: can't open '%s': %s", path.c_str(), strerror(errno));
  else
    {
#ifdef OVMS_PERSIST_METADATA
    // write meta data:
    fprintf(f, "#access=%s%s\n", m_readable ? "r" : "", m_writable ? "w" : "");
    fprintf(f, "#title=%s\n", m_title.c_str());
#endif
    // write instances:
    for (ConfigParamMap::iterator it=m_map.begin(); it!=m_map.end(); ++it)
      {
      fprintf(f,"%s\t%s\n",it->first.c_str(),it->second.c_str());
      }
    if (fclose(f))
      ESP_LOGE(TAG, "RewriteConfig: error writing '%s': %s", path.c_str(), strerror(errno));
    }
  }

void OvmsConfigParam::Load()
  {
  if (!m_loaded) LoadConfig();
  }

void OvmsConfigParam::Save()
  {
  if (m_name != "")
    {
    RewriteConfig();
    MyEvents.SignalEvent("config.changed", this);
    }
  }

/**
 * SetMap: replace all param instances
 * - Note: items will be removed from source map, map is empty afterwards
 */
void OvmsConfigParam::SetMap(ConfigParamMap& map)
  {
  m_map.clear();
  m_map = std::move(map);
  Save();
  }
