require 'puppet/util/windows'

module Puppet::Util::Windows::File
  require 'ffi'
  require 'windows/api'
  require 'windows/wide_string'

  ReplaceFileWithoutBackupW = Windows::API.new('ReplaceFileW', 'PPVLVV', 'B')
  def replace_file(target, source)
    result = ReplaceFileWithoutBackupW.call(WideString.new(target.to_s),
                                   WideString.new(source.to_s),
                                   0, 0x1, 0, 0)
    return true unless result == 0
    raise Puppet::Util::Windows::Error.new("ReplaceFile(#{target}, #{source})")
  end
  module_function :replace_file

  MoveFileEx = Windows::API.new('MoveFileExW', 'PPL', 'B')
  def move_file_ex(source, target, flags = 0)
    result = MoveFileEx.call(WideString.new(source.to_s),
                             WideString.new(target.to_s),
                             flags)
    return true unless result == 0
    raise Puppet::Util::Windows::Error.
      new("MoveFileEx(#{source}, #{target}, #{flags.to_s(8)})")
  end
  module_function :move_file_ex

  module API
    extend FFI::Library
    ffi_lib 'kernel32'
    ffi_convention :stdcall

    # BOOLEAN WINAPI CreateSymbolicLink(
    #   _In_  LPTSTR lpSymlinkFileName, - symbolic link to be created
    #   _In_  LPTSTR lpTargetFileName, - name of target for symbolic link
    #   _In_  DWORD dwFlags - 0x0 target is a file, 0x1 target is a directory
    # );
    # rescue on Windows < 6.0 so that code doesn't explode
    begin
      attach_function :create_symbolic_link, :CreateSymbolicLinkW,
        [:buffer_in, :buffer_in, :uint], :bool
    rescue LoadError
    end

    # DWORD WINAPI GetFileAttributes(
    #   _In_  LPCTSTR lpFileName
    # );
    attach_function :get_file_attributes, :GetFileAttributesW,
      [:buffer_in], :uint

    # HANDLE WINAPI CreateFile(
    #   _In_      LPCTSTR lpFileName,
    #   _In_      DWORD dwDesiredAccess,
    #   _In_      DWORD dwShareMode,
    #   _In_opt_  LPSECURITY_ATTRIBUTES lpSecurityAttributes,
    #   _In_      DWORD dwCreationDisposition,
    #   _In_      DWORD dwFlagsAndAttributes,
    #   _In_opt_  HANDLE hTemplateFile
    # );
    attach_function :create_file, :CreateFileW,
      [:buffer_in, :uint, :uint, :pointer, :uint, :uint, :uint], :uint

    # http://msdn.microsoft.com/en-us/library/windows/desktop/aa363216(v=vs.85).aspx
    # BOOL WINAPI DeviceIoControl(
    #   _In_         HANDLE hDevice,
    #   _In_         DWORD dwIoControlCode,
    #   _In_opt_     LPVOID lpInBuffer,
    #   _In_         DWORD nInBufferSize,
    #   _Out_opt_    LPVOID lpOutBuffer,
    #   _In_         DWORD nOutBufferSize,
    #   _Out_opt_    LPDWORD lpBytesReturned,
    #   _Inout_opt_  LPOVERLAPPED lpOverlapped
    # );
    attach_function :device_io_control, :DeviceIoControl,
      [:uint, :uint, :pointer, :uint, :pointer, :uint, :pointer, :pointer], :bool

    MAXIMUM_REPARSE_DATA_BUFFER_SIZE = 16384

    # REPARSE_DATA_BUFFER
    # http://msdn.microsoft.com/en-us/library/cc232006.aspx
    # http://msdn.microsoft.com/en-us/library/windows/hardware/ff552012(v=vs.85).aspx
    # struct is always MAXIMUM_REPARSE_DATA_BUFFER_SIZE bytes
    class ReparseDataBuffer < FFI::Struct
      layout :reparse_tag, :uint,
             :reparse_data_length, :ushort,
             :reserved, :ushort,
             :substitute_name_offset, :ushort,
             :substitute_name_length, :ushort,
             :print_name_offset, :ushort,
             :print_name_length, :ushort,
             :flags, :uint,
             # max less above fields dword / uint 4 bytes, ushort 2 bytes
             :path_buffer, [:uchar, MAXIMUM_REPARSE_DATA_BUFFER_SIZE - 20]
    end

    # BOOL WINAPI CloseHandle(
    #   _In_  HANDLE hObject
    # );
    attach_function :close_handle, :CloseHandle, [:uint], :bool
  end

  def symlink(target, symlink)
    flags = File.directory?(target) ? 0x1 : 0x0
    result = API.create_symbolic_link(WideString.new(symlink.to_s),
      WideString.new(target.to_s), flags)
    return true if result
    raise Puppet::Util::Windows::Error.new(
      "CreateSymbolicLink(#{symlink}, #{target}, #{flags.to_s(8)})")
  end
  module_function :symlink

  INVALID_FILE_ATTRIBUTES = 0xFFFFFFFF #define INVALID_FILE_ATTRIBUTES (DWORD (-1))
  def self.get_file_attributes(file_name)
    result = API.get_file_attributes(WideString.new(file_name.to_s))
    return result unless result == INVALID_FILE_ATTRIBUTES
    raise Puppet::Util::Windows::Error.new("GetFileAttributes(#{file_name})")
  end

  INVALID_HANDLE_VALUE = -1 #define INVALID_HANDLE_VALUE ((HANDLE)(LONG_PTR)-1)
  def self.create_file(file_name, desired_access, share_mode, security_attributes,
    creation_disposition, flags_and_attributes, template_file_handle)

    result = API.create_file(WideString.new(file_name.to_s),
      desired_access, share_mode, security_attributes, creation_disposition,
      flags_and_attributes, template_file_handle)

    return result unless result == INVALID_HANDLE_VALUE
    raise Puppet::Util::Windows::Error.new(
      "CreateFile(#{file_name}, #{desired_access.to_s(8)}, #{share_mode.to_s(8)}, " +
        "#{security_attributes}, #{creation_disposition.to_s(8)}, " +
        "#{flags_and_attributes.to_s(8)}, #{template_file_handle})")
  end

  def self.device_io_control(handle, io_control_code, in_buffer = nil, out_buffer = nil)
    if out_buffer.nil?
      raise Puppet::Util::Windows::Error.new("out_buffer is required")
    end

    result = API.device_io_control(
      handle,
      io_control_code,
      in_buffer, in_buffer.nil? ? 0 : in_buffer.size,
      out_buffer, out_buffer.size,
      FFI::MemoryPointer.new(:uint, 1),
      nil
    )

    return out_buffer if result
    raise Puppet::Util::Windows::Error.new(
      "DeviceIoControl(#{handle}, #{io_control_code}, #{in_buffer}, #{in_buffer.size}, " +
      "#{out_buffer}, #{out_buffer.size}")
  end

  FILE_ATTRIBUTE_REPARSE_POINT = 0x400
  def symlink?(file_name)
    begin
      attributes = get_file_attributes(file_name)
      (attributes & FILE_ATTRIBUTE_REPARSE_POINT) == FILE_ATTRIBUTE_REPARSE_POINT
    rescue
      # raised INVALID_FILE_ATTRIBUTES is equivalent to file not found
      false
    end
  end
  module_function :symlink?

  GENERIC_READ                  = 0x80000000
  FILE_SHARE_READ               = 1
  OPEN_EXISTING                 = 3
  FILE_FLAG_OPEN_REPARSE_POINT  = 0x00200000
  FILE_FLAG_BACKUP_SEMANTICS    = 0x02000000

  def self.open_symlink(link_name)
    begin
      yield handle = create_file(
      WideString.new(link_name.to_s),
      GENERIC_READ,
      FILE_SHARE_READ,
      nil, # security_attributes
      OPEN_EXISTING,
      FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS,
      0) # template_file
    ensure
      API.close_handle(handle) if handle
    end
  end

  def readlink(link_name)
    open_symlink(link_name) do |handle|
      resolve_symlink(handle)
    end
  end
  module_function :readlink

  def stat(file_name)
    file_name = file_name.to_s # accomodate PathName or String
    stat = File.stat(file_name)
    if symlink?(file_name)
      link_ftype = File.stat(readlink(file_name)).ftype
      # sigh, monkey patch instance method for instance, and close over link_ftype
      singleton_class = class << stat; self; end
      singleton_class.send(:define_method, :ftype) do
        link_ftype
      end
    end
    stat
  end
  module_function :stat

  def lstat(file_name)
    file_name = file_name.to_s # accomodate PathName or String
    # monkey'ing around!
    stat = File.lstat(file_name)
    if symlink?(file_name)
      def stat.ftype
        "link"
      end
    end
    stat
  end
  module_function :lstat

  private

  # http://msdn.microsoft.com/en-us/library/windows/desktop/aa364571(v=vs.85).aspx
  FSCTL_GET_REPARSE_POINT = 0x900a8

  def self.resolve_symlink(handle)
    # must be multiple of 1024, min 10240
    out_buffer = FFI::MemoryPointer.new(API::ReparseDataBuffer.size)
    device_io_control(handle, FSCTL_GET_REPARSE_POINT, nil, out_buffer)

    reparse_data = API::ReparseDataBuffer.new(out_buffer)
    offset = reparse_data[:print_name_offset]
    length = reparse_data[:print_name_length]

    result = reparse_data[:path_buffer].to_a[offset, length].pack('C*')
    result.force_encoding('UTF-16LE').encode(Encoding.default_external)
  end
end
