/* The copyright in this software is being made available under the BSD
 * License, included below. This software may be subject to other third party
 * and contributor rights, including patent rights, and no such rights are
 * granted under this license.
 *
 * Copyright (c) 2010-2023, ITU/ISO/IEC
 * 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 ITU/ISO/IEC 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 HOLDER OR CONTRIBUTORS
 * 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.
 */
#include <stdlib.h>
#include <iostream>
#include <fstream>
#include <sstream>
#include <string>
#include <list>
#include <map>
#include <algorithm>
#include "program_options_lite.h"

namespace ProgramOptionsLite
{
ErrorReporter default_error_reporter;

std::ostream& ErrorReporter::error(const std::string& where)
{
  is_errored = 1;
  std::cerr << where << " error: ";
  return std::cerr;
}

std::ostream& ErrorReporter::warn(const std::string& where)
{
  std::cerr << where << " warning: ";
  return std::cerr;
}

Options::~Options()
{
  for (Options::NamesPtrList::iterator it = opt_list.begin(); it != opt_list.end(); it++)
  {
    delete *it;
  }
}

void Options::addOption(OptionBase* opt)
{
  Names* names            = new Names();
  names->opt              = opt;
  std::string& opt_string = opt->opt_string;

  size_t opt_start = 0;
  for (size_t opt_end = 0; opt_end != std::string::npos;)
  {
    opt_end          = opt_string.find_first_of(',', opt_start);
    bool force_short = 0;
    if (opt_string[opt_start] == '-')
    {
      opt_start++;
      force_short = 1;
    }
    std::string opt_name = opt_string.substr(opt_start, opt_end - opt_start);
    if (force_short || opt_name.size() == 1)
    {
      names->opt_short.push_back(opt_name);
      opt_short_map[opt_name].push_back(names);
    }
    else
    {
      if (opt_name.size() > 0 && opt_name.back() == '*')
      {
        std::string prefix_name = opt_name.substr(0, opt_name.size() - 1);
        names->opt_prefix.push_back(prefix_name);
        opt_prefix_map[prefix_name].push_back(names);
      }
      else
      {
        names->opt_long.push_back(opt_name);
        opt_long_map[opt_name].push_back(names);
      }
    }
    opt_start += opt_end + 1;
  }
  opt_list.push_back(names);
}

/* Helper method to initiate adding options to Options */
OptionSpecific Options::addOptions() { return OptionSpecific(*this); }

static void setOptions(Options::NamesPtrList& opt_list, const std::string& value, ErrorReporter& error_reporter)
{
  /* multiple options may be registered for the same name:
   *   allow each to parse value */
  for (Options::NamesPtrList::iterator it = opt_list.begin(); it != opt_list.end(); ++it)
  {
    (*it)->opt->parse(value, error_reporter);
  }
}

static const char spaces[41] = "                                        ";

/* format help text for a single option:
 * using the formatting: "-x, --long",
 * if a short/long option isn't specified, it is not printed
 */
static void doHelpOpt(std::ostream& out, const Options::Names& entry, unsigned pad_short = 0)
{
  pad_short = std::min(pad_short, 8u);

  if (!entry.opt_short.empty())
  {
    unsigned pad = std::max((int) pad_short - (int) entry.opt_short.front().size(), 0);
    out << "-" << entry.opt_short.front();
    if (!entry.opt_long.empty())
    {
      out << ", ";
    }
    out << &(spaces[40 - pad]);
  }
  else
  {
    out << "   ";
    out << &(spaces[40 - pad_short]);
  }

  if (!entry.opt_long.empty())
  {
    out << "--" << entry.opt_long.front();
  }
  else if (!entry.opt_prefix.empty())
  {
    out << "--" << entry.opt_prefix.front() << "*";
  }
}

/* format the help text */
void doHelp(std::ostream& out, Options& opts, unsigned columns)
{
  const unsigned pad_short = 3;
  /* first pass: work out the longest option name */
  unsigned       max_width = 0;
  for (Options::NamesPtrList::iterator it = opts.opt_list.begin(); it != opts.opt_list.end(); it++)
  {
    std::ostringstream line(std::ios_base::out);
    doHelpOpt(line, **it, pad_short);
    max_width = std::max(max_width, (unsigned) line.tellp());
  }

  unsigned opt_width  = std::min(max_width + 2, 28u + pad_short) + 2;
  unsigned desc_width = columns - opt_width;

  /* second pass: write out formatted option and help text.
   *  - align start of help text to start at opt_width
   *  - if the option text is longer than opt_width, place the help
   *    text at opt_width on the next line.
   */
  for (Options::NamesPtrList::iterator it = opts.opt_list.begin(); it != opts.opt_list.end(); it++)
  {
    std::ostringstream line(std::ios_base::out);
    line << "  ";
    doHelpOpt(line, **it, pad_short);

    const std::string& opt_desc = (*it)->opt->opt_desc;
    if (opt_desc.empty())
    {
      /* no help text: output option, skip further processing */
      std::cout << line.str() << std::endl;
      continue;
    }
    size_t currlength = size_t(line.tellp());
    if (currlength > opt_width)
    {
      /* if option text is too long (and would collide with the
       * help text, split onto next line */
      line << std::endl;
      currlength = 0;
    }
    /* split up the help text, taking into account new lines,
     *   (add opt_width of padding to each new line) */
    for (size_t newline_pos = 0, cur_pos = 0; cur_pos != std::string::npos; currlength = 0)
    {
      /* print any required padding space for vertical alignment */
      line << &(spaces[40 - opt_width + currlength]);
      newline_pos = opt_desc.find_first_of('\n', newline_pos);
      if (newline_pos != std::string::npos)
      {
        /* newline found, print substring (newline needn't be stripped) */
        newline_pos++;
        line << opt_desc.substr(cur_pos, newline_pos - cur_pos);
        cur_pos = newline_pos;
        continue;
      }
      if (cur_pos + desc_width > opt_desc.size())
      {
        /* no need to wrap text, remainder is less than avaliable width */
        line << opt_desc.substr(cur_pos);
        break;
      }
      /* find a suitable point to split text (avoid spliting in middle of word) */
      size_t split_pos = opt_desc.find_last_of(' ', cur_pos + desc_width);
      if (split_pos != std::string::npos)
      {
        /* eat up multiple space characters */
        split_pos = opt_desc.find_last_not_of(' ', split_pos) + 1;
      }

      /* bad split if no suitable space to split at.  fall back to width */
      bool bad_split = split_pos == std::string::npos || split_pos <= cur_pos;
      if (bad_split)
      {
        split_pos = cur_pos + desc_width;
      }
      line << opt_desc.substr(cur_pos, split_pos - cur_pos);

      /* eat up any space for the start of the next line */
      if (!bad_split)
      {
        split_pos = opt_desc.find_first_not_of(' ', split_pos);
      }
      cur_pos = newline_pos = split_pos;

      if (cur_pos >= opt_desc.size())
      {
        break;
      }
      line << std::endl;
    }

    std::cout << line.str() << std::endl;
  }
}

struct OptionWriter
{
  OptionWriter(Options& rOpts, ErrorReporter& err) : opts(rOpts), error_reporter(err) {}
  virtual ~OptionWriter() {}

