module Garner
  module Strategies
    module Context
      module Key
        class Caller < Base
          def self.field
            :caller
          end

          # Determine the most likely root path for the current Garner client
          # application. If in a Rails application, Rails.root is used.
          # Otherwise, the nearest ancestor directory containing a Gemfile is
          # used.
          #
          # @see Garner::Config#caller_root=
          # @see Garner::Config#default_caller_root
          # @return [String] The default root path.
          def self.default_root
            if defined?(::Rails) && ::Rails.respond_to?(:root)
              ::Rails.root.realpath.to_s
            else
              # Try to use the nearest ancestor directory containing a Gemfile.
              requiring_caller = send(:caller).find do |line|
                !line.include?(File.join('lib', 'garner'))
              end
              return nil unless requiring_caller

              requiring_file = requiring_caller.split(':')[0]
              gemfile_root(File.dirname(requiring_file))
            end
          end

          # Injects the caller's location into the key hash.
          #
          # @param identity [Garner::Cache::Identity] The cache identity.
          # @param ruby_context [Object] An optional Ruby context.
          # @return [Garner::Cache::Identity] The modified identity.
          def self.apply(identity, ruby_context = nil)
            value = nil

            if ruby_context.send(:caller)
              ruby_context.send(:caller).compact.each do |line|
                parts = line.match(/(?<filename>[^:]+)\:(?<lineno>[^:]+)/)
                file = (Pathname.new(parts[:filename]).realpath.to_s rescue nil)
                next if file.nil? || file == ''
                next if file.include?(File.join('lib', 'garner'))

                if (root = Garner.config.caller_root)
                  root += File::SEPARATOR unless root[-1] == File::SEPARATOR
                  next unless file =~ /^#{root}/
                  value = "#{file.gsub(root || '', '')}:#{parts[:lineno]}"
                else
                  value = "#{file}:#{parts[:lineno]}"
                end

                break
              end
            end

            value ? identity.key(field => value) : super
          end

          def self.gemfile_root(path)
            path = Pathname.new(path).realpath.to_s
            newpath = Pathname.new(File.join(path, '..')).realpath.to_s
            if newpath == path
              # We've reached the filesystem root; return
              return nil
            elsif File.exist?(File.join(newpath, 'Gemfile'))
              # We've struck Gemfile gold; return current path
              return newpath
            else
              return gemfile_root(newpath)
            end
          end
        end
      end
    end
  end
end
