# encoding: utf-8

require 'astrolabe/builder'
require 'digest/md5'

module RuboCop
  # ProcessedSource contains objects which are generated by Parser
  # and other information such as disabled lines for cops.
  # It also provides a convenient way to access source lines.
  class ProcessedSource
    STRING_SOURCE_NAME = '(string)'
    attr_reader :path, :buffer, :ast, :comments, :tokens, :diagnostics,
                :parser_error, :raw_source

    def self.from_file(path)
      new(File.read(path), path)
    end

    def initialize(source, path = nil)
      # In Ruby 2, source code encoding defaults to UTF-8. We follow the same
      # principle regardless of which Ruby version we're running under.
      # Encoding comments will override this setting.
      source.force_encoding(Encoding::UTF_8)

      @raw_source = source
      @path = path
      @diagnostics = []
      parse(source)
    end

    def comment_config
      @comment_config ||= CommentConfig.new(self)
    end

    def disabled_line_ranges
      comment_config.cop_disabled_line_ranges
    end

    def lines
      @lines ||= raw_source.lines.map(&:chomp)
    end

    def [](*args)
      lines[*args]
    end

    def valid_syntax?
      return false if @parser_error
      @diagnostics.none? { |d| [:error, :fatal].include?(d.level) }
    end

    # Raw source checksum for tracking infinite loops.
    def checksum
      Digest::MD5.hexdigest(@raw_source)
    end

    private

    def parse(source)
      buffer_name = @path || STRING_SOURCE_NAME
      @buffer = Parser::Source::Buffer.new(buffer_name, 1)

      begin
        @buffer.source = source
      rescue EncodingError => error
        @parser_error = error
        return
      end

      parser = create_parser

      begin
        @ast, @comments, tokens = parser.tokenize(@buffer)
      rescue Parser::SyntaxError # rubocop:disable Lint/HandleExceptions
        # All errors are in diagnostics. No need to handle exception.
      end

      @tokens = tokens.map { |t| Token.from_parser_token(t) } if tokens
    end

    def create_parser
      builder = Astrolabe::Builder.new

      Parser::CurrentRuby.new(builder).tap do |parser|
        # On JRuby and Rubinius, there's a risk that we hang in tokenize() if we
        # don't set the all errors as fatal flag. The problem is caused by a bug
        # in Racc that is discussed in issue #93 of the whitequark/parser
        # project on GitHub.
        parser.diagnostics.all_errors_are_fatal = (RUBY_ENGINE != 'ruby')
        parser.diagnostics.ignore_warnings = false
        parser.diagnostics.consumer = lambda do |diagnostic|
          @diagnostics << diagnostic
        end
      end
    end
  end
end