  virtual const std::string where() = 0;

  bool storePair(bool allow_long, bool allow_short, const std::string& name, const std::string& value);
  bool storePair(const std::string& name, const std::string& value) { return storePair(true, true, name, value); }

  Options&       opts;
  ErrorReporter& error_reporter;
};

bool OptionWriter::storePair(bool allow_long, bool allow_short, const std::string& name, const std::string& value)
{
  bool                        found = false;
  std::string                 val   = value;
  Options::NamesMap::iterator opt_it;
  if (allow_long)
  {
    opt_it = opts.opt_long_map.find(name);
    if (opt_it != opts.opt_long_map.end())
    {
      found = true;
    }
  }

  /* check for the short list */
  if (allow_short && !(found && allow_long))
  {
    opt_it = opts.opt_short_map.find(name);
    if (opt_it != opts.opt_short_map.end())
    {
      found = true;
    }
  }
  bool allow_prefix = allow_long;
  if (allow_prefix && !found)
  {
    for (opt_it = opts.opt_prefix_map.begin(); opt_it != opts.opt_prefix_map.end(); opt_it++)
    {
      std::string name_prefix = name.substr(0, opt_it->first.size());
      if (name_prefix == opt_it->first)
      {
        // prepend value matching *
        val   = name.substr(name_prefix.size()) + std::string(" ") + val;
        found = true;
        break;
      }
    }
  }
  if (!found)
  {
    error_reporter.error(where()) << "Unknown option `" << name << "' (value:`" << value << "')\n";
    return false;
  }
  setOptions((*opt_it).second, val, error_reporter);
  return true;
}

struct ArgvParser : public OptionWriter
{
  ArgvParser(Options& rOpts, ErrorReporter& rError_reporter) : OptionWriter(rOpts, rError_reporter) {}

