require "erb"
require "forwardable"
require "lrama/report/duration"

module Lrama
  class Output
    extend Forwardable
    include Report::Duration

    attr_reader :grammar_file_path, :context, :grammar, :error_recovery

    def_delegators "@context", :yyfinal, :yylast, :yyntokens, :yynnts, :yynrules, :yynstates,
                               :yymaxutok, :yypact_ninf, :yytable_ninf

    def_delegators "@grammar", :eof_symbol, :error_symbol, :undef_symbol, :accept_symbol

    def initialize(
      out:, output_file_path:, template_name:, grammar_file_path:,
      header_out: nil, header_file_path: nil,
      context:, grammar:, error_recovery: false
    )
      @out = out
      @output_file_path = output_file_path
      @template_name = template_name
      @grammar_file_path = grammar_file_path
      @header_out = header_out
      @header_file_path = header_file_path
      @context = context
      @grammar = grammar
      @error_recovery = error_recovery
    end

    if ERB.instance_method(:initialize).parameters.last.first == :key
      def self.erb(input)
        ERB.new(input, trim_mode: '-')
      end
    else
      def self.erb(input)
        ERB.new(input, nil, '-')
      end
    end

    def eval_template(file, path)
      erb = self.class.erb(File.read(file))
      erb.filename = file
      tmp = erb.result_with_hash(context: @context, output: self)
      replace_special_variables(tmp, path)
    end

    def render
      report_duration(:render) do
        tmp = eval_template(template_file, @output_file_path)
        @out << tmp

        if @header_file_path
          tmp = eval_template(header_template_file, @header_file_path)

          if @header_out
            @header_out << tmp
          else
            File.write(@header_file_path, tmp)
          end
        end
      end
    end

    # A part of b4_token_enums
    def token_enums
      str = ""

      @context.yytokentype.each do |s_value, token_id, display_name|
        s = sprintf("%s = %d%s", s_value, token_id, token_id == yymaxutok ? "" : ",")

        if display_name
          str << sprintf("    %-30s /* %s  */\n", s, display_name)
        else
          str << sprintf("    %s\n", s)
        end
      end

      str
    end

    # b4_symbol_enum
    def symbol_enum
      str = ""

      last_sym_number = @context.yysymbol_kind_t.last[1]
      @context.yysymbol_kind_t.each do |s_value, sym_number, display_name|
        s = sprintf("%s = %d%s", s_value, sym_number, (sym_number == last_sym_number) ? "" : ",")

        if display_name
          str << sprintf("  %-40s /* %s  */\n", s, display_name)
        else
          str << sprintf("  %s\n", s)
        end
      end

      str
    end

    def yytranslate
      int_array_to_string(@context.yytranslate)
    end

    def yytranslate_inverted
      int_array_to_string(@context.yytranslate_inverted)
    end

    def yyrline
      int_array_to_string(@context.yyrline)
    end

    def yytname
      string_array_to_string(@context.yytname) + " YY_NULLPTR"
    end

    # b4_int_type_for
    def int_type_for(ary)
      min = ary.min
      max = ary.max

      case
      when (-127 <= min && min <= 127) && (-127 <= max && max <= 127)
        "yytype_int8"
      when (0 <= min && min <= 255) && (0 <= max && max <= 255)
        "yytype_uint8"
      when (-32767 <= min && min <= 32767) && (-32767 <= max && max <= 32767)
        "yytype_int16"
      when (0 <= min && min <= 65535) && (0 <= max && max <= 65535)
        "yytype_uint16"
      else
        "int"
      end
    end

    def symbol_actions_for_printer
      str = ""

      @grammar.symbols.each do |sym|
        next unless sym.printer

        str << <<-STR
    case #{sym.enum_name}: /* #{sym.comment}  */
#line #{sym.printer.lineno} "#{@grammar_file_path}"
         #{sym.printer.translated_code(sym.tag)}
#line [@oline@] [@ofile@]
        break;

        STR
      end

      str
    end

    # b4_user_initial_action
    def user_initial_action(comment = "")
      return "" unless @grammar.initial_action

      <<-STR
        #{comment}
#line #{@grammar.initial_action.line} "#{@grammar_file_path}"
        #{@grammar.initial_action.translated_code}
      STR
    end

    def symbol_actions_for_error_token
      str = ""

      @grammar.symbols.each do |sym|
        next unless sym.error_token

        str << <<-STR
    case #{sym.enum_name}: /* #{sym.comment}  */
