require "capistrano/proc_helpers"

module Capistrano
  class Configuration
    # Holds the variables assigned at Capistrano runtime via `set` and retrieved
    # with `fetch`. Does internal bookkeeping to help identify user mistakes
    # like spelling errors or unused variables that may lead to unexpected
    # behavior.
    class Variables
      CAPISTRANO_LOCATION = File.expand_path("../..", __FILE__).freeze
      IGNORED_LOCATIONS = [
        "#{CAPISTRANO_LOCATION}/configuration/variables.rb:",
        "#{CAPISTRANO_LOCATION}/configuration.rb:",
        "#{CAPISTRANO_LOCATION}/dsl/env.rb:",
        "/dsl.rb:",
        "/forwardable.rb:"
      ].freeze
      private_constant :CAPISTRANO_LOCATION, :IGNORED_LOCATIONS

      include Capistrano::ProcHelpers

      def initialize(values={})
        @trusted_keys = []
        @fetched_keys = []
        @locations = {}
        @values = values
        @trusted = true
      end

      def untrusted!
        @trusted = false
        yield
      ensure
        @trusted = true
      end

      def set(key, value=nil, &block)
        @trusted_keys << key if trusted? && !@trusted_keys.include?(key)
        remember_location(key)
        values[key] = block || value
        trace_set(key)
        values[key]
      end

      def fetch(key, default=nil, &block)
        fetched_keys << key unless fetched_keys.include?(key)
        peek(key, default, &block)
      end

      # Internal use only.
      def peek(key, default=nil, &block)
        value = fetch_for(key, default, &block)
        while callable_without_parameters?(value)
          value = (values[key] = value.call)
        end
        value
      end

      def fetch_for(key, default, &block)
        block ? values.fetch(key, &block) : values.fetch(key, default)
      end

      def delete(key)
        values.delete(key)
      end

      def trusted_keys
        @trusted_keys.dup
      end

      def untrusted_keys
        keys - @trusted_keys
      end

      def keys
        values.keys
      end

      # Keys that have been set, but which have never been fetched.
      def unused_keys
        keys - fetched_keys
      end

      # Returns an array of source file location(s) where the given key was
      # assigned (i.e. where `set` was called). If the key was never assigned,
      # returns `nil`.
      def source_locations(key)
        locations[key]
      end

      private

      attr_reader :locations, :values, :fetched_keys

      def trusted?
        @trusted
      end

      def remember_location(key)
        location = caller.find do |line|
          IGNORED_LOCATIONS.none? { |i| line.include?(i) }
        end
        (locations[key] ||= []) << location
      end

      def trace_set(key)
        return unless fetch(:print_config_variables, false)
        puts "Config variable set: #{key.inspect} => #{values[key].inspect}"
      end
    end
  end
end