  const std::string where() { return "command line"; }

  unsigned parseGNU(unsigned argc, const char* argv[]);
  unsigned parseSHORT(unsigned argc, const char* argv[]);
};

/**
 * returns number of extra arguments consumed
 */
unsigned ArgvParser::parseGNU(unsigned argc, const char* argv[])
{
  /* gnu style long options can take the forms:
   *  --option=arg
   *  --option arg
   */
  std::string arg(argv[0]);
  size_t      arg_opt_start = arg.find_first_not_of('-');
  size_t      arg_opt_sep   = arg.find_first_of('=');
  std::string option        = arg.substr(arg_opt_start, arg_opt_sep - arg_opt_start);

  unsigned extra_argc_consumed = 0;
  if (arg_opt_sep == std::string::npos)
  {
    /* no argument found => argument in argv[1] (maybe) */
    /* xxx, need to handle case where option isn't required */
    if (!storePair(true, false, option, "1"))
    {
      return 0;
    }
  }
  else
  {
    /* argument occurs after option_sep */
    std::string val = arg.substr(arg_opt_sep + 1);
    storePair(true, false, option, val);
  }

  return extra_argc_consumed;
}

unsigned ArgvParser::parseSHORT(unsigned argc, const char* argv[])
{
  /* short options can take the forms:
   *  --option arg
   *  -option arg
   */
  std::string arg(argv[0]);
  size_t      arg_opt_start = arg.find_first_not_of('-');
  std::string option        = arg.substr(arg_opt_start);
  /* lookup option */

  /* argument in argv[1] */
  /* xxx, need to handle case where option isn't required */
  if (argc == 1)
  {
    error_reporter.error(where()) << "Not processing option `" << option << "' without argument\n";
    return 0; /* run out of argv for argument */
  }
  storePair(false, true, option, std::string(argv[1]));

  return 1;
}

std::list<const char*> scanArgv(Options& opts, unsigned argc, const char* argv[], ErrorReporter& error_reporter)
{
  ArgvParser avp(opts, error_reporter);

  /* a list for anything that didn't get handled as an option */
  std::list<const char*> non_option_arguments;

  for (unsigned i = 1; i < argc; i++)
  {
    if (argv[i][0] != '-')
    {
      non_option_arguments.push_back(argv[i]);
      continue;
    }

    if (argv[i][1] == 0)
    {
      /* a lone single dash is an argument (usually signifying stdin) */
      non_option_arguments.push_back(argv[i]);
      continue;
    }

    if (argv[i][1] != '-')
    {
      /* handle short (single dash) options */
      i += avp.parseSHORT(argc - i, &argv[i]);
      continue;
    }

    if (argv[i][2] == 0)
    {
      /* a lone double dash ends option processing */
      while (++i < argc)
      {
        non_option_arguments.push_back(argv[i]);
      }
      break;
    }

    /* handle long (double dash) options */
    i += avp.parseGNU(argc - i, &argv[i]);
  }

  return non_option_arguments;
}

struct CfgStreamParser : public OptionWriter
{
  CfgStreamParser(const std::string& rName, Options& rOpts, ErrorReporter& rError_reporter)
    : OptionWriter(rOpts, rError_reporter), name(rName), linenum(0)
  {}

