/* Copyright (C) 2002-2005 RealVNC Ltd.  All Rights Reserved.
 * Copyright 2004-2005 Cendio AB.
 * Copyright 2017 Peter Astrand <astrand@cendio.se> for Cendio AB
 * 
 * This 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 2 of the License, or
 * (at your option) any later version.
 * 
 * This software 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 software; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307,
 * USA.
 */

// -=- Configuration.cxx

#include <stdlib.h>
#include <ctype.h>
#include <string.h>

#include <os/Mutex.h>

#include <rfb/util.h>
#include <rfb/Configuration.h>
#include <rfb/LogWriter.h>
#include <rfb/Exception.h>

#define LOCK_CONFIG os::AutoMutex a(mutex)

#include <rdr/HexOutStream.h>
#include <rdr/HexInStream.h>

using namespace rfb;

static LogWriter vlog("Config");


// -=- The Global/server/viewer Configuration objects
Configuration* Configuration::global_ = 0;
Configuration* Configuration::server_ = 0;
Configuration* Configuration::viewer_ = 0;

Configuration* Configuration::global() {
  if (!global_)
    global_ = new Configuration("Global");
  return global_;
}

Configuration* Configuration::server() {
  if (!server_)
    server_ = new Configuration("Server");
  return server_;
}

Configuration* Configuration::viewer() {
  if (!viewer_)
    viewer_ = new Configuration("Viewer");
  return viewer_;
}

// -=- Configuration implementation

bool Configuration::set(const char* n, const char* v, bool immutable) {
  return set(n, strlen(n), v, immutable);
}

bool Configuration::set(const char* name, int len,
                             const char* val, bool immutable)
{
  VoidParameter* current = head;
  while (current) {
    if ((int)strlen(current->getName()) == len &&
        strncasecmp(current->getName(), name, len) == 0)
    {
      bool b = current->setParam(val);
      if (b && immutable) 
	current->setImmutable();
      return b;
    }
    current = current->_next;
  }
  return _next ? _next->set(name, len, val, immutable) : false;
}

bool Configuration::set(const char* config, bool immutable) {
  bool hyphen = false;
  if (config[0] == '-') {
    hyphen = true;
    config++;
    if (config[0] == '-') config++; // allow gnu-style --<option>
  }
  const char* equal = strchr(config, '=');
  if (equal) {
    return set(config, equal-config, equal+1, immutable);
  } else if (hyphen) {
    VoidParameter* current = head;
    while (current) {
      if (strcasecmp(current->getName(), config) == 0) {
        bool b = current->setParam();
        if (b && immutable) 
	  current->setImmutable();
        return b;
      }
      current = current->_next;
    }
  }    
  return _next ? _next->set(config, immutable) : false;
}

VoidParameter* Configuration::get(const char* param)
{
  VoidParameter* current = head;
  while (current) {
    if (strcasecmp(current->getName(), param) == 0)
      return current;
    current = current->_next;
  }
  return _next ? _next->get(param) : 0;
}

void Configuration::list(int width, int nameWidth) {
  VoidParameter* current = head;

  fprintf(stderr, "%s Parameters:\n", name.buf);
  while (current) {
    char* def_str = current->getDefaultStr();
    const char* desc = current->getDescription();
    fprintf(stderr,"  %-*s -", nameWidth, current->getName());
    int column = strlen(current->getName());
    if (column < nameWidth) column = nameWidth;
    column += 4;
    while (true) {
      const char* s = strchr(desc, ' ');
      int wordLen;
      if (s) wordLen = s-desc;
      else wordLen = strlen(desc);

      if (column + wordLen + 1 > width) {
        fprintf(stderr,"\n%*s",nameWidth+4,"");
        column = nameWidth+4;
      }
      fprintf(stderr," %.*s",wordLen,desc);
      column += wordLen + 1;
      desc += wordLen + 1;
      if (!s) break;
    }

    if (def_str) {
      if (column + (int)strlen(def_str) + 11 > width)
        fprintf(stderr,"\n%*s",nameWidth+4,"");
      fprintf(stderr," (default=%s)\n",def_str);
      strFree(def_str);
    } else {
      fprintf(stderr,"\n");
    }
    current = current->_next;
  }

  if (_next)
    _next->list(width, nameWidth);
}


bool Configuration::remove(const char* param) {
  VoidParameter *current = head;
  VoidParameter **prevnext = &head;

  while (current) {
    if (strcasecmp(current->getName(), param) == 0) {
      *prevnext = current->_next;
      return true;
    }
    prevnext = &current->_next;
    current = current->_next;
  }

  return false;
}


// -=- VoidParameter

VoidParameter::VoidParameter(const char* name_, const char* desc_,
			     ConfigurationObject co)
  : immutable(false), name(name_), description(desc_)
{
  Configuration *conf = NULL;

  switch (co) {
  case ConfGlobal: conf = Configuration::global();
    break;
  case ConfServer: conf = Configuration::server();
    break;
  case ConfViewer: conf = Configuration::viewer();
    break;
  }

  _next = conf->head;
  conf->head = this;

  mutex = new os::Mutex();
}

VoidParameter::~VoidParameter() {
  delete mutex;
}

const char*
VoidParameter::getName() const {
  return name;
}

const char*
VoidParameter::getDescription() const {
  return description;
}

bool VoidParameter::setParam() {
  return false;
}

bool VoidParameter::isBool() const {
  return false;
}

void
VoidParameter::setImmutable() {
  vlog.debug("set immutable %s", getName());
  immutable = true;
}

// -=- AliasParameter

AliasParameter::AliasParameter(const char* name_, const char* desc_,
                               VoidParameter* param_, ConfigurationObject co)
  : VoidParameter(name_, desc_, co), param(param_) {
}

