require "forwardable"

module Lrama
  class Grammar
    class Code < Struct.new(:type, :token_code, keyword_init: true)
      extend Forwardable

      def_delegators "token_code", :s_value, :line, :column, :references

      # $$, $n, @$, @n is translated to C code
      def translated_code
        case type
        when :user_code
          translated_user_code
        when :initial_action
          translated_initial_action_code
        end
      end

      # * ($1) error
      # * ($$) *yyvaluep
      # * (@1) error
      # * (@$) *yylocationp
      def translated_printer_code(tag)
        t_code = s_value.dup

        references.reverse.each do |ref|
          first_column = ref.first_column
          last_column = ref.last_column

          case
          when ref.value == "$" && ref.type == :dollar # $$
            # Omit "<>"
            member = tag.s_value[1..-2]
            str = "((*yyvaluep).#{member})"
          when ref.value == "$" && ref.type == :at # @$
            str = "(*yylocationp)"
          when ref.type == :dollar # $n
            raise "$#{ref.value} can not be used in %printer."
          when ref.type == :at # @n
            raise "@#{ref.value} can not be used in %printer."
          else
            raise "Unexpected. #{self}, #{ref}"
          end

          t_code[first_column..last_column] = str
        end

        return t_code
      end
      alias :translated_error_token_code :translated_printer_code

      private

      # * ($1) yyvsp[i]
      # * ($$) yyval
      # * (@1) yylsp[i]
      # * (@$) yyloc
      def translated_user_code
        t_code = s_value.dup

        references.reverse.each do |ref|
          first_column = ref.first_column
          last_column = ref.last_column

          case
          when ref.value == "$" && ref.type == :dollar # $$
            # Omit "<>"
            member = ref.tag.s_value[1..-2]
            str = "(yyval.#{member})"
          when ref.value == "$" && ref.type == :at # @$
            str = "(yyloc)"
          when ref.type == :dollar # $n
            i = -ref.position_in_rhs + ref.value
            # Omit "<>"
            member = ref.tag.s_value[1..-2]
            str = "(yyvsp[#{i}].#{member})"
          when ref.type == :at # @n
            i = -ref.position_in_rhs + ref.value
            str = "(yylsp[#{i}])"
          else
            raise "Unexpected. #{self}, #{ref}"
          end

          t_code[first_column..last_column] = str
        end

        return t_code
      end

      # * ($1) error
      # * ($$) yylval
      # * (@1) error
      # * (@$) yylloc
      def translated_initial_action_code
        t_code = s_value.dup

        references.reverse.each do |ref|
          first_column = ref.first_column
          last_column = ref.last_column

          case
          when ref.value == "$" && ref.type == :dollar # $$
            str = "yylval"
          when ref.value == "$" && ref.type == :at # @$
            str = "yylloc"
          when ref.type == :dollar # $n
            raise "$#{ref.value} can not be used in initial_action."
          when ref.type == :at # @n
            raise "@#{ref.value} can not be used in initial_action."
          else
            raise "Unexpected. #{self}, #{ref}"
          end

          t_code[first_column..last_column] = str
        end

        return t_code
      end
    end
  end
end