  const std::string name;
  int               linenum;
  const std::string where()
  {
    std::ostringstream os;
    os << name << ":" << linenum;
    return os.str();
  }

  void scanLine(std::string& line);
  void scanStream(std::istream& in);
};

void CfgStreamParser::scanLine(std::string& line)
{
  /* strip any leading whitespace */
  size_t start = line.find_first_not_of(" \t\n\r");
  if (start == std::string::npos)
  {
    /* blank line */
    return;
  }
  if (line[start] == '#')
  {
    /* comment line */
    return;
  }
  /* look for first whitespace or ':' after the option end */
  size_t      option_end = line.find_first_of(": \t\n\r", start);
  std::string option     = line.substr(start, option_end - start);

  /* look for ':', eat up any whitespace first */
  start = line.find_first_not_of(" \t\n\r", option_end);
  if (start == std::string::npos)
  {
    /* error: badly formatted line */
    error_reporter.warn(where()) << "line formatting error\n";
    return;
  }
  if (line[start] != ':')
  {
    /* error: badly formatted line */
    error_reporter.warn(where()) << "line formatting error\n";
    return;
  }

  /* look for start of value string -- eat up any leading whitespace */
  start = line.find_first_not_of(" \t\n\r", ++start);
  if (start == std::string::npos)
  {
    /* error: badly formatted line */
    error_reporter.warn(where()) << "line formatting error\n";
    return;
  }

  /* extract the value part, which may contain embedded spaces
   * by searching for a word at a time, until we hit a comment or end of line */
  size_t value_end = start;
  do
  {
    if (line[value_end] == '#')
    {
      /* rest of line is a comment */
      value_end--;
      break;
    }
    value_end = line.find_first_of(" \t\n\r", value_end);
    /* consume any white space, incase there is another word.
     * any trailing whitespace will be removed shortly */
    value_end = line.find_first_not_of(" \t\n\r", value_end);
  } while (value_end != std::string::npos);
  /* strip any trailing space from value*/
  value_end = line.find_last_not_of(" \t\n\r", value_end);

  std::string value;
  if (value_end >= start)
  {
    value = line.substr(start, value_end + 1 - start);
  }
  else
  {
    /* error: no value */
    error_reporter.warn(where()) << "no value found\n";
    return;
  }

  /* store the value in option */
  storePair(true, false, option, value);
}

void CfgStreamParser::scanStream(std::istream& in)
{
  do
  {
    linenum++;
    std::string line;
    getline(in, line);
    scanLine(line);
  } while (!!in);
}

/* for all options in opts, set their storage to their specified
 * default value */
void setDefaults(Options& opts)
{
  for (Options::NamesPtrList::iterator it = opts.opt_list.begin(); it != opts.opt_list.end(); it++)
  {
    (*it)->opt->setDefault();
  }
}

void parseConfigFile(Options& opts, const std::string& filename, ErrorReporter& error_reporter)
{
  std::ifstream cfgstream(filename.c_str(), std::ifstream::in);
  if (!cfgstream)
  {
    error_reporter.error(filename) << "Failed to open config file\n";
    return;
  }
  CfgStreamParser csp(filename, opts, error_reporter);
  csp.scanStream(cfgstream);
}

}   // namespace ProgramOptionsLite
