#
# Author:: Noah Kantrowitz <noah@opscode.com>
# Cookbook Name:: application
# Library:: default
#
# Copyright:: 2011-2012, Opscode, Inc <legal@opscode.com>
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

require "chef/mixin/from_file"

class Chef
  class Resource
    # Monkey-Patch the blacklists to prevent infinite recursion in #to_json and similar
    # (this is global state,thus the set-union operator to change it idempotently + to not leak)
    [ :@application, :@application_provider ].each do |ivar|
      FORBIDDEN_IVARS << ivar unless FORBIDDEN_IVARS.include?(ivar)
      HIDDEN_IVARS << ivar unless HIDDEN_IVARS.include?(ivar)
    end
  end
end

class ApplicationCookbook

  module OptionsCollector
    def options
      @options ||= {}
    end

    def method_missing(method_sym, value=nil, &block)
      super
    rescue NameError
      value ||= block
      method_sym = method_sym.to_s.chomp('=').to_sym
      options[method_sym] = value if value
      options[method_sym] ||= nil
    end
  end

  module ResourceBase
    def self.included(klass)
      klass.actions :before_compile, :before_deploy, :before_migrate, :before_symlink, :before_restart, :after_restart
      klass.attribute :id, :kind_of => String, :name_attribute => true
      klass.attribute :environment, :kind_of => Hash, :default => {}
      klass.attribute :purge_before_symlink, :kind_of => Array, :default => []
      klass.attribute :create_dirs_before_symlink, :kind_of => Array, :default => []
      klass.attribute :symlinks, :kind_of => Hash, :default => {}
      klass.attribute :symlink_before_migrate, :kind_of => Hash, :default => {}
      klass.attribute :migration_command, :kind_of => [String, NilClass], :default => nil
      klass.attribute :application
      klass.attribute :application_provider
      klass.attribute :type
    end

    def restart_command(arg=nil, &block)
      arg ||= block
      raise "Invalid restart command" unless !arg || arg.is_a?(String) || arg.is_a?(Proc)
      @restart_command = arg if arg
      @restart_command
    end

    def method_missing(name, *args)
      if application.respond_to? name
        application.send(name, *args)
      else
        super
      end
    end

    class OptionsBlock
      include ApplicationCookbook::OptionsCollector
    end

    def options_block(options=nil, &block)
      options ||= {}
      if block
        collector = OptionsBlock.new
        collector.instance_eval(&block)
        options.update(collector.options)
      end
      options
    end

    def find_matching_role(role, single=true, &block)
      return nil if !role
      nodes = []
      if node['roles'].include? role
        nodes << node
      end
      if !single || nodes.empty?
        search(:node, "role:#{role} AND chef_environment:#{node.chef_environment}") do |n|
          nodes << n
        end
      end
      if block
        nodes.each do |n|
          yield n
        end
      else
        if single
          nodes.first
        else
          nodes
        end
      end
    end

    def find_database_server(role)
      dbm = find_matching_role(role)
      Chef::Log.warn("No node with role #{role}") if role && !dbm

      if respond_to?(:database) && database.has_key?('host')
        database['host']
      elsif dbm && dbm.attribute?('cloud')
        dbm['cloud']['local_ipv4']
      elsif dbm
        dbm['ipaddress']
      end
    end
  end

  module ProviderBase

    def self.included(klass)
      klass.send(:include, Chef::Mixin::FromFile)
    end

    def deploy_provider
      @deploy_provider ||= begin
        provider = @deploy_resource.provider_for_action(:nothing)
        provider.load_current_resource
        provider
      end
    end

    def release_path
      deploy_provider.release_path
    end

    def shared_path
      @deploy_resource.shared_path
    end

    def callback(what, callback_code=nil)
      Chef::Log.debug("Got callback #{what}: #{callback_code.inspect}")
      @collection = Chef::ResourceCollection.new
      case callback_code
      when Proc
        Chef::Log.info "#{@new_resource} running callback #{what}"
        safe_recipe_eval(&callback_code)
      when String
        callback_file = "#{release_path}/#{callback_code}"
        unless ::File.exist?(callback_file)
          raise RuntimeError, "Can't find your callback file #{callback_file}"
        end
        run_callback_from_file(callback_file)
      when nil
        nil
      else
        raise RuntimeError, "You gave me a callback I don't know what to do with: #{callback_code.inspect}"
      end
    end

    def run_callback_from_file(callback_file)
      if ::File.exist?(callback_file)
        Dir.chdir(release_path) do
          Chef::Log.info "#{@new_resource} running deploy hook #{callback_file}"
          safe_recipe_eval { from_file(callback_file) }
        end
      end
    end

    def safe_recipe_eval(&callback_code)
      recipe_eval(&callback_code)
      converge if respond_to?(:converge)
    end
  end
end
