# This file is part of Yaggo.

# Yaggo 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.

# Yaggo 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 Yaggo.  If not, see <http://www.gnu.org/licenses/>.

def quote_newline_dquotes str, spaces = ""
  str.gsub(/"/, '\\"').split(/\n/).join("\\n\" \\\n#{spaces}\"")
end

def output_options_descriptions out, opts, hidden
  opts.each { |o|
    # need to be improved. break lines if too long
    next if o.secret || (o.hidden ^ hidden)
    s = " " + o.switches
    if s.size >= $switchesjust
      s += "\\n" + "".ljust($switchesjust)
    else
      s = s.ljust($switchesjust)
    end
    out.puts("    \"#{s} #{o.help}\\n\"") 
  }
end

def output_cpp_parser(h, class_name)
  $options.each { |o| o.check }
  $args.each { |a| a.check }
  if $args.size > 1
    mul_args = $args[0..-2].select { |a| a.multiple }
    if mul_args.size > 0
      gram = mul_args.size > 1 ? "s are" : " is"
      raise "The following#{gram} not the last arg but marked multiple: #{mul_args.map { |a| a.name }.join(", ")}"
    end
  end

  # Headers

  h.puts(<<EOS)
/***** This code was generated by Yaggo. Do not edit ******/

EOS

  if $license
    lines = $license.split(/\n/)
    h.puts("/* #{lines[0]}", *(lines[1..-1].map { |l| " * " + l }))
    h.puts(" */", "")
  elsif $yaggo_options[:license]
    open($yaggo_options[:license]) { |fd|
      h.puts(fd.read)
    }
    h.puts("")
  end

h.puts(<<EOS)
#ifndef __#{class_name.upcase()}_HPP__
#define __#{class_name.upcase()}_HPP__

#include <stdint.h>
#include <unistd.h>
#include <stdlib.h>
#include <getopt.h>
#include <errno.h>
#include <string.h>
#include <stdexcept>
#include <string>
#include <limits>
#include <vector>
#include <iostream>
#include <sstream>
#include <memory>

class #{class_name} {
 // Boiler plate stuff. Conversion from string to other formats
EOS

  output_conversion_code h

  h.puts(<<EOS)
public:
EOS

  static_decl = $options.map { |o| o.static_decl }.flatten
  h.puts("  " + static_decl.join("\n  "), "") unless static_decl.empty?

  ($options + $args).each { |o| h.puts("  " + o.var_decl.join("\n  ")) }
  h.puts("")

  # Create enum if option with no short version
  only_long = $options.map { |o| o.long_enum }.flatten.compact
  need_full = $options.any? { |o| o.hidden }

  help_no_h = $options.any? { |o| o.short == "h" }
  version_no_V = $options.any? { |o| o.short == "V" }
  usage_no_U = $options.any? { |o| o.short == "U" }
  h.print("  enum {\n    START_OPT = 1000")
  h.print(",\n    FULL_HELP_OPT") if need_full
  h.print(",\n    HELP_OPT") if help_no_h
  h.print(",\n    VERSION_OPT") if version_no_V
  h.print(",\n    USAGE_OPT") if usage_no_U
  if only_long.empty?
    h.puts("\n  };")
  else
    h.puts(",", "    " + only_long.join(",\n    "), "  };")
  end

  # Constructors and initialization
  h.puts("", "  #{class_name}()")
  h.puts("    :" + ($options + $args).map { |o| o.init }.join("\n    ,")) unless $options.empty? && $args.empty?
  h.puts("  { }")
  h.puts("", "  #{class_name}(int argc, char* argv[])")
  h.puts("    :" + ($options + $args).map { |o| o.init }.join("\n    ,")) unless $options.empty? && $args.empty?
  h.puts("  { parse(argc, argv); }", "");

  # Main arsing function
  h.puts("  void parse(int argc, char* argv[]) {",
         "    static struct option long_options[] = {")
  $options.empty? or
    h.puts("      " + $options.map { |o| o.struct }.flatten.join(",\n      ") + ",")
  h.puts("      {\"help\", 0, 0, #{help_no_h ? "HELP_OPT" : "'h'"}},")
  h.puts("      {\"full-help\", 0, 0, FULL_HELP_OPT},") if need_full
  h.puts("      {\"usage\", 0, 0, #{usage_no_U ? "USAGE_OPT" : "'U'"}},",
         "      {\"version\", 0, 0, #{version_no_V ? "VERSION_OPT" : "'V'"}},",
         "      {0, 0, 0, 0}", "    };")
  short_str = $posix ? "+" : ""
  short_str += "h" unless help_no_h
  short_str += "V" unless version_no_V
  short_str += "U" unless usage_no_U
  short_str += $options.map { |o| o.short_str }.compact.join("")
  
  h.puts("    static const char *short_options = \"#{short_str}\";", "")

  need_err   = $options.any? { |o| o.type != :flag && o.type != :string && o.type != :c_string}
  need_err ||= $args.any? { |a| a.type != :string && a.type != :c_string }
  need_err ||= ($options + $args).any? { |o| !o.access_types.empty? }
  h.puts("    ::std::string err;") if need_err

  # Actual parsing
  h.puts(<<EOS)
#define CHECK_ERR(type,val,which) if(!err.empty()) { ::std::cerr << "Invalid " #type " '" << val << "' for [" which "]: " << err << "\\n"; exit(1); }
    while(true) {
      int index = -1;
      int c = getopt_long(argc, argv, short_options, long_options, &index);
      if(c == -1) break;
      switch(c) {
      case ':':
        ::std::cerr << \"Missing required argument for \"
                  << (index == -1 ? ::std::string(1, (char)optopt) : std::string(long_options[index].name))
                  << ::std::endl;
        exit(1);
      case #{help_no_h ? "HELP_OPT" : "'h'"}:
        ::std::cout << usage() << \"\\n\\n\" << help() << std::endl;
        exit(0);
      case #{usage_no_U ? "USAGE_OPT" : "'U'"}:
        ::std::cout << usage() << \"\\nUse --help for more information.\" << std::endl;
        exit(0);
      case 'V':
        print_version();
        exit(0);
      case '?':
        ::std::cerr << \"Use --usage or --help for some help\\n\";
        exit(1);
EOS
  if need_full
    h.puts(<<EOS)
      case FULL_HELP_OPT:
        ::std::cout << usage() << \"\\n\\n\" << help() << \"\\n\\n\" << hidden() << std::flush;
        exit(0);
EOS
  end
  
  $options.each { |o|
    if o.type == :flag && o.noflag
      h.puts("      case #{o.long_enum[0]}:",
             "        " + o.parse_arg.join("\n        "),
             "        break;",
             "      case #{o.long_enum[1]}:",
             "        " + o.parse_arg(true).join("\n        "),
             "        break;")
    else
      h.puts("      case #{o.long_enum ? o.long_enum[0] : "'" + o.short + "'"}:",
             "        " + o.parse_arg.join("\n        "),
             "        break;")
    end
  }
  h.puts("      }", # close case
         "    }") # close while(true)

  # Check required
  $options.any? { |o| o.required} and
    h.puts("", "    // Check that required switches are present")
  $options.each { |o|
    next unless o.required
    h.puts(<<EOS)
    if(!#{o.var}_given)
      error("[#{o.switches}] required switch");
EOS
  }
  # Check conflict
  $options.any? { |o| !o.conflict.empty? } and
    h.puts("", "    // Check mutually exlusive switches")
  $options.each { |o|
    o_check = o.var + (o.type == :flag ? "_flag" : "_given")
    o.conflict.each { |cos|
      co = $opt_hash[cos]
      co_check = co.var + (co.type == :flag ? "_flag" : "_given")
      h.puts(<<EOS)
    if(#{o_check} && #{co_check})
      error("Switches [#{o.switches}] and [#{co.switches}] are mutually exclusive");
EOS
    }
  }
  # Check at_least
  $options.any? { |o| o.at_least } and
    h.puts("", "    // Check at_least requirements")
  $options.each { |o|
    next unless o.multiple && !o.at_least.nil?
    h.puts(<<EOS)
    if(#{o.var}_arg.size() < #{o.at_least})
      error("[#{o.switches}] must be given at least #{o.at_least} times");
EOS
  }
  
  # Parse arguments
  h.puts("", "    // Parse arguments")
  if $args.size == 0 || !$args[-1].multiple
    h.puts(<<EOS)
    if(argc - optind != #{$args.size})
      error("Requires exactly #{$args.size} argument#{$args.size > 1 ? "s" : ""}.");
EOS
  else
    min_args = $args.size - 1 + $args[-1].at_least
    h.puts(<<EOS)
    if(argc - optind < #{min_args})
      error("Requires at least #{min_args} argument#{min_args > 1 ? "s" : ""}.");
EOS
  end
  $args.each { |a| h.puts("    " + a.parse_arg.join("\n    ")) }

  # Check access rights
  if ($options + $args).any? { |o| !o.access_types.empty? }
    r_to_f = { "read" => "R_OK", "write" => "W_OK", "exec" => "X_OK" }
    h.puts("", "    // Check access rights")
    ($args + $options).each { |o|
      next if o.access_types.empty?
      mode = o.access_types.map { |t| r_to_f[t] }.join("|")
      msg = Arg === o ? "Argument " + o.name : "Switch " + o.switches
      msg += ", access right (#{o.access_types.join("|")}) failed for file '"
      h.puts("    if(access(#{o.var}_arg, #{mode})) {",
             "      err = \"#{msg}\";",
             "      ((err += #{o.var}_arg) += \"': \") += strerror(errno);",
             "      error(err.c_str());",
             "    }")
    }
  end

  h.puts("  }") # close parser

  # Usage
  if !$usage.nil?
    ausage = quote_newline_dquotes($usage, "  ")
  else
    ausage = "Usage: #{$package || class_name} [options]"
    $args.each { |a|
     ausage += " #{a.name}:#{a.typestr || dflt_typestr(a.type)}#{a.multiple ? "+" : ""}"
    }
  end

  h.puts(<<EOS)
  static const char * usage() { return "#{ausage}"; }
  class error {
    int code_;
    std::ostringstream msg_;

    // Select the correct version (GNU or XSI) version of
    // strerror_r. strerror_ behaves like the GNU version of strerror_r,
    // regardless of which version is provided by the system.
    static const char* strerror__(char* buf, int res) {
      return res != -1 ? buf : "Invalid error";
    }
    static const char* strerror__(char* buf, char* res) {
      return res;
    }
    static const char* strerror_(int err, char* buf, size_t buflen) {
      return strerror__(buf, strerror_r(err, buf, buflen));
    }
    struct no_t { };

  public:
    static no_t no;
    error(int code = EXIT_FAILURE) : code_(code) { }
    explicit error(const char* msg, int code = EXIT_FAILURE) : code_(code)
      { msg_ << msg; }
    error(const std::string& msg, int code = EXIT_FAILURE) : code_(code)
      { msg_ << msg; }
    error& operator<<(no_t) {
      char buf[1024];
      msg_ << ": " << strerror_(errno, buf, sizeof(buf));
      return *this;
    }
    template<typename T>
    error& operator<<(const T& x) { msg_ << x; return (*this); }
    ~error() {
      ::std::cerr << "Error: " << msg_.str() << "\\n"
                  << usage() << "\\n"
                  << "Use --help for more information"
                  << ::std::endl;
      exit(code_);
    }
  };
EOS

  # Help
  desc = ""
  unless $purpose.nil?
    desc += $purpose + "\\n\\n"
  end
  unless $description.nil?
    desc += $description.split(/\n/).join("\\n\" \\\n    \"") + "\\n\\n"
  end

  h.puts(<<EOS)
  static const char * help() { return
    "#{desc}"
    "Options (default value in (), *required):\\n"
EOS
  output_options_descriptions(h, $options, false)
  usage_switch = " -U, "
  usage_switch = " " * usage_switch.size if usage_no_U
  usage_switch += "--usage"
  h.puts("    \"#{usage_switch.ljust($switchesjust)}  Usage\\n\"")
  help_switch = " -h, "
  help_switch = " " * help_switch.size if help_no_h
  help_switch += "--help"
  h.puts("    \"#{help_switch.ljust($switchesjust)}  This message\\n\"")
  h.puts("    \"#{"     --full-help".ljust($switchesjust)}  Detailed help\\n\"") if need_full
  version_switch = " -V, "
  version_switch = " " * version_switch.size if version_no_V
  version_switch += "--version"
  h.print("    \"#{version_switch.ljust($switchesjust)}  Version")
  if $after_text.nil?
    h.puts("\";")
  else
    h.puts("\\n\" \\", "  \"\\n\"")
    atext = quote_newline_dquotes($after_text, "  ")
    h.puts("    \"#{atext}\";")
  end
  h.puts("  }")

  # Hidden help
  has_hidden = $options.any? { |o| o.hidden }
  if has_hidden 
    h.puts(<<EOS)
  static const char* hidden() { return
    "Hidden options:\\n"
EOS
  output_options_descriptions(h, $options, true)
  h.puts(<<EOS)
    "";
  }
EOS
  else
    h.puts(<<EOS)
  static const char* hidden() { return ""; }
EOS
  end
  

  # Version
  h.puts("  void print_version(::std::ostream &os = std::cout) const {",
         "#ifndef PACKAGE_VERSION",
         "#define PACKAGE_VERSION \"0.0.0\"",
         "#endif",
         "    os << #{$version ? "\"" + $version + "\"" : "PACKAGE_VERSION"} << \"\\n\";",
         "  }")
  
  # Dump
  h.puts("  void dump(::std::ostream &os = std::cout) {")
  ($options + $args).each { |o| h.puts("    os << #{o.dump.join(" << ")} << \"\\n\";") }
  h.puts("  }")

  # Private methods
  h.puts(<<EOS)
};
EOS

  # Initialize static members
  # TODO: Should we have an option to put this in a .cc file?
  $options.each { |o|
    next unless o.type == :enum
    h.puts("const char* const #{class_name}::#{o.var}::strs[#{o.enum.size + 1}] = { #{o.enum.map { |x| "\"#{x}\"" }.join(", ") }, (const char*)0 };")
  }

h.puts(<<EOS)
#endif // __#{class_name.upcase}_HPP__"
EOS
end
