#
# Author:: Seth Chisamore (<schisamo@chef.io>)
# Cookbook Name:: windows
# Library:: helper
#
# Copyright:: 2011, Chef Software, Inc.
#
# 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 'uri'
require 'Win32API' if Chef::Platform.windows?
require 'chef/exceptions'

module Windows
  module Helper

    AUTO_RUN_KEY = 'HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Run'.freeze unless defined?(AUTO_RUN_KEY)
    ENV_KEY = 'HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Environment'.freeze unless defined?(ENV_KEY)
    ExpandEnvironmentStrings = Win32API.new('kernel32', 'ExpandEnvironmentStrings', ['P', 'P', 'L'], 'L') if Chef::Platform.windows?

    # returns windows friendly version of the provided path,
    # ensures backslashes are used everywhere
    def win_friendly_path(path)
      path.gsub(::File::SEPARATOR, ::File::ALT_SEPARATOR || '\\') if path
    end

    # account for Window's wacky File System Redirector
    # http://msdn.microsoft.com/en-us/library/aa384187(v=vs.85).aspx
    # especially important for 32-bit processes (like Ruby) on a
    # 64-bit instance of Windows.
    def locate_sysnative_cmd(cmd)
      if ::File.exists?("#{ENV['WINDIR']}\\sysnative\\#{cmd}")
        "#{ENV['WINDIR']}\\sysnative\\#{cmd}"
      elsif ::File.exists?("#{ENV['WINDIR']}\\system32\\#{cmd}")
        "#{ENV['WINDIR']}\\system32\\#{cmd}"
      else
        cmd
      end
    end

    # Create a feature provider dependent value object.
    # mainly created becasue Windows Feature names are
    # different based on whether dism.exe or servicemanagercmd.exe
    # is used for installation
    def value_for_feature_provider(provider_hash)
      p = Chef::Platform.find_provider_for_node(node, :windows_feature)
      key = p.to_s.downcase.split('::').last
      provider_hash[key] || provider_hash[key.to_sym]
    end

    # singleton instance of the Windows Version checker
    def win_version
      @win_version ||= Windows::Version.new
    end

    # if a file is local it returns a windows friendly path version
    # if a file is remote it caches it locally
    def cached_file(source, checksum=nil, windows_path=true)
      @installer_file_path ||= begin

        if source =~ ::URI::ABS_URI && %w[ftp http https].include?(URI.parse(source).scheme)
          uri = ::URI.parse(source)
          cache_file_path = "#{Chef::Config[:file_cache_path]}/#{::File.basename(::URI.unescape(uri.path))}"
          Chef::Log.debug("Caching a copy of file #{source} at #{cache_file_path}")
          r = Chef::Resource::RemoteFile.new(cache_file_path, run_context)
          r.source(source)
          r.backup(false)
          r.checksum(checksum) if checksum
          r.run_action(:create)
        else
          cache_file_path = source
        end

        windows_path ? win_friendly_path(cache_file_path) : cache_file_path
      end
    end

    # Expands the environment variables
    def expand_env_vars(path)
      # We pick 32k because that is the largest it could be:
      # http://msdn.microsoft.com/en-us/library/windows/desktop/ms724265%28v=vs.85%29.aspx
      buf = 0.chr * 32 * 1024 # 32k
      if ExpandEnvironmentStrings.call(path.dup, buf, buf.length) == 0
        raise Chef::Exceptions::Win32APIError, "Failed calling ExpandEnvironmentStrings (received 0)"
      end
      buf.strip
    end

    def is_package_installed?(package_name)
      installed_packages.include?(package_name)
    end

    def installed_packages
      @installed_packages || begin
        installed_packages = {}
        # Computer\HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Uninstall
        installed_packages.merge!(extract_installed_packages_from_key(::Win32::Registry::HKEY_LOCAL_MACHINE)) #rescue nil
        # 64-bit registry view
        # Computer\HKEY_LOCAL_MACHINE\Software\Wow6464Node\Microsoft\Windows\CurrentVersion\Uninstall
        installed_packages.merge!(extract_installed_packages_from_key(::Win32::Registry::HKEY_LOCAL_MACHINE, (::Win32::Registry::Constants::KEY_READ | 0x0100))) #rescue nil
        # 32-bit registry view
        # Computer\HKEY_LOCAL_MACHINE\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall
        installed_packages.merge!(extract_installed_packages_from_key(::Win32::Registry::HKEY_LOCAL_MACHINE, (::Win32::Registry::Constants::KEY_READ | 0x0200))) #rescue nil
        # Computer\HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Uninstall
        installed_packages.merge!(extract_installed_packages_from_key(::Win32::Registry::HKEY_CURRENT_USER)) #rescue nil
        installed_packages
      end
    end

    private
    def extract_installed_packages_from_key(hkey = ::Win32::Registry::HKEY_LOCAL_MACHINE, desired = ::Win32::Registry::Constants::KEY_READ)
      uninstall_subkey = 'Software\Microsoft\Windows\CurrentVersion\Uninstall'
      packages = {}
      begin
        ::Win32::Registry.open(hkey, uninstall_subkey, desired) do |reg|
          reg.each_key do |key, wtime|
            begin
              k = reg.open(key, desired)
              display_name = k["DisplayName"] rescue nil
              version = k["DisplayVersion"] rescue "NO VERSION"
              uninstall_string = k["UninstallString"] rescue nil
              if display_name
                packages[display_name] = {:name => display_name,
                                          :version => version,
                                          :uninstall_string => uninstall_string}
              end
            rescue ::Win32::Registry::Error
            end
          end
        end
      rescue ::Win32::Registry::Error
      end
      packages
    end
  end
end

Chef::Recipe.send(:include, Windows::Helper)
