require 'set'
require 'brakeman/util'
require 'brakeman/version'
require 'brakeman/report/renderer'
require 'brakeman/processors/output_processor'

# Base class for report formats
class Brakeman::Report::Base
  include Brakeman::Util

  attr_reader :tracker, :checks

  TEXT_CONFIDENCE = %w(High Medium Weak)

  def initialize app_tree, tracker
    @app_tree = app_tree
    @tracker = tracker
    @checks = tracker.checks
    @ignore_filter = tracker.ignored_filter
    @highlight_user_input = tracker.options[:highlight_user_input]
    @warnings_summary = nil
  end

  #Generate table of how many warnings of each warning type were reported
  def generate_warning_overview
    types = warnings_summary.keys
    types.delete :high_confidence
    values = types.sort.collect{|warning_type| [warning_type, warnings_summary[warning_type]] }
    locals = {:types => types, :warnings_summary => warnings_summary}

    render_array('warning_overview', ['Warning Type', 'Total'], values, locals)
  end

  #Generate table of controllers and routes found for those controllers
  def generate_controllers
    controller_rows = []

    tracker.controllers.keys.map{|k| k.to_s}.sort.each do |name|
      name = name.to_sym
      c = tracker.controllers[name]

      if tracker.routes.include? :allow_all_actions or (tracker.routes[name] and tracker.routes[name].include? :allow_all_actions)
        routes = c.methods_public.keys.map{|e| e.to_s}.sort.join(", ")
      elsif tracker.routes[name].nil?
        #No routes defined for this controller.
        #This can happen when it is only a parent class
        #for other controllers, for example.
        routes = "[None]"

      else
        routes = (Set.new(c.methods_public.keys) & tracker.routes[name.to_sym]).
          to_a.
          map {|e| e.to_s}.
          sort.
          join(", ")
      end

      if routes == ""
        routes = "[None]"
      end

      controller_rows << { "Name" => name.to_s,
        "Parent" => c.parent.to_s,
        "Includes" => c.includes.join(", "),
        "Routes" => routes
      }
    end

    cols = %w(Name Parent Includes Routes)

    locals = {:controller_rows => controller_rows}
    values = controller_rows.collect{|row| row.values_at(*cols) }
    render_array('controller_overview', cols, values, locals)
  end

  #Generate table of errors or return nil if no errors
  def generate_errors
    values = tracker.errors.collect{|error| [error[:error], error[:backtrace][0]]}
    render_array('error_overview', %w(Error Location), values, {:tracker => tracker})
  end

  def generate_warnings
    render_warnings generic_warnings,
                    :warning,
                    'security_warnings',
                    ["Confidence", "Class", "Method", "Warning Type", "Message"],
                    'Class'
  end

  #Generate table of template warnings or return nil if no warnings
  def generate_template_warnings
    render_warnings template_warnings,
                    :template,
                    'view_warnings',
                    ['Confidence', 'Template', 'Warning Type', 'Message'],
                    'Template'

  end

  #Generate table of model warnings or return nil if no warnings
  def generate_model_warnings
    render_warnings model_warnings,
                    :model,
                    'model_warnings',
                    ['Confidence', 'Model', 'Warning Type', 'Message'],
                    'Model'
  end

  #Generate table of controller warnings or nil if no warnings
  def generate_controller_warnings
    render_warnings controller_warnings,
                    :controller,
                    'controller_warnings',
                    ['Confidence', 'Controller', 'Warning Type', 'Message'],
                    'Controller'
  end

  def generate_ignored_warnings
    render_warnings ignored_warnings,
                    :ignored,
                    'ignored_warnings',
                    ['Confidence', 'Warning Type', 'File', 'Message'],
                    'Warning Type'
  end

  def render_warnings warnings, type, template, cols, sort_col
    unless warnings.empty?
      rows = sort(convert_to_rows(warnings, type), sort_col)

      values = rows.collect { |row| row.values_at(*cols) }

      locals = { :warnings => rows }

      render_array(template, cols, values, locals)
    else
      nil
    end
  end

  def convert_to_rows warnings, type = :warning
    warnings.map do |warning|
      w = warning.to_row type

      case type
        when :warning
          convert_warning w, warning
        when :template
          convert_template_warning w, warning
        when :model
          convert_model_warning w, warning
        when :controller
          convert_controller_warning w, warning
        when :ignored
          convert_ignored_warning w, warning
        end
    end
  end

  def convert_warning warning, original
    warning["Confidence"] = TEXT_CONFIDENCE[warning["Confidence"]]
    warning["Message"] = text_message original, warning["Message"]
    warning
  end

  def convert_template_warning warning, original
    convert_warning warning, original
  end

  def convert_model_warning warning, original
    convert_warning warning, original
  end

  def convert_controller_warning warning, original
    convert_warning warning, original
  end

  def convert_ignored_warning warning, original
    convert_warning warning, original
  end

  def sort rows, sort_col
    stabilizer = 0
    rows.sort_by do |row|
      stabilizer += 1

      row.values_at("Confidence", "Warning Type", sort_col) << stabilizer
    end
  end

  #Return summary of warnings in hash and store in @warnings_summary
  def warnings_summary
    return @warnings_summary if @warnings_summary

    summary = Hash.new(0)
    high_confidence_warnings = 0

    [all_warnings].each do |warnings|
      warnings.each do |warning|
        summary[warning.warning_type.to_s] += 1
        high_confidence_warnings += 1 if warning.confidence == 0
      end
    end

    summary[:high_confidence] = high_confidence_warnings
    @warnings_summary = summary
  end

  def all_warnings
    if @ignore_filter
      @all_warnings ||= @ignore_filter.shown_warnings
    else
      @all_warnings ||= tracker.checks.all_warnings
    end
  end

  def filter_warnings warnings
    if @ignore_filter
      warnings.reject do |w|
        @ignore_filter.ignored? w
      end
    else
      warnings
    end
  end

  def generic_warnings
    filter_warnings tracker.checks.warnings
  end

  def template_warnings
    filter_warnings tracker.checks.template_warnings
  end

  def model_warnings
    filter_warnings tracker.checks.model_warnings
  end

  def controller_warnings
    filter_warnings tracker.checks.controller_warnings
  end

  def ignored_warnings
    if @ignore_filter
      @ignore_filter.ignored_warnings
    else
      []
    end
  end

  def number_of_templates tracker
    Set.new(tracker.templates.map {|k,v| v.name.to_s[/[^.]+/]}).length
  end

  def warning_file warning, absolute = @tracker.options[:absolute_paths]
    return nil if warning.file.nil?

    if absolute
      warning.file
    else
      relative_path warning.file
    end
  end

  def rails_version
    case
    when tracker.config.rails_version
      tracker.config.rails_version
    when tracker.options[:rails4]
      "4.x"
    when tracker.options[:rails3]
      "3.x"
    else
      "Unknown"
    end
  end

  #Escape warning message and highlight user input in text output
  def text_message warning, message
    if @highlight_user_input and warning.user_input
      user_input = warning.format_user_input
      message.gsub(user_input, "+#{user_input}+")
    else
      message
    end
  end
end
