require 'pdk/util/windows'

module PDK
  module Util
    module Windows
      module Process
        require 'ffi'
        extend PDK::Util::Windows::String
        extend FFI::Library

        def environment_hash
          env_ptr = GetEnvironmentStringsW()

          contains_unicode_replacement = lambda do |string|
            return false unless string.include?("\uFFFD")

            PDK.logger.warning "Discarding environment variable #{string} which contains invalid bytes"
            true
          end

          # pass :invalid => :replace to the Ruby String#encode to use replacement
          # characters
          pairs = env_ptr.read_arbitrary_wide_string_up_to(65_534, :double_null, invalid: :replace)
                         .split(?\x00)
                         .reject { |env_str| env_str.nil? || env_str.empty? || env_str[0] == '=' }
                         .reject { |env_str| contains_unicode_replacement.call(env_str) }
                         .map { |env_pair| env_pair.split('=', 2) }
          pairs.to_h
        ensure
          PDK.logger.debug 'FreeEnvironmentStringsW memory leak' if env_ptr && !env_ptr.null? && (FreeEnvironmentStringsW(env_ptr) == PDK::Util::Windows::WIN32_FALSE)
        end
        module_function :environment_hash

        def set_environment_variable(name, val)
          raise ArgumentError, 'Environment variable name must not be nil or empty' if name.nil? || name.empty?

          FFI::MemoryPointer.from_string_to_wide_string(name) do |name_ptr|
            if val.nil?
              raise format('Failed to remove environment variable: %{name}', name: name) if SetEnvironmentVariableW(name_ptr, FFI::MemoryPointer::NULL) == PDK::Util::Windows::WIN32_FALSE
            else
              FFI::MemoryPointer.from_string_to_wide_string(val) do |val_ptr|
                raise format('Failed to set environment variaible: %{name}', name: name) if SetEnvironmentVariableW(name_ptr, val_ptr) == PDK::Util::Windows::WIN32_FALSE
              end
            end
          end
        end
        module_function :set_environment_variable

        ffi_convention :stdcall

        # https://msdn.microsoft.com/en-us/library/windows/desktop/ms683187(v=vs.85).aspx
        # LPTCH GetEnvironmentStrings(void);
        ffi_lib :kernel32
        attach_function_private :GetEnvironmentStringsW, [], :pointer

        # https://msdn.microsoft.com/en-us/library/windows/desktop/ms683151(v=vs.85).aspx
        # BOOL FreeEnvironmentStrings(
        #   _In_ LPTCH lpszEnvironmentBlock
        # );
        ffi_lib :kernel32
        attach_function_private :FreeEnvironmentStringsW, [:pointer], :win32_bool

        # https://msdn.microsoft.com/en-us/library/windows/desktop/ms686206(v=vs.85).aspx
        # BOOL WINAPI SetEnvironmentVariableW(
        #   _In_     LPCTSTR lpName,
        #   _In_opt_ LPCTSTR lpValue
        # );
        ffi_lib :kernel32
        attach_function_private :SetEnvironmentVariableW, [:lpcwstr, :lpcwstr],
                                :win32_bool
      end
    end
  end
end