bool
AliasParameter::setParam(const char* v) {
  return param->setParam(v);
}

bool AliasParameter::setParam() {
  return param->setParam();
}

char*
AliasParameter::getDefaultStr() const {
  return 0;
}

char* AliasParameter::getValueStr() const {
  return param->getValueStr();
}

bool AliasParameter::isBool() const {
  return param->isBool();
}

void
AliasParameter::setImmutable() {
  vlog.debug("set immutable %s (Alias)", getName());
  param->setImmutable();
}


// -=- BoolParameter

BoolParameter::BoolParameter(const char* name_, const char* desc_, bool v,
			     ConfigurationObject co)
: VoidParameter(name_, desc_, co), value(v), def_value(v) {
}

bool
BoolParameter::setParam(const char* v) {
  if (immutable) return true;

  if (*v == 0 || strcasecmp(v, "1") == 0 || strcasecmp(v, "on") == 0
      || strcasecmp(v, "true") == 0 || strcasecmp(v, "yes") == 0)
    value = 1;
  else if (strcasecmp(v, "0") == 0 || strcasecmp(v, "off") == 0
           || strcasecmp(v, "false") == 0 || strcasecmp(v, "no") == 0)
    value = 0;
  else {
    vlog.error("Bool parameter %s: invalid value '%s'", getName(), v);
    return false;
  }

  vlog.debug("set %s(Bool) to %s(%d)", getName(), v, value);
  return true;
}

bool BoolParameter::setParam() {
  setParam(true);
  return true;
}

void BoolParameter::setParam(bool b) {
  if (immutable) return;
  value = b;
  vlog.debug("set %s(Bool) to %d", getName(), value);
}

char*
BoolParameter::getDefaultStr() const {
  return strDup(def_value ? "1" : "0");
}

char* BoolParameter::getValueStr() const {
  return strDup(value ? "1" : "0");
}

bool BoolParameter::isBool() const {
  return true;
}

BoolParameter::operator bool() const {
  return value;
}

// -=- IntParameter

IntParameter::IntParameter(const char* name_, const char* desc_, int v,
                           int minValue_, int maxValue_, ConfigurationObject co)
  : VoidParameter(name_, desc_, co), value(v), def_value(v),
    minValue(minValue_), maxValue(maxValue_)
{
}

bool
IntParameter::setParam(const char* v) {
  if (immutable) return true;
  vlog.debug("set %s(Int) to %s", getName(), v);
  int i = strtol(v, NULL, 0);
  if (i < minValue || i > maxValue)
    return false;
  value = i;
  return true;
}

bool
IntParameter::setParam(int v) {
  if (immutable) return true;
  vlog.debug("set %s(Int) to %d", getName(), v);
  if (v < minValue || v > maxValue)
    return false;
  value = v;
  return true;
}

char*
IntParameter::getDefaultStr() const {
  char* result = new char[16];
  sprintf(result, "%d", def_value);
  return result;
}

char* IntParameter::getValueStr() const {
  char* result = new char[16];
  sprintf(result, "%d", value);
  return result;
}

IntParameter::operator int() const {
  return value;
}

// -=- StringParameter

StringParameter::StringParameter(const char* name_, const char* desc_,
                                 const char* v, ConfigurationObject co)
  : VoidParameter(name_, desc_, co), value(strDup(v)), def_value(strDup(v))
{
  if (!v) {
    vlog.error("Default value <null> for %s not allowed",name_);
    throw rfb::Exception("Default value <null> not allowed");
  }
}

StringParameter::~StringParameter() {
  strFree(value);
  strFree(def_value);
}

bool StringParameter::setParam(const char* v) {
  LOCK_CONFIG;
  if (immutable) return true;
  if (!v)
    throw rfb::Exception("setParam(<null>) not allowed");
  vlog.debug("set %s(String) to %s", getName(), v);
  CharArray oldValue(value);
  value = strDup(v);
  return value != 0;
}

char* StringParameter::getDefaultStr() const {
  return strDup(def_value);
}

char* StringParameter::getValueStr() const {
  LOCK_CONFIG;
  return strDup(value);
}

StringParameter::operator const char *() const {
  return value;
}

// -=- BinaryParameter

BinaryParameter::BinaryParameter(const char* name_, const char* desc_,
				 const void* v, size_t l, ConfigurationObject co)
: VoidParameter(name_, desc_, co), value(0), length(0), def_value(0), def_length(0) {
  if (l) {
    value = new char[l];
    length = l;
    memcpy(value, v, l);
    def_value = new char[l];
    def_length = l;
    memcpy(def_value, v, l);
  }
}
BinaryParameter::~BinaryParameter() {
  delete [] value;
  delete [] def_value;
}

bool BinaryParameter::setParam(const char* v) {
  LOCK_CONFIG;
  if (immutable) return true;
  vlog.debug("set %s(Binary) to %s", getName(), v);
  return rdr::HexInStream::hexStrToBin(v, &value, &length);
}

void BinaryParameter::setParam(const void* v, size_t len) {
  LOCK_CONFIG;
  if (immutable) return; 
  vlog.debug("set %s(Binary)", getName());
  delete [] value; value = 0;
  if (len) {
    value = new char[len];
    length = len;
    memcpy(value, v, len);
  }
}

char* BinaryParameter::getDefaultStr() const {
  return rdr::HexOutStream::binToHexStr(def_value, def_length);
}

char* BinaryParameter::getValueStr() const {
  LOCK_CONFIG;
  return rdr::HexOutStream::binToHexStr(value, length);
}

void BinaryParameter::getData(void** data_, size_t* length_) const {
  LOCK_CONFIG;
  if (length_) *length_ = length;
  if (data_) {
    *data_ = new char[length];
    memcpy(*data_, value, length);
  }
}