module Laser
  class Scanner
    attr_accessor :settings
    attr_accessor :indent_stack

    DEFAULT_SETTINGS = {fix: false, output: STDOUT, indent_size: 2,
                        __using__: Laser::Warning.warning_set,
                        __fix__: Laser::Warning.warning_set}

    # Initializes the scanner with the given settings
    #
    # @param [Hash] settings the settings to use to customize this scanner's
    #   scanning behavior
    def initialize(settings = {})
      @settings = DEFAULT_SETTINGS.merge(settings)
      @settings[:__scanner__] = self
      self.indent_stack = []
    end

    # Returns the list of warnings to use for scanning.
    def using
      @settings[:__using__]
    end

    # Should we use this warning?
    def using?(warning)
      @settings[:__using__].include? warning
    end

    # Returns the list of warnings to use for scanning.
    def fix
      @settings[:__fix__]
    end

    # Should we use this warning?
    def fixing?(warning)
      @settings[:__fix__].include? warning.class
    end

    # Scans the text for warnings.
    #
    # @param [String] text the input ruby file to scan
    # @return [Array[Laser::Warnings]] the warnings generated by the code.
    #   If the code is clean, an empty array is returned.
    def scan(text, filename='(none)')
      warnings = scan_for_file_warnings(text, filename)
      text = filter_fixable(warnings).inject(text) do |text, warning|
        warning.fix(text)
      end
      with_fixing_piped_to_output do
        text.split(/\n/).each_with_index do |line, number|
          warnings.concat process_line(line, number + 1, filename)
        end
      end
      warnings += unused_method_warnings
      warnings
    end

    def with_fixing_piped_to_output
      self.settings[:output_lines] = []
      yield
      if @settings[:fix]
        self.settings[:output_file].write self.settings[:output_lines].join("\n")
      end
    end

    # Finds all matching warnings, and if the user wishes, fix a subset of them.
    def process_line(line, line_number, filename)
      warnings = all_warnings_for_line(line, line_number, filename)
      fix_input(warnings, line, line_number, filename) if @settings[:fix]
      warnings
    end

    # Tries to fix the given line with a set of matching warnings for that line.
    # May recurse if there are multiple warnings on the same line.
    def fix_input(warnings, line, line_number, filename)
      fixable_warnings = filter_fixable warnings
      if fixable_warnings.size == 1
        self.settings[:output_lines] << fixable_warnings.first.fix rescue line
      elsif fixable_warnings.size > 1
        new_text = fixable_warnings.first.fix rescue line
        process_line(new_text, line_number, filename)
      else
        self.settings[:output_lines] << line
      end
    end

    # Returns all warnings that match the line
    def all_warnings_for_line(line, line_number, filename)
      new_warnings = check_for_indent_warnings!(line, filename)
      new_warnings.concat scan_for_line_warnings(line, filename)
      new_warnings.each {|warning| warning.line_number = line_number}
    end

    # Returns only the warnings that we should fix
    def filter_fixable(warnings)
      warnings.select {|warning| warning.fixable? && fixing?(warning) }
    end

    # Checks for new warnings based on indentation.
    def check_for_indent_warnings!(line, filename)
      return [] if line == ""
      indent_size = get_indent_size line
      if indent_size > current_indent
        self.indent_stack.push indent_size
      elsif indent_size < current_indent
        previous = self.indent_stack.pop
        if indent_size != current_indent &&
           using.include?(MisalignedUnindentationWarning)
          warnings_to_check = [MisalignedUnindentationWarning.new(filename, line, current_indent)]
          return filtered_warnings_from_line(line, warnings_to_check)
        end
      end
      []
    end

    def unused_method_warnings
      Analysis::MethodAnalysis.unused_methods.map do |method| 
        warning = UncalledMethodWarning.new(method.proc.ast_node.file_name, '', method: method)
        warning.line_number = method.proc.line_number
        warning
      end
    end

    def method_expectation_warnings
      
    end

    # Gets the current indent size
    def current_indent
      self.indent_stack.last || 0
    end

    # Gets the indent size of a given line
    def get_indent_size(line)
      line.match(/^\s*/)[0].size
    end

    # Goes through all file warning subclasses and see what warnings the file
    # generates as a whole.
    def scan_for_file_warnings(file, filename)
      scan_for_warnings(using & FileWarning.all_warnings, file, filename)
    end

    # Goes through all line warning subclasses and checks if we got some new
    # warnings for a given line
    def scan_for_line_warnings(line, filename)
      warnings = scan_for_warnings(using & LineWarning.all_warnings, line, filename)
      filtered_warnings_from_line(line, warnings)
    end

    private

    # Filters the list of warnings by checking the line for warnings to
    # ignore. The line should contain "laser: ignore ClassToIgnore" in a comment,
    # though you can omit the space between "laser:" and "ignore".
    def filtered_warnings_from_line(line, warnings)
      match = line.match(/#.*laser:\s*ignore\s+(.*)$/)
      return warnings unless match && ignore_label = match[1]
      class_names = ignore_label.split
      result = warnings.reject do |warning|
        class_names.include?(warning.class.name.gsub(/.*::(.*)/, '\1')) ||
          class_names.include?(warning.class.short_name)
      end
      result
    end

    def scan_for_warnings(warnings, content, filename)
      warnings.map! { |warning| warning.new(filename, content, @settings)}
      warnings.map { |warning| warning.generated_warnings(warning.body)}.flatten
    end
  end
end
