module Lrama
  class StatesReporter
    include Lrama::Report::Duration

    def initialize(states)
      @states = states
    end

    def report(io, **options)
      report_duration(:report) do
        _report(io, **options)
      end
    end

    private

    def _report(io, grammar: false, states: false, itemsets: false, lookaheads: false, solved: false, verbose: false)
      # TODO: Unused terms
      # TODO: Unused rules

      report_conflicts(io)
      report_grammar(io) if grammar
      report_states(io, itemsets, lookaheads, solved, verbose)
    end

    def report_conflicts(io)
      has_conflict = false

      @states.states.each do |state|
        messages = []
        cs = state.conflicts.group_by(&:type)
        if cs[:shift_reduce]
          messages << "#{cs[:shift_reduce].count} shift/reduce"
        end

        if cs[:reduce_reduce]
          messages << "#{cs[:reduce_reduce].count} reduce/reduce"
        end

        if !messages.empty?
          has_conflict = true
          io << "State #{state.id} conflicts: #{messages.join(', ')}\n"
        end
      end

      if has_conflict
        io << "\n\n"
      end
    end

    def report_grammar(io)
      io << "Grammar\n"
      last_lhs = nil

      @states.rules.each do |rule|
        if rule.rhs.empty?
          r = "ε"
        else
          r = rule.rhs.map(&:display_name).join(" ")
        end

        if rule.lhs == last_lhs
          io << sprintf("%5d %s| %s\n", rule.id, " " * rule.lhs.display_name.length, r)
        else
          io << "\n"
          io << sprintf("%5d %s: %s\n", rule.id, rule.lhs.display_name, r)
        end

        last_lhs = rule.lhs
      end
      io << "\n\n"
    end

    def report_states(io, itemsets, lookaheads, solved, verbose)
      @states.states.each do |state|
        # Report State
        io << "State #{state.id}\n\n"

        # Report item
        last_lhs = nil
        list = itemsets ? state.items : state.kernels
        list.sort_by {|i| [i.rule_id, i.position] }.each do |item|
          rule = item.rule
          position = item.position
          if rule.rhs.empty?
            r = "ε •"
          else
            r = rule.rhs.map(&:display_name).insert(position, "•").join(" ")
          end
          if rule.lhs == last_lhs
            l = " " * rule.lhs.id.s_value.length + "|"
          else
            l = rule.lhs.id.s_value + ":"
          end
          la = ""
          if lookaheads && item.end_of_rule?
            reduce = state.find_reduce_by_item!(item)
            look_ahead = reduce.selected_look_ahead
            if !look_ahead.empty?
              la = "  [#{look_ahead.map(&:display_name).join(", ")}]"
            end
          end
          last_lhs = rule.lhs

          io << sprintf("%5i %s %s%s\n", rule.id, l, r, la)
        end
        io << "\n"


        # Report shifts
        tmp = state.term_transitions.select do |shift, _|
          !shift.not_selected
        end.map do |shift, next_state|
          [shift.next_sym, next_state.id]
        end
        max_len = tmp.map(&:first).map(&:display_name).map(&:length).max
        tmp.each do |term, state_id|
          io << "    #{term.display_name.ljust(max_len)}  shift, and go to state #{state_id}\n"
        end
        io << "\n" if !tmp.empty?


        # Report error caused by %nonassoc
        nl = false
        tmp = state.resolved_conflicts.select do |resolved|
          resolved.which == :error
        end.map do |error|
          error.symbol.display_name
        end
        max_len = tmp.map(&:length).max
        tmp.each do |name|
          nl = true
          io << "    #{name.ljust(max_len)}  error (nonassociative)\n"
        end
        io << "\n" if !tmp.empty?


        # Report reduces
        nl = false
        max_len = state.non_default_reduces.flat_map(&:look_ahead).compact.map(&:display_name).map(&:length).max || 0
        max_len = [max_len, "$default".length].max if state.default_reduction_rule
        ary = []

        state.non_default_reduces.each do |reduce|
          reduce.look_ahead.each do |term|
            ary << [term, reduce]
          end
        end

        ary.sort_by do |term, reduce|
          term.number
        end.each do |term, reduce|
          rule = reduce.item.rule
          io << "    #{term.display_name.ljust(max_len)}  reduce using rule #{rule.id} (#{rule.lhs.display_name})\n"
          nl = true
        end

        if r = state.default_reduction_rule
          nl = true
          s = "$default".ljust(max_len)

          if r.initial_rule?
            io << "    #{s}  accept\n"
          else
            io << "    #{s}  reduce using rule #{r.id} (#{r.lhs.display_name})\n"
          end
        end
        io << "\n" if nl


        # Report nonterminal transitions
        tmp = []
        max_len = 0
        state.nterm_transitions.each do |shift, next_state|
          nterm = shift.next_sym
          tmp << [nterm, next_state.id]
          max_len = [max_len, nterm.id.s_value.length].max
        end
        tmp.uniq!
        tmp.sort_by! do |nterm, state_id|
          nterm.number
        end
        tmp.each do |nterm, state_id|
          io << "    #{nterm.id.s_value.ljust(max_len)}  go to state #{state_id}\n"
        end
        io << "\n" if !tmp.empty?


        if solved
          # Report conflict resolutions
          state.resolved_conflicts.each do |resolved|
            io << "    #{resolved.report_message}\n"
          end
          io << "\n" if !state.resolved_conflicts.empty?
        end


        if verbose
          # Report direct_read_sets
          io << "  [Direct Read sets]\n"
          direct_read_sets = @states.direct_read_sets
          @states.nterms.each do |nterm|
            terms = direct_read_sets[[state.id, nterm.token_id]]
            next if !terms
            next if terms.empty?

            str = terms.map {|sym| sym.id.s_value }.join(", ")
            io << "    read #{nterm.id.s_value}  shift #{str}\n"
          end
          io << "\n"


          # Reprot reads_relation
          io << "  [Reads Relation]\n"
          @states.nterms.each do |nterm|
            a = @states.reads_relation[[state.id, nterm.token_id]]
            next if !a

            a.each do |state_id2, nterm_id2|
              n = @states.nterms.find {|n| n.token_id == nterm_id2 }
              io << "    (State #{state_id2}, #{n.id.s_value})\n"
            end
          end
          io << "\n"


          # Reprot read_sets
          io << "  [Read sets]\n"
          read_sets = @states.read_sets
          @states.nterms.each do |nterm|
            terms = read_sets[[state.id, nterm.token_id]]
            next if !terms
            next if terms.empty?

            terms.each do |sym|
              io << "    #{sym.id.s_value}\n"
            end
          end
          io << "\n"


          # Reprot includes_relation
          io << "  [Includes Relation]\n"
          @states.nterms.each do |nterm|
            a = @states.includes_relation[[state.id, nterm.token_id]]
            next if !a

            a.each do |state_id2, nterm_id2|
              n = @states.nterms.find {|n| n.token_id == nterm_id2 }
              io << "    (State #{state.id}, #{nterm.id.s_value}) -> (State #{state_id2}, #{n.id.s_value})\n"
            end
          end
          io << "\n"


          # Report lookback_relation
          io << "  [Lookback Relation]\n"
          @states.rules.each do |rule|
            a = @states.lookback_relation[[state.id, rule.id]]
            next if !a

            a.each do |state_id2, nterm_id2|
              n = @states.nterms.find {|n| n.token_id == nterm_id2 }
              io << "    (Rule: #{rule.to_s}) -> (State #{state_id2}, #{n.id.s_value})\n"
            end
          end
          io << "\n"


          # Reprot follow_sets
          io << "  [Follow sets]\n"
          follow_sets = @states.follow_sets
          @states.nterms.each do |nterm|
            terms = follow_sets[[state.id, nterm.token_id]]

            next if !terms

            terms.each do |sym|
              io << "    #{nterm.id.s_value} -> #{sym.id.s_value}\n"
            end
          end
          io << "\n"


          # Report LA
          io << "  [Look-Ahead Sets]\n"
          tmp = []
          max_len = 0
          @states.rules.each do |rule|
            syms = @states.la[[state.id, rule.id]]
            next if !syms

            tmp << [rule, syms]
            max_len = ([max_len] + syms.map {|s| s.id.s_value.length }).max
          end
          tmp.each do |rule, syms|
            syms.each do |sym|
              io << "    #{sym.id.s_value.ljust(max_len)}  reduce using rule #{rule.id} (#{rule.lhs.id.s_value})\n"
            end
          end
          io << "\n" if !tmp.empty?
        end


        # End of Report State
        io << "\n"
      end
    end
  end
end