# frozen_string_literal: true

require_relative 'version'

# The Getopt module serves as a namespace only
module Getopt
  REQUIRED  = 0 # Argument is required if switch is provided.
  BOOLEAN   = 1 # Value of argument is true if provided, false otherwise.
  OPTIONAL  = 2 # Argument is optional if switch is provided.
  INCREMENT = 3 # Argument is incremented by 1 each time the switch appears.
  NEGATABLE = 4 # Automatically provide --no-<switch> to set value to false.

  # INTEGER   = 5 # Argument automatically converted to integer if provided.
  # FLOAT     = 6 # Argument automatically converted to float if provided.

  # The Getopt::Long class encapsulates longhanded parameter parsing options
  class Long
    include Getopt::Version

    # Error raised if an illegal option or argument is passed
    class Error < StandardError; end

    # Takes an array of switches. Each array consists of up to three
    # elements that indicate the name and type of switch. Returns a hash
    # containing each switch name, minus the '-', as a key. The value
    # for each key depends on the type of switch and/or the value provided
    # by the user.
    #
    # The long switch _must_ be provided. The short switch defaults to the
    # first letter of the short switch. The default type is BOOLEAN.
    #
    # Example:
    #
    #  opts = Getopt::Long.getopts(
    #     ['--debug'                   ],
    #     ['--verbose', '-v'           ],
    #     ['--level',   '-l', INCREMENT]
    #  )
    #
    # See the README file for more information.
    #
    def self.getopts(*switches)
      if switches.empty?
        raise ArgumentError, 'no switches provided'
      end

      hash  = {} # Hash returned to user
      valid = [] # Tracks valid switches
      types = {} # Tracks argument types
      syns  = {} # Tracks long and short arguments, or multiple shorts
      negs  = {} # Tracks negated switches (maps --no-foo to --foo)

      # If a string is passed, split it and convert it to an array of arrays
      if switches.first.is_a?(String)
        switches = switches.join.split
        switches.map!{ |switch| [switch] }
      end

      # Set our list of valid switches, and proper types for each switch
      switches.each do |switch|
        valid.push(switch[0]) # Set valid long switches

        # Set type for long switch, default to BOOLEAN.
        if switch[1].is_a?(Integer)
          switch[2] = switch[1]
          types[switch[0]] = switch[2]
          switch[1] = switch[0][1..2]
        else
          switch[2] ||= BOOLEAN
          types[switch[0]] = switch[2]
          switch[1] ||= switch[0][1..2]
        end

        # Create synonym hash.  Default to first char of long switch for
        # short switch, e.g. "--verbose" creates a "-v" synonym.  The same
        # synonym can only be used once - first one wins.
        syns[switch[0]] = switch[1] unless syns[switch[1]]
        syns[switch[1]] = switch[0] unless syns[switch[1]]

        switch[1] = [switch[1]]

        switch[1].each do |char|
          types[char] = switch[2]  # Set type for short switch
          valid.push(char)         # Set valid short switches
        end

        # For NEGATABLE switches, register the --no- variant
        if switch[2] == NEGATABLE
          negated = switch[0].sub(/^--/, '--no-')
          valid.push(negated)
          types[negated] = NEGATABLE
          negs[negated] = switch[0] # Map --no-foo back to --foo
        end
      end

      re_long     = /^(--\w+[-\w+]*)?$/
      re_short    = /^(-[^\s-])$/
      re_long_eq  = /^(--\w+[-\w+]*)?=(.*?)$|(-[^\s-])=(.*?)$/
      re_short_sq = /^(-[^\s-])(\S+?)$/

      ARGV.each_with_index do |opt, index|
        # Allow either -x -v or -xv style for single char args
        if re_short_sq.match(opt)
          chars = opt.chars[1..].map{ |s| "-#{s}" }

          chars.each_with_index do |char, i|
            unless valid.include?(char)
              raise Error, "invalid switch '#{char}'"
            end

            # Grab the next arg if the switch takes a required arg
            if types[char] == REQUIRED
              # Deal with a argument squished up against switch
              if chars[i + 1]
                arg = chars[i + 1..].join.tr('-', '')
                ARGV.push(char, arg)
                break
              else
                arg = ARGV.delete_at(index + 1)
                if arg.nil? || valid.include?(arg) # Minor cheat here
                  err = "no value provided for required argument '#{char}'"
                  raise Error, err
                end
                ARGV.push(char, arg)
              end
            elsif types[char] == OPTIONAL
              if chars[i + 1] && !valid.include?(chars[i + 1])
                arg = chars[i + 1..].join.tr('-', '')
                ARGV.push(char, arg)
                break
              elsif ARGV[index + 1] && !valid.include?(ARGV[index + 1])
                arg = ARGV.delete_at(index + 1)
                ARGV.push(char, arg)
              else
                ARGV.push(char)
              end
            else
              ARGV.push(char)
            end
          end
          next
        end

        match = re_long.match(opt) || re_short.match(opt)
        switch = match.captures.first if match

        if match = re_long_eq.match(opt)
          switch, value = match.captures.compact
          ARGV.push(switch, value)
          next
        end

        # Make sure that all the switches are valid.  If 'switch' isn't
        # defined at this point, it means an option was passed without
        # a preceding switch, e.g. --option foo bar.
        unless valid.include?(switch)
          switch ||= opt
          raise Error, "invalid switch '#{switch}'"
        end

        # Required arguments
        if types[switch] == REQUIRED
          nextval = ARGV[index + 1]

          # Make sure there's a value for mandatory arguments
          if nextval.nil?
            err = "no value provided for required argument '#{switch}'"
            raise Error, err
          end

          # If there is a value, make sure it's not another switch
          if valid.include?(nextval)
            err = "cannot pass switch '#{nextval}' as an argument"
            raise Error, err
          end

          # If the same option appears more than once, put the values in array.
          if hash[switch]
            hash[switch] = [hash[switch], nextval].flatten
          else
            hash[switch] = nextval
          end

          ARGV.delete_at(index + 1)
        end

        # For boolean arguments set the switch's value to true.
        if types[switch] == BOOLEAN
          if hash.key?(switch)
            raise Error, 'boolean switch already set'
          end
          hash[switch] = true
        end

        # For increment arguments, set the switch's value to 0, or
        # increment it by one if it already exists.
        if types[switch] == INCREMENT
          if hash.key?(switch)
            hash[switch] += 1
          else
            hash[switch] = 1
          end
        end

        # For negatable arguments, --foo sets true, --no-foo sets false.
        # The value is stored under the base switch name (without --no-).
        if types[switch] == NEGATABLE
          base_switch = negs[switch] || switch
          if hash.key?(base_switch)
            raise Error, 'negatable switch already set'
          end
          hash[base_switch] = !negs.key?(switch) # true unless it's a --no- variant
        end

        # For optional argument, there may be an argument.  If so, it
        # cannot be another switch.  If not, it is set to true.
        if types[switch] == OPTIONAL
          nextval = ARGV[index + 1]
          if valid.include?(nextval)
            hash[switch] = true
          else
            hash[switch] = nextval
            ARGV.delete_at(index + 1)
          end
        end
      end

      # rubocop:disable Style/CombinableLoops

      # Set synonymous switches to the same value, e.g. if -t is a synonym
      # for --test, and the user passes "--test", then set "-t" to the same
      # value that "--test" was set to.
      #
      # This allows users to refer to the long or short switch and get
      # the same value
      hash.dup.each do |switch, val|
        # Skip negated switches - they've already been mapped to base switch
        next if negs.key?(switch)

        if syns.keys.include?(switch)
          syns[switch] = [syns[switch]]
          syns[switch].each do |key|
            hash[key] = val
          end
        end
      end

      # Get rid of leading "--" and "-" to make it easier to reference
      hash.dup.each do |key, value|
        if key =~ /^-/
          if key[0, 2] == '--'
            nkey = key.sub('--', '')
          else
            nkey = key.sub('-', '')
          end
          hash.delete(key)
          hash[nkey] = value
        end
      end

      # rubocop:enable Style/CombinableLoops

      hash
    end
  end
end