#line #{sym.error_token.lineno} "#{@grammar_file_path}"
         #{sym.error_token.translated_code(sym.tag)}
#line [@oline@] [@ofile@]
        break;

        STR
      end

      str
    end

    # b4_user_actions
    def user_actions
      str = ""

      @context.states.rules.each do |rule|
        next unless rule.code

        rule = rule
        code = rule.code
        spaces = " " * (code.column - 1)

        str << <<-STR
  case #{rule.id + 1}: /* #{rule.as_comment}  */
#line #{code.line} "#{@grammar_file_path}"
#{spaces}#{rule.translated_code}
#line [@oline@] [@ofile@]
    break;

        STR
      end

      str << <<-STR

#line [@oline@] [@ofile@]
      STR

      str
    end

    def omit_braces_and_blanks(param)
      param[1..-2].strip
    end

    # b4_parse_param
    def parse_param
      if @grammar.parse_param
        omit_braces_and_blanks(@grammar.parse_param)
      else
        ""
      end
    end

    def lex_param
      if @grammar.lex_param
        omit_braces_and_blanks(@grammar.lex_param)
      else
        ""
      end
    end

    # b4_user_formals
    def user_formals
      if @grammar.parse_param
        ", #{parse_param}"
      else
        ""
      end
    end

    # b4_user_args
    def user_args
      if @grammar.parse_param
        ", #{parse_param_name}"
      else
        ""
      end
    end

    def extract_param_name(param)
      param[/\b([a-zA-Z0-9_]+)(?=\s*\z)/]
    end

    def parse_param_name
      if @grammar.parse_param
        extract_param_name(parse_param)
      else
        ""
      end
    end

    def lex_param_name
      if @grammar.lex_param
        extract_param_name(lex_param)
      else
        ""
      end
    end

    # b4_parse_param_use
    def parse_param_use(val, loc)
      str = <<-STR
  YY_USE (#{val});
  YY_USE (#{loc});
      STR

      if @grammar.parse_param
        str << "  YY_USE (#{parse_param_name});"
      end

      str
    end

    # b4_yylex_formals
    def yylex_formals
      ary = ["&yylval", "&yylloc"]

      if @grammar.lex_param
        ary << lex_param_name
      end

      "(#{ary.join(', ')})"
    end

    # b4_table_value_equals
    def table_value_equals(table, value, literal, symbol)
      if literal < table.min || table.max < literal
        "0"
      else
        "((#{value}) == #{symbol})"
      end
    end

    # b4_yyerror_args
    def yyerror_args
      ary = ["&yylloc"]

      if @grammar.parse_param
        ary << parse_param_name
      end

      "#{ary.join(', ')}"
    end

    def template_basename
      File.basename(template_file)
    end

    def aux
      @grammar.aux
    end

    def int_array_to_string(ary)
      last = ary.count - 1

      s = ary.each_with_index.each_slice(10).map do |slice|
        str = "  "

        slice.each do |e, i|
          str << sprintf("%6d%s", e, (i == last) ? "" : ",")
        end

        str
      end

      s.join("\n")
    end

    def spec_mapped_header_file
      @header_file_path
    end

    def b4_cpp_guard__b4_spec_mapped_header_file
      if @header_file_path
        "YY_YY_" + @header_file_path.gsub(/[^a-zA-Z_0-9]+/, "_").upcase + "_INCLUDED"
      else
        ""
      end
    end

    private

    def template_file
      File.join(template_dir, @template_name)
    end

    def header_template_file
      File.join(template_dir, "bison/yacc.h")
    end

    def template_dir
      File.expand_path("../../../template", __FILE__)
    end

    def string_array_to_string(ary)
      str = ""
      tmp = " "

      ary.each do |s|
        s = s.gsub('\\', '\\\\\\\\')
        s = s.gsub('"', '\\"')

        if (tmp + s + " \"\",").length > 75
          str << tmp << "\n"
          tmp = "  \"#{s}\","
        else
          tmp << " \"#{s}\","
        end
      end

      str << tmp
    end

    def replace_special_variables(str, ofile)
      str.each_line.with_index(1).map do |line, i|
        line.gsub!("[@oline@]", (i + 1).to_s)
        line.gsub!("[@ofile@]", "\"#{ofile}\"")
        line
      end.join
    end
  end
end
