# frozen_string_literal: true

require_relative '../../../puppet/concurrent/thread_local_singleton'

module Puppet::Pops
module Types
# Converts Puppet runtime objects to String under the control of a Format.
# Use from Puppet Language is via the function `new`.
#
# @api private
#
class StringConverter
  # @api private
  class FormatError < ArgumentError
    def initialize(type_string, actual, expected)
      super "Illegal format '#{actual}' specified for value of #{type_string} type - expected one of the characters '#{expected}'"
    end
  end

  class Indentation
    attr_reader :level
    attr_reader :first
    attr_reader :is_indenting
    alias :first? :first
    alias :is_indenting? :is_indenting

    def initialize(level, first, is_indenting)
      @level = level
      @first = first
      @is_indenting = is_indenting
    end

    def subsequent
      first? ? self.class.new(level, false, @is_indenting) : self
    end

    def indenting(indenting_flag)
      self.class.new(level, first?, indenting_flag)
    end

    def increase(indenting_flag = false)
      self.class.new(level + 1, true, indenting_flag)
    end

    def breaks?
      is_indenting? && level > 0 && !first?
    end

    def padding
      ' ' * 2 * level
    end
  end

  # Format represents one format specification that is textually represented by %<flags><width>.<precision><format>
  # Format parses and makes the individual parts available when an instance is created.
  #
  # @api private
  #
  class Format
    # Boolean, alternate form (varies in meaning)
    attr_reader :alt
    alias :alt? :alt

    # Nil or Integer with width of field > 0
    attr_reader :width
    # Nil or Integer precisions
    attr_reader :prec
    # One char symbol denoting the format
    attr_reader :format
    # Symbol, :space, :plus, :ignore
    attr_reader :plus
    # Boolean, left adjust in given width or not
    attr_reader :left
    # Boolean left_pad with zero instead of space
    attr_reader :zero_pad
    # Delimiters for containers, a "left" char representing the pair <[{(
    attr_reader :delimiters

    # Map of type to format for elements contained in an object this format applies to
    attr_accessor :container_string_formats

    # Separator string inserted between elements in a container
    attr_accessor :separator

    # Separator string inserted between sub elements in a container
    attr_accessor :separator2

    attr_reader :orig_fmt

    FMT_PATTERN_STR = '^%([\s\[+#0{<(|-]*)([1-9][0-9]*)?(?:\.([0-9]+))?([a-zA-Z])$'
    FMT_PATTERN = Regexp.compile(FMT_PATTERN_STR)
    DELIMITERS  = ['[', '{', '(', '<', '|']
    DELIMITER_MAP = {
      '[' => ['[', ']'],
      '{' => ['{', '}'],
      '(' => ['(', ')'],
      '<' => ['<', '>'],
      '|' => ['|', '|'],
      :space => ['', '']
    }.freeze

    def initialize(fmt)
      @orig_fmt = fmt
      match = FMT_PATTERN.match(fmt)
      unless match
        raise ArgumentError, "The format '#{fmt}' is not a valid format on the form '%<flags><width>.<prec><format>'"
      end

      @format = match[4]
      unless @format.is_a?(String) && @format.length == 1
        raise ArgumentError, "The format must be a one letter format specifier, got '#{@format}'"
      end

      @format = @format.to_sym
      flags  = match[1].split('') || []
      unless flags.uniq.size == flags.size
        raise ArgumentError, "The same flag can only be used once, got '#{fmt}'"
      end

      @left  = flags.include?('-')
      @alt   = flags.include?('#')
      @plus  = if flags.include?(' ')
                 :space
               else
                 flags.include?('+') ? :plus : :ignore
               end
      @zero_pad = flags.include?('0')

      @delimiters = nil
      DELIMITERS.each do |d|
        next unless flags.include?(d)
        unless @delimiters.nil?
          raise ArgumentError, "Only one of the delimiters [ { ( < | can be given in the format flags, got '#{fmt}'"
        end

        @delimiters = d
      end

      @width = match[2] ? match[2].to_i : nil
      @prec  = match[3] ? match[3].to_i : nil
    end

    # Merges one format into this and returns a new `Format`. The `other` format overrides this.
    # @param [Format] other
    # @returns [Format] a merged format
    #
    def merge(other)
      result = Format.new(other.orig_fmt)
      result.separator = other.separator || separator
      result.separator2 = other.separator2 || separator2
      result.container_string_formats = Format.merge_string_formats(container_string_formats, other.container_string_formats)
      result
    end

    # Merges two formats where the `higher` format overrides the `lower`. Produces a new `Format`
    # @param [Format] lower
    # @param [Format] higher
    # @returns [Format] the merged result
    #
    def self.merge(lower, higher)
      unless lower && higher
        return lower || higher
      end

      lower.merge(higher)
    end

    # Merges a type => format association and returns a new merged and sorted association.
    # @param [Format] lower
    # @param [Format] higher
    # @returns [Hash] the merged type => format result
    #
    def self.merge_string_formats(lower, higher)
      unless lower && higher
        return lower || higher
      end

      # drop all formats in lower than is more generic in higher. Lower must never
      # override higher
      lower = lower.reject { |lk, _| higher.keys.any? { |hk| hk != lk && hk.assignable?(lk) } }

      merged = (lower.keys + higher.keys).uniq.map do |k|
        [k, merge(lower[k], higher[k])]
      end
      sort_formats(merged)
    end

    # Sorts format based on generality of types - most specific types before general
    #
    def self.sort_formats(format_map)
      format_map = format_map.sort do |(a, _), (b, _)|
        ab = b.assignable?(a)
        ba = a.assignable?(b)
        if a == b
          0
        elsif ab && !ba
          -1
        elsif !ab && ba
          1
        else
          # arbitrary order if disjunct (based on name of type)
          rank_a = type_rank(a)
          rank_b = type_rank(b)
          if rank_a == 0 || rank_b == 0
            a.to_s <=> b.to_s
          else
            rank_a <=> rank_b
          end
        end
      end
      format_map.to_h
    end

    # Ranks type on specificity where it matters
    # lower number means more specific
    def self.type_rank(t)
      case t
      when PStructType
        1
      when PHashType
        2
      when PTupleType
        3
      when PArrayType
        4
      when PPatternType
        10
      when PEnumType
        11
      when PStringType
        12
      else
        0
      end
    end

    # Returns an array with a delimiter pair derived from the format.
    # If format does not contain a delimiter specification the given default is returned
    #
    # @param [Array<String>] the default delimiters
    # @returns [Array<String>] a tuple with left, right delimiters
    #
    def delimiter_pair(default = StringConverter::DEFAULT_ARRAY_DELIMITERS)
      DELIMITER_MAP[@delimiters || @plus] || default
    end

    def to_s
      "%#{@flags}#{@width}.#{@prec}#{@format}"
    end
  end

  extend Puppet::Concurrent::ThreadLocalSingleton

  # @api public
  def self.convert(value, string_formats = :default)
    singleton.convert(value, string_formats)
  end

  # @api private
  #
  def initialize
    @string_visitor = Visitor.new(self, "string", 3, 3)
  end

  DEFAULT_INDENTATION = Indentation.new(0, true, false).freeze

  # format used by default for values in a container
  # (basically strings are quoted since they may contain a ','))
  #
  DEFAULT_CONTAINER_FORMATS = {
    PAnyType::DEFAULT => Format.new('%p').freeze, # quoted string (Ruby inspect)
  }.freeze

  DEFAULT_ARRAY_FORMAT                          = Format.new('%a')
  DEFAULT_ARRAY_FORMAT.separator                = ', '
  DEFAULT_ARRAY_FORMAT.separator2               = ', '
  DEFAULT_ARRAY_FORMAT.container_string_formats = DEFAULT_CONTAINER_FORMATS
  DEFAULT_ARRAY_FORMAT.freeze

  DEFAULT_HASH_FORMAT                           = Format.new('%h')
  DEFAULT_HASH_FORMAT.separator                 = ', '
  DEFAULT_HASH_FORMAT.separator2                = ' => '
  DEFAULT_HASH_FORMAT.container_string_formats  = DEFAULT_CONTAINER_FORMATS
  DEFAULT_HASH_FORMAT.freeze

  DEFAULT_HASH_DELIMITERS                       = ['{', '}'].freeze
  DEFAULT_ARRAY_DELIMITERS                      = ['[', ']'].freeze

  DEFAULT_STRING_FORMATS = {
    PObjectType::DEFAULT => Format.new('%p').freeze, # call with initialization hash
    PFloatType::DEFAULT => Format.new('%f').freeze, # float
    PNumericType::DEFAULT => Format.new('%d').freeze, # decimal number
    PArrayType::DEFAULT => DEFAULT_ARRAY_FORMAT.freeze,
    PHashType::DEFAULT => DEFAULT_HASH_FORMAT.freeze,
    PBinaryType::DEFAULT => Format.new('%B').freeze, # strict base64 string unquoted
    PAnyType::DEFAULT => Format.new('%s').freeze, # unquoted string
  }.freeze

  DEFAULT_PARAMETER_FORMAT = {
    PCollectionType::DEFAULT => '%#p',
    PObjectType::DEFAULT => '%#p',
    PBinaryType::DEFAULT => '%p',
    PStringType::DEFAULT => '%p',
    PRuntimeType::DEFAULT => '%p'
  }.freeze

  # Converts the given value to a String, under the direction of formatting rules per type.
  #
  # When converting to string it is possible to use a set of built in conversion rules.
  #
  # A format is specified on the form:
  #
  # ´´´
  # %[Flags][Width][.Precision]Format
  # ´´´
  #
  # `Width` is the number of characters into which the value should be fitted. This allocated space is
  # padded if value is shorter. By default it is space padded, and the flag 0 will cause padding with 0
  # for numerical formats.
  #
  # `Precision` is the number of fractional digits to show for floating point, and the maximum characters
  # included in a string format.
  #
  # Note that all data type supports the formats `s` and `p` with the meaning "default to-string" and
  # "default-programmatic to-string".
  #
  # ### Integer
  #
  # | Format  | Integer Formats
  # | ------  | ---------------
  # | d       | Decimal, negative values produces leading '-'
  # | x X     | Hexadecimal in lower or upper case. Uses ..f/..F for negative values unless # is also used
  # | o       | Octal. Uses ..0 for negative values unless # is also used
  # | b B     | Binary with prefix 'b' or 'B'. Uses ..1/..1 for negative values unless # is also used
  # | c       | numeric value representing a Unicode value, result is a one unicode character string, quoted if alternative flag # is used
  # | s       | same as d, or d in quotes if alternative flag # is used
  # | p       | same as d
  # | eEfgGaA | converts integer to float and formats using the floating point rules
  #
  # Defaults to `d`
  #
  # ### Float
  #
  # | Format  | Float formats
  # | ------  | -------------
  # | f       | floating point in non exponential notation
  # | e E     | exponential notation with 'e' or 'E'
  # | g G     | conditional exponential with 'e' or 'E' if exponent < -4 or >= the precision
  # | a A     | hexadecimal exponential form, using 'x'/'X' as prefix and 'p'/'P' before exponent
  # | s       | converted to string using format p, then applying string formatting rule, alternate form # quotes result
  # | p       | f format with minimum significant number of fractional digits, prec has no effect
  # | dxXobBc | converts float to integer and formats using the integer rules
  #
  # Defaults to `p`
  #
  # ### String
  #
  # | Format | String
  # | ------ | ------
  # | s      | unquoted string, verbatim output of control chars
  # | p      | programmatic representation - strings are quoted, interior quotes and control chars are escaped
  # | C      | each :: name segment capitalized, quoted if alternative flag # is used
  # | c      | capitalized string, quoted if alternative flag # is used
  # | d      | downcased string, quoted if alternative flag # is used
  # | u      | upcased string, quoted if alternative flag # is used
  # | t      | trims leading and trailing whitespace from the string, quoted if alternative flag # is used
  #
  # Defaults to `s` at top level and `p` inside array or hash.
  #
  # ### Boolean
  #
  # | Format    | Boolean Formats
  # | ----      | -------------------
  # | t T       | 'true'/'false' or 'True'/'False' , first char if alternate form is used (i.e. 't'/'f' or 'T'/'F').
  # | y Y       | 'yes'/'no', 'Yes'/'No', 'y'/'n' or 'Y'/'N' if alternative flag # is used
  # | dxXobB    | numeric value 0/1 in accordance with the given format which must be valid integer format
  # | eEfgGaA   | numeric value 0.0/1.0 in accordance with the given float format and flags
  # | s         | 'true' / 'false'
  # | p         | 'true' / 'false'
  #
  # ### Regexp
  #
  # | Format    | Regexp Formats (%/)
  # | ----      | ------------------
  # | s         | / / delimiters, alternate flag replaces / delimiters with quotes
  # | p         | / / delimiters
  #
  # ### Undef
  #
  # | Format    | Undef formats
  # | ------    | -------------
  # | s         | empty string, or quoted empty string if alternative flag # is used
  # | p         | 'undef', or quoted '"undef"' if alternative flag # is used
  # | n         | 'nil', or 'null' if alternative flag # is used
  # | dxXobB    | 'NaN'
  # | eEfgGaA   | 'NaN'
  # | v         | 'n/a'
  # | V         | 'N/A'
  # | u         | 'undef', or 'undefined' if alternative # flag is used
  #
  # ### Default (value)
  #
  # | Format    | Default formats
  # | ------    | ---------------
  # | d D       | 'default' or 'Default', alternative form # causes value to be quoted
  # | s         | same as d
  # | p         | same as d
  #
  # ### Binary (value)
  #
  # | Format    | Default formats
  # | ------    | ---------------
  # | s         | binary as unquoted characters
  # | p         | 'Binary("<base64strict>")'
  # | b         | '<base64>' - base64 string with newlines inserted
  # | B         | '<base64strict>' - base64 strict string (without newlines inserted)
  # | u         | '<base64urlsafe>' - base64 urlsafe string
  # | t         | 'Binary' - outputs the name of the type only
  # | T         | 'BINARY' - output the name of the type in all caps only
  #
  # The alternate form flag `#` will quote the binary or base64 text output
  # The width and precision values are applied to the text part only in `%p` format.
  #
  # ### Array & Tuple
  #
  # | Format    | Array/Tuple Formats
  # | ------    | -------------
  # | a         | formats with `[ ]` delimiters and `,`, alternate form `#` indents nested arrays/hashes
  # | s         | same as a
  # | p         | same as a
  #
  # See "Flags" `<[({\|` for formatting of delimiters, and "Additional parameters for containers; Array and Hash" for
  # more information about options.
  #
  # The alternate form flag `#` will cause indentation of nested array or hash containers. If width is also set
  # it is taken as the maximum allowed length of a sequence of elements (not including delimiters). If this max length
  # is exceeded, each element will be indented.
  #
  # ### Hash & Struct
  #
  # | Format    | Hash/Struct Formats
  # | ------    | -------------
  # | h         | formats with `{ }` delimiters, `,` element separator and ` => ` inner element separator unless overridden by flags
  # | s         | same as h
  # | p         | same as h
  # | a         | converts the hash to an array of [k,v] tuples and formats it using array rule(s)
  #
  # See "Flags" `<[({\|` for formatting of delimiters, and "Additional parameters for containers; Array and Hash" for
  # more information about options.
  #
  # The alternate form flag `#` will format each hash key/value entry indented on a separate line.
  #
  # ### Type
  #
  # | Format    | Array/Tuple Formats
  # | ------    | -------------
  # | s        | The same as p, quoted if alternative flag # is used
  # | p        | Outputs the type in string form as specified by the Puppet Language
  #
  # ### Flags
  #
  # | Flag     | Effect
  # | ------   | ------
  # | (space)  | space instead of + for numeric output (- is shown), for containers skips delimiters
  # | #        | alternate format; prefix 0x/0x, 0 (octal) and 0b/0B for binary, Floats force decimal '.'. For g/G keep trailing 0.
  # | +        | show sign +/- depending on value's sign, changes x,X, o,b, B format to not use 2's complement form
  # | -        | left justify the value in the given width
  # | 0        | pad with 0 instead of space for widths larger than value
  # | <[({\|   | defines an enclosing pair <> [] () {} or \| \| when used with a container type
  #
  #
  # ### Additional parameters for containers; Array and Hash
  #
  # For containers (Array and Hash), the format is specified by a hash where the following keys can be set:
  # * `'format'` - the format specifier for the container itself
  # * `'separator'` - the separator string to use between elements, should not contain padding space at the end
  # * `'separator2'` - the separator string to use between association of hash entries key/value
  # * `'string_formats'´ - a map of type to format for elements contained in the container
  #
  # Note that the top level format applies to Array and Hash objects contained/nested in an Array or a Hash.
  #
  # Given format mappings are merged with (default) formats and a format specified for a narrower type
  # wins over a broader.
  #
  # @param mode [String, Symbol] :strict or :extended (or :default which is the same as :strict)
  # @param string_formats [String, Hash] format tring, or a hash mapping type to a format string, and for Array and Hash types map to hash of details
  #
  def convert(value, string_formats = :default)
    options = DEFAULT_STRING_FORMATS

    value_type = TypeCalculator.infer_set(value)
    if string_formats.is_a?(String)
      # For Array and Hash, the format is given as a Hash where 'format' key is the format for the collection itself
      if Puppet::Pops::Types::PArrayType::DEFAULT.assignable?(value_type)
        # add the format given for the exact type
        string_formats = { Puppet::Pops::Types::PArrayType::DEFAULT => { 'format' => string_formats } }
      elsif Puppet::Pops::Types::PHashType::DEFAULT.assignable?(value_type)
        # add the format given for the exact type
        string_formats = { Puppet::Pops::Types::PHashType::DEFAULT => { 'format' => string_formats } }
      else
        # add the format given for the exact type
        string_formats = { value_type => string_formats }
      end
    end

    case string_formats
    when :default
    # do nothing, use default formats

    when Hash
      # Convert and validate user input
      string_formats = validate_input(string_formats)
      # Merge user given with defaults such that user options wins, merge is deep and format specific
      options = Format.merge_string_formats(DEFAULT_STRING_FORMATS, string_formats)
    else
      raise ArgumentError, "string conversion expects a Default value or a Hash of type to format mappings, got a '#{string_formats.class}'"
    end

    _convert(value_type, value, options, DEFAULT_INDENTATION)
  end

  #  # A method only used for manual debugging as the default output of the formatting rules is
  #  # very hard to read otherwise.
  #  #
  #  # @api private
  #  def dump_string_formats(f, indent = 1)
  #     return f.to_s unless f.is_a?(Hash)
  #     "{#{f.map {|k,v| "#{k.to_s} => #{dump_string_formats(v,indent+1)}"}.join(",\n#{'  '*indent}  ")}}"
  #  end

  def _convert(val_type, value, format_map, indentation)
    @string_visitor.visit_this_3(self, val_type, value, format_map, indentation)
  end
  private :_convert

  def validate_input(fmt)
    return nil if fmt.nil?
    unless fmt.is_a?(Hash)
      raise ArgumentError, "expected a hash with type to format mappings, got instance of '#{fmt.class}'"
    end

    fmt.each_with_object({}) do |entry, result|
      key, value = entry
      unless key.is_a?(Types::PAnyType)
        raise ArgumentError, "top level keys in the format hash must be data types, got instance of '#{key.class}'"
      end

      if value.is_a?(Hash)
        result[key] = validate_container_input(value)
      else
        result[key] = Format.new(value)
      end
    end
  end
  private :validate_input

  FMT_KEYS = %w[separator separator2 format string_formats].freeze

  def validate_container_input(fmt)
    if (fmt.keys - FMT_KEYS).size > 0
      raise ArgumentError, "only #{FMT_KEYS.map { |k| "'#{k}'" }.join(', ')} are allowed in a container format, got #{fmt}"
    end

    result                          = Format.new(fmt['format'])
    result.separator                = fmt['separator']
    result.separator2               = fmt['separator2']
    result.container_string_formats = validate_input(fmt['string_formats'])
    result
  end
  private :validate_container_input

  def string_PObjectType(val_type, val, format_map, indentation)
    f = get_format(val_type, format_map)
    case f.format
    when :p
      fmt = TypeFormatter.singleton
      indentation = indentation.indenting(f.alt? || indentation.is_indenting?)
      fmt = fmt.indented(indentation.level, 2) if indentation.is_indenting?
      fmt.string(val)
    when :s
      val.to_s
    when :q
      val.inspect
    else
      raise FormatError.new('Object', f.format, 'spq')
    end
  end

  def string_PObjectTypeExtension(val_type, val, format_map, indentation)
    string_PObjectType(val_type.base_type, val, format_map, indentation)
  end

  def string_PRuntimeType(val_type, val, format_map, indent)
    # Before giving up on this, and use a string representation of the unknown
    # object, a check is made to see if the object can present itself as
    # a hash or an array. If it can, then that representation is used instead.
    case val
    when Hash
      hash = val.to_hash
      # Ensure that the returned value isn't derived from Hash
      return string_PHashType(val_type, hash, format_map, indent) if hash.instance_of?(Hash)
    when Array
      array = val.to_a
      # Ensure that the returned value isn't derived from Array
      return string_PArrayType(val_type, array, format_map, indent) if array.instance_of?(Array)
    end

    f = get_format(val_type, format_map)
    case f.format
    when :s
      val.to_s
    when :p
      puppet_quote(val.to_s)
    when :q
      val.inspect
    else
      raise FormatError.new('Runtime', f.format, 'spq')
    end
  end

  # Basically string_PAnyType converts the value to a String and then formats it according
  # to the resulting type
  #
  # @api private
  def string_PAnyType(val_type, val, format_map, _)
    f = get_format(val_type, format_map)
    Kernel.format(f.orig_fmt, val)
  end

  def string_PDefaultType(val_type, val, format_map, _)
    f = get_format(val_type, format_map)
    apply_string_flags(f, case f.format
                          when :d, :s, :p
                            f.alt? ? '"default"' : 'default'
                          when :D
                            f.alt? ? '"Default"' : 'Default'
                          else
                            raise FormatError.new('Default', f.format, 'dDsp')
                          end)
  end

  # @api private
  def string_PUndefType(val_type, val, format_map, _)
    f = get_format(val_type, format_map)
    apply_string_flags(f, case f.format
                          when :n
                            f.alt? ? 'null' : 'nil'
                          when :u
                            f.alt? ? 'undefined' : 'undef'
                          when :d, :x, :X, :o, :b, :B, :e, :E, :f, :g, :G, :a, :A
                            'NaN'
                          when :v
                            'n/a'
                          when :V
                            'N/A'
                          when :s
                            f.alt? ? '""' : ''
                          when :p
                            f.alt? ? '"undef"' : 'undef'
                          else
                            raise FormatError.new('Undef', f.format,
                                                  'nudxXobBeEfgGaAvVsp')
                          end)
  end

  # @api private
  def string_PBooleanType(val_type, val, format_map, indentation)
    f = get_format(val_type, format_map)
    case f.format
    when :t
      # 'true'/'false' or 't'/'f' if in alt mode
      str_bool = val.to_s
      apply_string_flags(f, f.alt? ? str_bool[0] : str_bool)

    when :T
      # 'True'/'False' or 'T'/'F' if in alt mode
      str_bool = val.to_s.capitalize
      apply_string_flags(f, f.alt? ? str_bool[0] : str_bool)

    when :y
      # 'yes'/'no' or 'y'/'n' if in alt mode
      str_bool = val ? 'yes' : 'no'
      apply_string_flags(f, f.alt? ? str_bool[0] : str_bool)

    when :Y
      # 'Yes'/'No' or 'Y'/'N' if in alt mode
      str_bool = val ? 'Yes' : 'No'
      apply_string_flags(f, f.alt? ? str_bool[0] : str_bool)

    when :d, :x, :X, :o, :b, :B
      # Boolean in numeric form, formated by integer rule
      numeric_bool = val ? 1 : 0
      string_formats = { Puppet::Pops::Types::PIntegerType::DEFAULT => f }
      _convert(TypeCalculator.infer_set(numeric_bool), numeric_bool, string_formats, indentation)

    when :e, :E, :f, :g, :G, :a, :A
      # Boolean in numeric form, formated by float rule
      numeric_bool = val ? 1.0 : 0.0
      string_formats = { Puppet::Pops::Types::PFloatType::DEFAULT => f }
      _convert(TypeCalculator.infer_set(numeric_bool), numeric_bool, string_formats, indentation)

    when :s
      apply_string_flags(f, val.to_s)

    when :p
      apply_string_flags(f, val.inspect)

    else
      raise FormatError.new('Boolean', f.format, 'tTyYdxXobBeEfgGaAsp')
    end
  end

  # Performs post-processing of literals to apply width and precision flags
  def apply_string_flags(f, literal_str)
    if f.left || f.width || f.prec
      fmt = '%'.dup
      fmt << '-' if f.left
      fmt << f.width.to_s if f.width
      fmt << '.' << f.prec.to_s if f.prec
      fmt << 's'
      Kernel.format(fmt, literal_str)
    else
      literal_str
    end
  end
  private :apply_string_flags

  # @api private
  def string_PIntegerType(val_type, val, format_map, _)
    f = get_format(val_type, format_map)
    case f.format
    when :d, :x, :X, :o, :b, :B, :p
      Kernel.format(f.orig_fmt, val)

    when :e, :E, :f, :g, :G, :a, :A
      Kernel.format(f.orig_fmt, val.to_f)

    when :c
      char = [val].pack("U")
      char = f.alt? ? "\"#{char}\"" : char
      Kernel.format(f.orig_fmt.tr('c', 's'), char)

    when :s
      fmt = f.alt? ? 'p' : 's'
      int_str = Kernel.format('%d', val)
      Kernel.format(f.orig_fmt.gsub('s', fmt), int_str)

    else
      raise FormatError.new('Integer', f.format, 'dxXobBeEfgGaAspc')
    end
  end

  # @api private
  def string_PFloatType(val_type, val, format_map, _)
    f = get_format(val_type, format_map)
    case f.format
    when :d, :x, :X, :o, :b, :B
      Kernel.format(f.orig_fmt, val.to_i)

    when :e, :E, :f, :g, :G, :a, :A, :p
      Kernel.format(f.orig_fmt, val)

    when :s
      float_str = f.alt? ? "\"#{Kernel.format('%p', val)}\"" : Kernel.format('%p', val)
      Kernel.format(f.orig_fmt, float_str)

    else
      raise FormatError.new('Float', f.format, 'dxXobBeEfgGaAsp')
    end
  end

  # @api private
  def string_PBinaryType(val_type, val, format_map, _)
    f = get_format(val_type, format_map)
    substitute = f.alt? ? 'p' : 's'
    case f.format
    when :s
      val_to_convert = val.binary_buffer
      if !f.alt?
        # Assume it is valid UTF-8
        val_to_convert = val_to_convert.dup.force_encoding('UTF-8')
        # If it isn't
        unless val_to_convert.valid_encoding?
          # try to convert and fail with details about what is wrong
          val_to_convert = val.binary_buffer.encode('UTF-8')
        end
      else
        val_to_convert = val.binary_buffer
      end
      Kernel.format(f.orig_fmt.gsub('s', substitute), val_to_convert)

    when :p
      # width & precision applied to string, not the name of the type
      "Binary(\"#{Kernel.format(f.orig_fmt.tr('p', 's'), val.to_s)}\")"

    when :b
      Kernel.format(f.orig_fmt.gsub('b', substitute), val.relaxed_to_s)

    when :B
      Kernel.format(f.orig_fmt.gsub('B', substitute), val.to_s)

    when :u
      Kernel.format(f.orig_fmt.gsub('u', substitute), val.urlsafe_to_s)

    when :t
      # Output as the type without any data
      Kernel.format(f.orig_fmt.gsub('t', substitute), 'Binary')

    when :T
      # Output as the type without any data in all caps
      Kernel.format(f.orig_fmt.gsub('T', substitute), 'BINARY')

    else
      raise FormatError.new('Binary', f.format, 'bButTsp')
    end
  end

  # @api private
  def string_PStringType(val_type, val, format_map, _)
    f = get_format(val_type, format_map)
    case f.format
    when :s
      Kernel.format(f.orig_fmt, val)

    when :p
      apply_string_flags(f, puppet_quote(val, f.alt?))

    when :c
      c_val = val.capitalize
      f.alt? ? apply_string_flags(f, puppet_quote(c_val)) : Kernel.format(f.orig_fmt.tr('c', 's'), c_val)

    when :C
      c_val = val.split('::').map(&:capitalize).join('::')
      f.alt? ? apply_string_flags(f, puppet_quote(c_val)) : Kernel.format(f.orig_fmt.tr('C', 's'), c_val)

    when :u
      c_val = val.upcase
      f.alt? ? apply_string_flags(f, puppet_quote(c_val)) : Kernel.format(f.orig_fmt.tr('u', 's'), c_val)

    when :d
      c_val = val.downcase
      f.alt? ? apply_string_flags(f, puppet_quote(c_val)) : Kernel.format(f.orig_fmt.tr('d', 's'), c_val)

    when :t # trim
      c_val = val.strip
      f.alt? ? apply_string_flags(f, puppet_quote(c_val)) : Kernel.format(f.orig_fmt.tr('t', 's'), c_val)

    else
      raise FormatError.new('String', f.format, 'cCudspt')
    end
  end

  # Performs a '%p' formatting of the given _str_ such that the output conforms to Puppet syntax. An ascii string
  # without control characters, dollar, single-qoute, or backslash, will be quoted using single quotes. All other
  # strings will be quoted using double quotes.
  #
  # @param [String] str the string that should be formatted
  # @param [Boolean] enforce_double_quotes if true the result will be double quoted (even if single quotes would be possible)
  # @return [String] the formatted string
  #
  # @api public
  def puppet_quote(str, enforce_double_quotes = false)
    if enforce_double_quotes
      return puppet_double_quote(str)
    end

    # Assume that the string can be single quoted
    bld = "'".dup
    bld.force_encoding(str.encoding)
    escaped = false
    str.each_codepoint do |codepoint|
      # Control characters and non-ascii characters cannot be present in a single quoted string
      return puppet_double_quote(str) if codepoint < 0x20

      if escaped
        bld << 0x5c << codepoint
        escaped = false
      elsif codepoint == 0x27
        bld << 0x5c << codepoint
      elsif codepoint == 0x5c
        escaped = true
      elsif codepoint <= 0x7f
        bld << codepoint
      else
        bld << [codepoint].pack('U')
      end
    end

    # If string ended with a backslash, then that backslash must be escaped
    bld << 0x5c if escaped

    bld << "'"
    bld
  end

  def puppet_double_quote(str)
    bld = '"'.dup
    str.each_codepoint do |codepoint|
      case codepoint
      when 0x09
        bld << '\\t'
      when 0x0a
        bld << '\\n'
      when 0x0d
        bld << '\\r'
      when 0x22
        bld << '\\"'
      when 0x24
        bld << '\\$'
      when 0x5c
        bld << '\\\\'
      else
        if codepoint < 0x20
          bld << sprintf('\\u{%X}', codepoint)
        elsif codepoint <= 0x7f
          bld << codepoint
        else
          bld << [codepoint].pack('U')
        end
      end
    end
    bld << '"'
    bld
  end

  # @api private
  def string_PRegexpType(val_type, val, format_map, _)
    f = get_format(val_type, format_map)
    case f.format
    when :p
      str_regexp = PRegexpType.regexp_to_s_with_delimiters(val)
      f.orig_fmt == '%p' ? str_regexp : Kernel.format(f.orig_fmt.tr('p', 's'), str_regexp)
    when :s
      str_regexp = PRegexpType.regexp_to_s(val)
      str_regexp = puppet_quote(str_regexp) if f.alt?
      f.orig_fmt == '%s' ? str_regexp : Kernel.format(f.orig_fmt, str_regexp)
    else
      raise FormatError.new('Regexp', f.format, 'sp')
    end
  end

  def string_PArrayType(val_type, val, format_map, indentation)
    format         = get_format(val_type, format_map)
    sep            = format.separator || DEFAULT_ARRAY_FORMAT.separator
    string_formats = format.container_string_formats || DEFAULT_CONTAINER_FORMATS
    delims         = format.delimiter_pair(DEFAULT_ARRAY_DELIMITERS)

    # Make indentation active, if array is in alternative format, or if nested in indenting
    indentation = indentation.indenting(format.alt? || indentation.is_indenting?)

    case format.format
    when :a, :s, :p
      buf = ''.dup
      if indentation.breaks?
        buf << "\n"
        buf << indentation.padding
      end
      buf << delims[0]

      # Make a first pass to format each element
      children_indentation = indentation.increase(format.alt?) # tell children they are expected to indent
      mapped = val.map do |v|
        if children_indentation.first?
          children_indentation = children_indentation.subsequent
        end
        val_t = TypeCalculator.infer_set(v)
        _convert(val_t, v, is_container?(val_t) ? format_map : string_formats, children_indentation)
      end

      # compute widest run in the array, skip nested arrays and hashes
      # then if size > width, set flag if a break on each element should be performed
      if format.alt? && format.width
        widest = val.each_with_index.each_with_object([0]) do |v_i, memo|
          # array or hash breaks
          if is_a_or_h?(v_i[0])
            memo << 0
          else
            memo[-1] += mapped[v_i[1]].length
          end
        end
        widest = widest.max
        sz_break = widest > (format.width || Float::INFINITY)
      else
        sz_break = false
      end

      # output each element with breaks and padding
      children_indentation = indentation.increase(format.alt?)
      val.each_with_index do |v, i|
        str_val = mapped[i]
        if children_indentation.first?
          children_indentation = children_indentation.subsequent
          # if breaking, indent first element by one
          if sz_break && !is_a_or_h?(v)
            buf << ' '
          end
        else
          buf << sep
          # if break on each (and breaking will not occur because next is an array or hash)
          # or, if indenting, and previous was an array or hash, then break and continue on next line
          # indented.
          if (sz_break && !is_a_or_h?(v)) || (format.alt? && i > 0 && is_a_or_h?(val[i - 1]) && !is_a_or_h?(v))
            buf.rstrip! unless buf[-1] == "\n"
            buf << "\n"
            buf << children_indentation.padding
          end
        end
        # remove trailing space added by separator if followed by break
        buf.rstrip! if buf[-1] == ' ' && str_val[0] == "\n"
        buf << str_val
      end
      buf << delims[1]
      buf
    else
      raise FormatError.new('Array', format.format, 'asp')
    end
  end

  def is_a_or_h?(x)
    x.is_a?(Array) || x.is_a?(Hash)
  end

  def is_container?(t)
    case t
    when PArrayType, PHashType, PStructType, PTupleType, PObjectType
      true
    else
      false
    end
  end

  # @api private
  def string_PTupleType(val_type, val, format_map, indentation)
    string_PArrayType(val_type, val, format_map, indentation)
  end

  # @api private
  def string_PIteratorType(val_type, val, format_map, indentation)
    v = val.to_a
    _convert(TypeCalculator.infer_set(v), v, format_map, indentation)
  end

  # @api private
  def string_PHashType(val_type, val, format_map, indentation)
    format         = get_format(val_type, format_map)
    sep            = format.separator  || DEFAULT_HASH_FORMAT.separator
    assoc          = format.separator2 || DEFAULT_HASH_FORMAT.separator2
    string_formats = format.container_string_formats || DEFAULT_CONTAINER_FORMATS
    delims         = format.delimiter_pair(DEFAULT_HASH_DELIMITERS)

    if format.alt?
      sep = sep.rstrip unless sep[-1] == "\n"
      sep = "#{sep}\n"
    end

    cond_break     = ''
    padding        = ''

    case format.format
    when :a
      # Convert to array and use array rules
      array_hash = val.to_a
      _convert(TypeCalculator.infer_set(array_hash), array_hash, format_map, indentation)

    when :h, :s, :p
      indentation = indentation.indenting(format.alt? || indentation.is_indenting?)
      buf = ''.dup
      if indentation.breaks?
        buf << "\n"
        buf << indentation.padding
      end

      children_indentation = indentation.increase
      if format.alt?
        cond_break = "\n"
        padding = children_indentation.padding
      end
      buf << delims[0]
      buf << cond_break # break after opening delimiter if pretty printing
      buf << val.map do |k, v|
        key_type = TypeCalculator.infer_set(k)
        val_type = TypeCalculator.infer_set(v)
        key = _convert(key_type, k, is_container?(key_type) ? format_map : string_formats, children_indentation)
        val = _convert(val_type, v, is_container?(val_type) ? format_map : string_formats, children_indentation)
        "#{padding}#{key}#{assoc}#{val}"
      end.join(sep)
      if format.alt?
        buf << cond_break
        buf << indentation.padding
      end
      buf << delims[1]
      buf
    else
      raise FormatError.new('Hash', format.format, 'hasp')
    end
  end

  # @api private
  def string_PStructType(val_type, val, format_map, indentation)
    string_PHashType(val_type, val, format_map, indentation)
  end

  # @api private
  def string_PTypeType(val_type, val, format_map, _)
    f = get_format(val_type, format_map)
    case f.format
    when :s
      str_val = f.alt? ? "\"#{val}\"" : val.to_s
      Kernel.format(f.orig_fmt, str_val)
    when :p
      Kernel.format(f.orig_fmt.tr('p', 's'), val.to_s)
    else
      raise FormatError.new('Type', f.format, 'sp')
    end
  end

  # @api private
  def string_PURIType(val_type, val, format_map, indentation)
    f = get_format(val_type, format_map)
    case f.format
    when :p
      fmt = TypeFormatter.singleton
      indentation = indentation.indenting(f.alt? || indentation.is_indenting?)
      fmt = fmt.indented(indentation.level, 2) if indentation.is_indenting?
      fmt.string(val)
    when :s
      str_val = val.to_s
      Kernel.format(f.orig_fmt, f.alt? ? puppet_quote(str_val) : str_val)
    else
      raise FormatError.new('URI', f.format, 'sp')
    end
  end

  # Maps the inferred type of o to a formatting rule
  def get_format(val_t, format_options)
    fmt = format_options.find { |k, _| k.assignable?(val_t) }
    return fmt[1] unless fmt.nil?

    Format.new("%s")
  end
  private :get_format
end
end
end
