require 'digest/sha1'

#Processes a call to render() in a controller or template
module Brakeman::RenderHelper

  #Process s(:render, TYPE, OPTION?, OPTIONS)
  def process_render exp
    process_default exp
    @rendered = true
    case exp.render_type
    when :action, :template, :inline
      action = exp[2]
      args = exp[3]

      if string? action or symbol? action
        process_action action.value, args, exp.line
      else
        process_model_action action, args
      end
    when :default
      begin
        # exp[2] is either the action name (from controller) or :default when no explicit arg
        name_arg = (exp[2].nil? || exp[2] == :default) ? nil : exp[2]
        process_template template_name(name_arg), exp[3], nil, exp.line
      rescue ArgumentError
        Brakeman.debug "Problem processing render: #{exp}"
      end
    when :partial, :layout
      process_partial exp[2], exp[3], exp.line
    when :nothing
    end
    exp
  end

  #Processes layout
  def process_layout name = nil
    if name.nil? and defined? layout_name
      name = layout_name
    end

    return unless name

    process_template name, nil, nil, nil
  end

  #Determines file name for partial and then processes it
  def process_partial name, args, line
    if !(string? name or symbol? name) or name.value == ""
      return
    end

    names = name.value.to_s.split("/")
    names[-1] = "_" + names[-1]
    process_template template_name(names.join("/")), args, nil, line
  end

  #Processes a given action
  def process_action name, args, line
    if name.is_a? String or name.is_a? Symbol
      process_template template_name(name), args, nil, line
    else
      Brakeman.debug "Not processing render #{name.inspect}"
    end
  end

  SINGLE_RECORD = [:first, :find, :last, :new]
  COLLECTION = [:all, :where]

  def process_model_action action, args
    return unless call? action

    method = action.method

    klass = get_class_target(action) || Brakeman::Tracker::UNKNOWN_MODEL
    name = Sexp.new(:lit, klass.downcase)

    if SINGLE_RECORD.include? method
      # Set a local variable with name based on class of model
      # and value of the value passed to render
      local_key = Sexp.new(:lit, :locals)
      locals = hash_access(args, local_key) || Sexp.new(:hash)
      hash_insert(locals, name, action)
      hash_insert(args, local_key, locals)

      process_partial name, args, action.line
    elsif COLLECTION.include? method
      collection_key = Sexp.new(:lit, :collection)
      hash_insert(args, collection_key, action)

      process_partial name, args, action.line
    end
  end

  #Processes a template, adding any instance variables
  #to its environment.
  def process_template name, args, called_from = nil, *_

    Brakeman.debug "Rendering #{name} (#{called_from})"
    #Get scanned source for this template
    name = name.to_s.gsub(/^\//, "")
    template = @tracker.templates[name.to_sym]
    unless template
      Brakeman.debug "No such template: #{name}"
      return
    end

    if called_from
      # Track actual template that was rendered
      called_from.last_template = template
    end

    template_env = only_ivars(:include_request_vars)

    #Hash the environment and the source of the template to avoid
    #pointlessly processing templates, which can become prohibitively
    #expensive in terms of time and memory.
    digest = Digest::SHA1.new.update(template_env.instance_variable_get(:@env).to_a.sort.to_s << name).to_s.to_sym

    if @tracker.template_cache.include? digest
      #Already processed this template with identical environment
      return
    else
      @tracker.template_cache << digest

      options = get_options args

      #Process layout
      if string? options[:layout]
        process_template "layouts/#{options[:layout][1]}", nil, nil, nil
      elsif node_type? options[:layout], :false
        #nothing
      elsif not template.name.to_s.match(/[^\/_][^\/]+$/)
        #Don't do this for partials

        process_layout
      end

      if hash? options[:locals]
        hash_iterate options[:locals] do |key, value|
          if symbol? key
            template_env[Sexp.new(:call, nil, key.value)] = value
          end
        end
      end

      if options[:collection]

        #The collection name is the name of the partial without the leading
        #underscore.
        variable = template.name.to_s.match(/[^\/_][^\/]+$/)[0].to_sym

        #Unless the :as => :variable_name option is used
        if options[:as]
          if string? options[:as] or symbol? options[:as]
            variable = options[:as].value.to_sym
          end
        end

        collection = get_class_target(options[:collection]) || Brakeman::Tracker::UNKNOWN_MODEL

        template_env[Sexp.new(:call, nil, variable)] = Sexp.new(:call, Sexp.new(:const, collection), :new)
      end

      #Set original_line for values so it is clear
      #that values came from another file
      template_env.all.each do |_var, value|
        unless value.original_line
          #TODO: This has been broken for a while now and no one noticed
          #so maybe we can skip it
          value.original_line = value.line
        end
      end

      #Run source through AliasProcessor with instance variables from the
      #current environment.
      #TODO: Add in :locals => { ... } to environment
      src = Brakeman::TemplateAliasProcessor.new(@tracker, template, called_from).process_safely(template.src, template_env)

      digest = Digest::SHA1.new.update(name + src.to_s).to_s.to_sym

      if @tracker.template_cache.include? digest
        return
      else
        @tracker.template_cache << digest
      end

      #Run alias-processed src through the template processor to pull out
      #information and outputs.
      #This information will be stored in tracker.templates, but with a name
      #specifying this particular route. The original source should remain
      #pristine (so it can be processed within other environments).
      @tracker.processor.process_template name, src, template.type, called_from, template.file
    end
  end

  #Override to process name, such as adding the controller name.
  def template_name name
    raise "RenderHelper#template_name should be overridden."
  end

  #Turn options Sexp into hash
  def get_options args
    options = {}
    return options unless hash? args

    hash_iterate args do |key, value|
      if symbol? key
        options[key.value] = value
      end
    end

    options
  end

  def get_class_target sexp
    if call? sexp
      get_class_target sexp.target
    else
      klass = class_name sexp
      if klass.is_a? Symbol
        klass
      else
        nil
      end
    end
  end
end
