module Rubinius
  class Sprinter
    def self.cache
      @cache
    end

    def self.get(format)
      if ec = @cache
        if sprinter = ec.retrieve(format)
          return sprinter
        end
      end

      sprinter = new(format)

      ec.set(format, sprinter) if ec

      sprinter
    end

    def initialize(format)
      begin
        Rubinius::Type.object_singleton_class(self).dynamic_method :call do |g|
          Builder.new(self, format, g).build
        end
      rescue Rubinius::ToolSets::Runtime::CompileError => e
        raise RuntimeError, "failed to compile printf format: #{format}", e
      end
    end

    def debug_print
      printer = Rubinius::ToolSets::Runtime::Compiler::MethodPrinter.new
      printer.input(method(:call).executable)
      printer.bytecode = true
      printer.run
    end

    def zero_two_expand_integer(int)
      int = Integer(int) unless int.kind_of? Fixnum

      return int.to_s if int < 0 or int >= 10

      "0#{int}"
    end

    def zero_expand_integer(int, width)
      int = Integer(int) unless int.kind_of? Fixnum

      str = int.to_s
      return str if width <= str.size
      diff = width - str.size

      if int < 0
        case diff
        when 1
          "-0#{-int}"
        when 2
          "-00#{-int}"
        when 3
          "-000#{-int}"
        else
          "-#{'0' * diff}#{-int}"
        end
      else
        case diff
        when 1
          "0#{str}"
        when 2
          "00#{str}"
        when 3
          "000#{str}"
        when 4
          "0000#{str}"
        else
          ("0" * diff) << str
        end
      end
    end

    def zero_expand_leader(int, width)
      if int < 0
        zero_expand_integer(int, width)
      else
        zero_expand_integer(int, width-1)
      end
    end

    def space_expand_integer(int, width)
      int = Integer(int) unless int.kind_of? Fixnum

      str = int.to_s

      width = -width if width < 0
      return str if width <= str.size
      diff = width - str.size

      case diff
      when 1
        " #{str}"
      when 2
        "  #{str}"
      when 3
        "   #{str}"
      when 4
        "    #{str}"
      else
        (" " * diff) << str
      end
    end

    def space_expand_leader(int, width)
      if int < 0
        space_expand_integer(int, width)
      else
        space_expand_integer(int, width-1)
      end
    end

    def space_expand_integer_left(int, width)
      int = Integer(int) unless int.kind_of? Fixnum

      str = int.to_s
      return str if width <= str.size
      diff = width - str.size

      case diff
      when 1
        "#{str} "
      when 2
        "#{str}  "
      when 3
        "#{str}   "
      when 4
        "#{str}    "
      else
        str + (" " * diff)
      end
    end

    def space_expand_leader_left(int, width)
      if int < 0
        space_expand_integer_left(int, width)
      else
        space_expand_integer_left(int, width-1)
      end
    end

    def as_int(int)
      if int.kind_of? Integer
        return int
      else
        Integer(int)
      end
    end

    def compute_space(int)
      if int >= 0
        " "
      else
        ""
      end
    end

    def compute_plus(int)
      if int >= 0
        "+"
      else
        ""
      end
    end

    def digit_expand_precision(int, precision)
      str = int.to_s

      diff = Integer(precision) - str.size

      if int < 0
        # account for the - sign infront
        diff += 1

        return str if diff <= 0

        case diff
        when 1
          "-0#{-int}"
        when 2
          "-00#{-int}"
        when 3
          "-000#{-int}"
        else
          "-#{'0' * diff}#{-int}"
        end
      else
        return str if diff <= 0
        case diff
        when 1
          "0#{str}"
        when 2
          "00#{str}"
        when 3
          "000#{str}"
        when 4
          "0000#{str}"
        else
          ("0" * diff) << str
        end
      end
    end

    def space_expand_left(str, width)
      width = Integer(width)

      sz = str.size

      width = -width if width < 0
      return str if sz >= width

      diff = width - sz

      case diff
      when 1
        "#{str} "
      when 2
        "#{str}  "
      when 3
        "#{str}   "
      when 4
        "#{str}    "
      else
        str + (" " * diff)
      end
    end

    def space_expand(str, width)
      width = Integer(width)
      return space_expand_left(str, -width) if width < 0

      sz = str.size

      return str if sz >= width

      diff = width - sz

      case diff
      when 1
        " #{str}"
      when 2
        "  #{str}"
      when 3
        "   #{str}"
      when 4
        "    #{str}"
      else
        (" " * diff) << str
      end
    end

    class Builder
      def initialize(code, format, g)
        @code, @format = code, format

        @append_parts = 0

        @g = g
      end

      def build
        self.parse

        @g.required_args = @arg_count

        @g.total_args = @arg_count + 1
        # We won't use it, but we accept a splat; our semantics require
        # that we ignore any excess arguments provided.
        @g.splat_index = @arg_count

        @g.local_count = @arg_count + 1
        if @index_mode == :absolute
          @g.local_names = (0...@arg_count).map { |i| :"#{i + 1}$" } + [:splat]
        else
          @g.local_names = (0...@arg_count).map { |i| :"arg#{i}" } + [:splat]
        end

        @g.string_build @append_parts
        @g.ret
      end

      def invert
        @g.push_int 0
        @g.swap
        @g.send :-, 1
      end

      def is_negative
        @g.push_int 0
        @g.send :<, 1
      end

      def justify(direction, may_be_negative=true)
        if may_be_negative && direction != :ljust
          width_done = @g.new_label

          @g.dup
          is_negative
          if_false do
            @g.send direction, 1
            @g.goto width_done
          end

          invert
          @g.send :ljust, 1

          width_done.set!
        else
          @g.send direction, 1
        end
      end

      def next_index(specified=nil)
        if specified
          specified = specified.to_i
          raise ArgumentError, "invalid positional index" if specified == 0

          if @index_mode == :relative
            raise ArgumentError, "unnumbered mixed with numbered"
          end

          @index_mode = :absolute
          @arg_count = specified if specified > @arg_count
          specified - 1
        else
          if @index_mode == :absolute
            raise ArgumentError, "unnumbered mixed with numbered"
          end
          @index_mode = :relative
          (@arg_count += 1) - 1
        end
      end

      def append_literal(str)
        @g.push_literal str
        append_str
      end

      def append_str
        @append_parts += 1
      end

      def encode_value(str)
        str.force_encoding(@format.encoding)
        str
      end

      def is_zero
        @g.push_int 0
        @g.send :equal?, 1
      end

      class Atom
        def initialize(b, g, format_code, flags, name = nil)
          @b, @g = b, g
          @format_code, @flags = format_code, flags
          @name = name

          @f_alt = flags.index(?#)
          @f_zero = flags.index(?0)
          @f_plus = flags.index(?+)
          @f_ljust = flags.index(?-)
          @f_space = flags.index(?\ )

          @just_dir = @f_ljust ? :ljust : :rjust

          @prefix = PREFIX[@format_code] if @f_alt

          @full_leader_size = @prefix ? @prefix.size : 0
          @full_leader_size += 1 if @f_plus || @f_space
        end

        def prepend_prefix
          @g.push_literal @prefix
          @g.string_dup
          @g.string_append
        end

        def set_value(ref)
          unless @name
            @field_index = @b.next_index(ref)
          end
        end

        def set_width(full, ref, static)
          @width_static = static && static.to_i
          if !@name && full && !static
            @width_index = @b.next_index(ref)
          end

          @has_width = @width_static || @width_index
        end

        def set_precision(full, ref, static)
          @prec_static = static && static.to_i
          if !@name && full && !static
            @prec_index = @b.next_index(ref)
          end

          @has_precision = @prec_static || @prec_index
        end

        def push_value
          if @name
            @g.push_local 0
            @g.push_literal @name
            @g.send :fetch, 1
          else
            @g.push_local @field_index
          end
        end

        def push_width_value
          if @width_static
            @g.push_int @width_static
          elsif
            @g.push_local @width_index
          else
            raise "No width to push"
          end
        end

        def push_width(adjust=true)
          if @width_static
            raise ArgumentError, "width too big" unless @width_static.class == Fixnum
            if adjust && @full_leader_size > 0
              @g.push_int(@width_static - @full_leader_size)
            else
              @g.push_int @width_static
            end

          elsif @width_index
            @g.push_local @width_index

            @b.force_type :Fixnum, :Integer do
              # If we had to do a conversion, we check we ended up
              # with a Fixnum
              @g.dup
              @b.push_Fixnum
              @g.swap
              @g.kind_of

              @b.if_false do
                @b.raise_ArgumentError "width too big"
              end
            end

          else
            raise "push without a width"

          end
        end

        def push_precision_value
          if @prec_static
            @g.push_int @prec_static
          else
            @g.push_local @prec_index
          end
        end

        def push_precision
          if @prec_static
            raise ArgumentError, "precision too big" unless @prec_static.class == Fixnum
            @g.push_int @prec_static

          elsif @prec_index
            @g.push_local @prec_index

            @b.force_type :Fixnum, :Integer do
              # If we had to do a conversion, we check we ended up
              # with a Fixnum
              @g.dup
              @b.push_Fixnum
              @g.swap
              @g.kind_of

              @b.if_false do
                @b.raise_ArgumentError "precision too big"
              end
            end

          else
            raise "push without a precision"

          end
        end

        def push_format_string
          float_format_code = @format_code

          leader = "%#{@flags}"
          if !@width_index && !@prec_index
            leader << @width_static.to_s if @width_static
            leader << ".#{@prec_static}" if @prec_static
            @g.push_literal "#{leader}#{float_format_code}"
          else
            format_parts = 1
            if @prec_static
              @g.push_literal ".#{@prec_static}#{float_format_code}"
            else
              @g.push_literal(float_format_code)
              if @prec_index
                push_precision
                @g.send :to_s, 0
                format_parts += 1

                if @width_index
                  @g.push_literal "."
                  format_parts += 1
                end
              end
            end

            if @width_static
              leader << @width_static.to_s
            elsif @width_index
              push_width
              @g.send :to_s, 0
              format_parts += 1
            end
            leader << "." if @prec_index && !@width_index

            @g.push_literal leader
            @g.string_dup
            format_parts.times do
              @g.string_append
            end
          end
        end

        def positive_sign
          if @f_plus
            '+'
          elsif @f_space
            ' '
          else
            ''
          end
        end

        def justify_width(adjust=true)
          if @has_width
            push_width adjust
            @b.justify @just_dir, @width_static.nil?
          end
        end

        def zero_pad(pad="0")
          if @has_precision
            push_precision
          elsif @has_width && @f_zero
            push_width true
          else
            # exit early if no width has been pushed
            return
          end

          # let the caller adjust the width if needed
          yield if block_given?

          @g.push_literal pad
          @g.send :rjust, 2
        end

        attr_reader :width_static

        def leader?
          @full_leader_size > 0
        end
      end

      class CharAtom < Atom
        def bytecode
          push_value
          # We need to push the original value here
          # because we have to possibly make multiple
          # attempts to coerce it to either a string
          # or an integer. If we don't duplicate we can't
          # retry it for another type with the same original
          # value.
          @g.dup

          @b.try_type :String, :to_str do
            # Attempt failed, pop the nil value
            # and retry for Integer
            @g.pop
            @g.dup
            @b.force_type :Fixnum, :Integer
            @g.send :chr, 0
          end

          @g.dup
          @g.send :length, 0
          @g.push_int 1
          @g.send :equal?, 1
          @b.if_false do
            @b.raise_ArgumentError "%c requires a character"
          end

          justify_width

          # Swap the given value with the coerced value and
          # pop off the given value afterwards
          @g.swap
          @g.pop
          @b.append_str
        end
      end

      class IntegerAtom < Atom
        def fast_common_case?
          @f_zero && @width_static && !@has_precision && !@f_space && !@f_plus && !@f_ljust
        end

        def expand_with_width
          if @f_ljust
            if @f_space || @f_plus
              @g.send :space_expand_leader_left, 2
            else
              @g.send :space_expand_integer_left, 2
            end
          else
            if @f_zero
              if @f_space || @f_plus
                @g.send :zero_expand_leader, 2
              else
                @g.send :zero_expand_integer, 2
              end
            else
              if @f_space || @f_plus
                @g.send :space_expand_leader, 2
              else
                @g.send :space_expand_integer, 2
              end
            end
          end
        end

        def bytecode
          if fast_common_case?
            @g.push_self

            push_value

            if @width_static == 2
              @g.send :zero_two_expand_integer, 1
            else
              @g.push_int @width_static
              @g.send :zero_expand_integer, 2
            end

            @b.append_str
          else
            @g.push_self
            push_value
            @g.send :as_int, 1

            val_idx = @g.new_stack_local
            @g.set_stack_local val_idx
            @g.pop

            # generic case

            if @f_space
              @g.push_self
              @g.push_stack_local val_idx
              @g.send :compute_space, 1
              @b.append_str
            elsif @f_plus
              @g.push_self
              @g.push_stack_local val_idx
              @g.send :compute_plus, 1
              @b.append_str
            end

            if @has_precision
              @g.push_self
              @g.push_stack_local val_idx

              push_precision_value

              @g.send :digit_expand_precision, 2

              if @has_width
                @g.push_self
                @g.swap
                push_width_value

                if @f_ljust
                  @g.send :space_expand_left, 2
                else
                  @g.send :space_expand, 2
                end
              end

              @b.append_str

            elsif @has_width
              @g.push_self
              @g.push_stack_local val_idx

              push_width_value

              expand_with_width

              @b.append_str
            else
              @g.push_stack_local val_idx
              @g.send :to_s, 0

              @b.append_str
            end
          end
        end
      end

      class ExtIntegerAtom < Atom
        def pad_negative_int(padding)
          zero_pad(padding) do
            # decrease the width by 2 to account for the ".." below
            @g.push_int 2
            @g.send :-, 1
          end

          @g.push_literal ".."
          @g.string_dup
          @g.string_append
        end

        def prepend_prefix_bytecode
          skip_prefix = @g.new_label

          # For 'x' and 'X', don't prepend the "0x" prefix
          # if the value is zero
          if @format_code == 'x' || @format_code == 'X'
            push_value
            @b.is_zero
            @g.goto_if_true skip_prefix
          end

          prepend_prefix

          skip_prefix.set!
        end

        def format_negative_int(radix)
          # (num + radix ** num.to_s(radix).size).to_s(radix)
          @g.push_int radix
          @g.dup_many 2
          @g.send :to_s, 1
          @g.send :size, 0
          @g.send :**, 1
          @g.send :+, 1
          @g.push_int radix
          @g.send :to_s, 1

          (radix - 1).to_s(radix)
        end

        def bytecode
          radix = RADIX[@format_code]

          push_value

          # Bignum is obviously also perfectly acceptable. But we
          # just address the most common case by avoiding the call
          # if we've been given a Fixnum. The call is enough
          # overhead to bother, but not something to panic about.
          @b.force_type :Fixnum, :Integer


          if @f_plus || @f_space
            @g.dup

            # stash away whether it's negative
            @b.is_negative
            @g.dup
            @g.move_down 2

            @b.if_true do
              # but treat it as positive for now
              @b.invert
            end

            @g.push_int radix
            @g.send :to_s, 1
          else
            have_formatted = @g.new_label

            @g.dup
            @b.is_negative

            @b.if_false do
              @g.push_int radix
              @g.send :to_s, 1
              @g.goto have_formatted
            end

            padding = format_negative_int(radix)
            pad_negative_int(padding)

            have_formatted.set!
          end

          # 'B' also returns an uppercase string, but there, the
          # only alpha character is in the prefix -- and that's
          # already uppercase
          if @format_code == 'X'
            @g.send :upcase, 0
          end

          zero_pad

          if @prefix
            prepend_prefix_bytecode
          end

          if @f_plus || @f_space
            append_sign = @g.new_label

            @g.swap
            @b.if_true do
              @g.push_literal '-'

              @g.goto append_sign
            end

            @g.push_literal positive_sign

            append_sign.set!
            @g.string_dup
            @g.string_append
          end


          if @has_precision || !@f_zero
            justify_width false
          end

          @b.append_str
        end
      end

      class StringAtom < Atom
        def string_justify
          # by default, f_space changes the padding introduced
          # by justify_width, but for string, we want to go
          # ahead and actually ignore it not have it count
          # against the justification space width.
          if @f_space
            @full_leader_size -= 1
          end

          justify_width

          if @has_precision
            @g.push_int 0
            push_precision
            @g.send :[], 2
          end
        end

        def bytecode
          push_value
          @b.force_type :String

          string_justify

          @b.append_str
        end
      end

      class InspectAtom < StringAtom
        def bytecode
          push_value
          @g.send :inspect, 0

          string_justify

          @b.append_str
        end
      end

      class FloatAtom < Atom
        def bytecode
          push_value
          @b.force_type :Float

          format_done = @g.new_label

          @g.dup
          @g.send :finite?, 0

          @b.if_true do
            push_format_string
            @g.send :to_s_formatted, 1, true

            @g.goto format_done
          end

          formatted_non_finite = @g.new_label

          @g.dup
          @g.send :nan?, 0

          @b.if_false do
            @b.is_negative

            @b.if_false do
              @g.push_literal "#{positive_sign}Inf"
              @g.goto formatted_non_finite
            end
            @g.push_literal '-Inf'
            @g.goto formatted_non_finite
          end

          @g.pop
          @g.push_literal 'NaN'

          formatted_non_finite.set!
          justify_width false

          format_done.set!

          @b.append_str
        end
      end

      class LiteralAtom < Atom
        def set_value(ref)
          @value = ref
        end

        def bytecode
          @b.append_literal(@value)
        end
      end

      def push_Kernel
        @g.push_const :Kernel
      end

      def push_Fixnum
        @g.push_const :Fixnum
      end

      def push_String
        @g.push_const :String
      end

      def push_Hash
        @g.push_const :Hash
      end

      def raise_ArgumentError(msg)
        @g.push_const :ArgumentError
        @g.push_literal msg
        @g.send :new, 1
        @g.raise_exc
      end

      def force_type(klass, method=klass)
        @g.dup
        @g.push_const klass
        @g.swap
        @g.kind_of
        if_false do
          @g.push_self
          @g.swap
          @g.send method, 1, true

          yield if block_given?
        end
      end

      def try_type(klass, method)
        @g.dup
        @g.push_const klass
        @g.swap
        @g.kind_of
        if_false do
          @g.push_type
          @g.swap
          @g.push_const klass
          @g.push_literal method
          @g.send :check_convert_type, 3
          @g.dup
          if_false do
            yield if block_given?
          end
        end
      end

      def if_true
        l = @g.new_label
        @g.goto_if_false l
        yield
        l.set!
      end

      def if_false
        l = @g.new_label
        @g.goto_if_true l
        yield
        l.set!
      end

      def parse
        @arg_count = 0
        @index_mode = nil

        atoms = []

        # Always push an empty string at first for correct
        # encoding protocols
        atom = LiteralAtom.new(self, @g, "", "")
        atom.set_value(encode_value(""))

        atoms << atom
        pos = 0
        while match = RE.match_start(@format, pos)
          pos = match.full.at(1)

          _,
          plain_string,
          whole_format,
          name_format,
          flags_a,
          field_ref_a,
          flags_b,
          width_full, width_ref, width_static,
          prec_full, prec_ref, prec_static,
          field_ref_b,
          format_code,
          literal_char,
          name_reference,
          invalid_format = *match

          flags = "#{flags_a}#{flags_b}"


          if plain_string
            atom = LiteralAtom.new(self, @g, format_code, flags)
            atom.set_value(plain_string)
            atoms << atom
          elsif literal_char
            atom = LiteralAtom.new(self, @g, format_code, flags)
            atom.set_value(literal_char)
            atoms << atom
          elsif invalid_format || (field_ref_a && field_ref_b)
            raise ArgumentError, "malformed format string: #{@format.inspect}"
          else
            field_ref = field_ref_a || field_ref_b

            if name_reference
              format_code = "s"
              @index_mode = :name
              name = name_reference[2...-1].to_sym
            elsif name_format
              @index_mode = :name
              name = name_format[1...-1].to_sym
            end

            klass = AtomMap[format_code[0]]

            unless klass
              raise ArgumentError, "unknown format type - #{format_code}"
            end

            atom = klass.new(self, @g, format_code, flags, name)
            atom.set_width width_full, width_ref, width_static
            atom.set_precision prec_full, prec_ref, prec_static
            atom.set_value field_ref
            atoms << atom
          end
        end

        if @index_mode == :name
          exception = @g.new_label
          continue  = @g.new_label

          @arg_count = 1
          @g.passed_arg @arg_count
          @g.goto_if_true exception
          push_Hash
          @g.push_local 0
          @g.kind_of
          @g.goto_if_false exception
          @g.goto continue

          exception.set!

          raise_ArgumentError "named format string needs single Hash argument"

          continue.set!
        elsif @index_mode != :absolute
          no_exception = @g.new_label

          # If we've used relative arguments, and $DEBUG is true, we
          # throw an exception if passed more arguments than we need.

          # Check this first; it's much faster, and generally false
          @g.passed_arg @arg_count
          @g.goto_if_false no_exception

          gva = Rubinius::ToolSets::Runtime::AST::GlobalVariableAccess
          gva.new(1, :$DEBUG).bytecode(@g)
          @g.goto_if_false no_exception

          raise_ArgumentError "too many arguments for format string"

          no_exception.set!
        end

        atoms.each do |atom|
          atom.bytecode
        end
      end
    end
  end
end
