#-----------------------------------------------------------------------------
#
# Thomas Thomassen
# thomas[at]thomthom[dot]net
#
#-----------------------------------------------------------------------------

require 'TT_Lib2.rb'
require 'TT_Lib2/system.rb'

# @since 2.5.0
module TT::Win32

  if !file_loaded?( __FILE__ )

    if RUBY_VERSION.to_i < 2
      require File.join( TT::Lib::PATH_LIBS_CEXT, 'tt_api' )
    else

      # Compatibility shim. Since Ruby 2.0 ships with the standard library which
      # include Win32API it will be used instead. But that means some tweaks
      # must be used to make the two libs interchangable.
      # This is just a quick fix to make old code compatble without requiring to
      # recompile for every new SketchUp release where binary C extensions are
      # made incompatible. New extensions should use `fiddle` instead.
      require File.join( TT::Lib::PATH, 'win32_shim.rb' )
      require "Win32API"
      class API
        def initialize(function, input, output, library)
          @api = ::Win32API.new(library, function, input, output)
        end
        def call(*args)
          args.map! { |arg| arg.nil? ? 0 : arg }
          @api.call(*args)
        end
      end # class API

    end # if RUBY_VERSION


    # C/C++ constants.
    NULL = 0

    # Windows constants.
    MAX_PATH = 260

    # Code Page Identifiers
    # http://msdn.microsoft.com/en-us/library/windows/desktop/dd317756%28v=vs.85%29.aspx
    CP_ACP = 0
    CP_UTF8 = 65001

    # SHGetFolderPath constants.
    SHGFP_TYPE_CURRENT = 0
    SHGFP_TYPE_DEFAULT = 1

    # CSIDL
    # http://msdn.microsoft.com/en-us/library/bb762494%28v=vs.85%29.aspx
    CSIDL_LOCAL_APPDATA = 0x001c

    # Window Styles
    # http://msdn.microsoft.com/en-us/library/czada357.aspx
    # http://msdn.microsoft.com/en-us/library/ms632680%28VS.85%29.aspx
    # http://msdn.microsoft.com/en-us/library/ms632600%28v=vs.85%29.aspx
    WS_CAPTION      = 0x00C00000
    WS_SYSMENU      = 0x00080000
    WS_MAXIMIZEBOX  = 0x10000
    WS_MINIMIZEBOX  = 0x20000
    WS_SIZEBOX      = 0x40000
    WS_POPUP        = 0x80000000

    WS_EX_TOOLWINDOW = 0x00000080
    WS_EX_NOACTIVATE = 0x08000000

    # GetWindowLong() flags
    # http://msdn.microsoft.com/en-us/library/ms633584%28v=vs.85%29.aspx
    GWL_STYLE   = -16
    GWL_EXSTYLE = -20

    # SetWindowPos() flags
    # http://msdn.microsoft.com/en-us/library/ms633545%28v=vs.85%29.aspx
    SWP_NOSIZE       = 0x0001
    SWP_NOMOVE       = 0x0002
    SWP_NOACTIVATE   = 0x0010
    SWP_DRAWFRAME    = 0x0020
    SWP_FRAMECHANGED = 0x0020
    SWP_NOREPOSITION = 0x0200

    HWND_BOTTOM     =  1
    HWND_TOP        =  0
    HWND_TOPMOST    = -1
    HWND_NOTOPMOST  = -2

    # GetWindow() flags
    # http://msdn.microsoft.com/en-us/library/ms633515%28v=vs.85%29.aspx
    #GW_HWNDFIRST    = 0
    #GW_HWNDLAST     = 1
    #GW_HWNDNEXT     = 2
    #GW_HWNDPREV     = 3
    #GW_OWNER        = 4
    #GW_CHILD        = 5
    #GW_ENABLEDPOPUP = 6

    # GetAncestor() flags
    # http://msdn.microsoft.com/en-us/library/ms633502%28v=vs.85%29.aspx
    GA_PARENT     = 1
    GA_ROOT       = 2
    GA_ROOTOWNER  = 3

    # PeekMessage() flags
    PM_NOREMOVE = 0x0000 # Messages are not removed from the queue after processing by PeekMessage.
    PM_REMOVE   = 0x0001 # Messages are removed from the queue after processing by PeekMessage.
    PM_NOYIELD  = 0x0002 # Prevents the system from releasing any thread that is waiting for the caller to go idle (see WaitForInputIdle).


    # Windows Functions
    # L = Long (includes hwnd)
    # I = Integer
    # P = Pointer
    # V = Void
    GetAncestor         = API.new('GetAncestor' , 'LI', 'L', 'user32')

    # http://msdn.microsoft.com/en-us/library/ms646292%28v=vs.85%29.aspx
    # http://blogs.msdn.com/b/oldnewthing/archive/2008/10/06/8969399.aspx
    #
    # The return value is the handle to the active window attached to the calling
    # thread's message queue. Otherwise, the return value is NULL.
    #
    # To get the handle to the foreground window, you can use GetForegroundWindow.
    GetActiveWindow     = API.new('GetActiveWindow', '', 'L', 'user32')

    # http://msdn.microsoft.com/en-us/library/ms646311%28v=vs.85%29.aspx
    SetActiveWindow     = API.new('SetActiveWindow', 'L', 'L', 'user32')

    SetWindowPos        = API.new('SetWindowPos' , 'LLIIIII', 'I', 'user32')
    SetWindowLong       = API.new('SetWindowLong', 'LIL', 'L', 'user32')
    GetWindowLong       = API.new('GetWindowLong', 'LI' , 'L', 'user32')
    GetWindowText       = API.new('GetWindowText', 'LPI', 'I', 'user32')
    GetWindowTextLength = API.new('GetWindowTextLength', 'L', 'I', 'user32')

    # http://msdn.microsoft.com/en-us/library/ms644943%28v=vs.85%29.aspx
    PeekMessage         = API.new('PeekMessage' , 'PLIII', 'I', 'user32')

    OutputDebugString   = API.new('OutputDebugString', 'P', 'V', 'kernel32')

    if RUBY_VERSION.to_i < 2
      # Raises an error in Ruby 2.2 because it uses Fiddle directly while 2.0
      # used DL. It didn't work in 2.0, but it didn't raise an error.
      EnumThreadWindows = API.new('EnumThreadWindows', 'LKP', 'I', 'user32')
    end

    # Process.pid returns the same value as GetCurrentProcessId.
    #GetCurrentProcessId = API.new('GetCurrentProcessId' , '', 'L', 'kernel32')
    GetCurrentThreadId = API.new('GetCurrentThreadId', '', 'L', 'kernel32')

    # Shell functons.
    SHGetFolderPath = API.new('SHGetFolderPathW', 'LILLP', 'I', 'shell32')

    # File functions.
    GetShortPathName = API.new('GetShortPathNameW', 'PPL', 'L', 'kernel32')

    # String encoding.
    MultiByteToWideChar = API.new('MultiByteToWideChar', 'LLPIPI', 'I', 'kernel32')
    WideCharToMultiByte = API.new('WideCharToMultiByte', 'LLPIPIPP', 'I', 'kernel32')
  end
  # There is a limit to how many API object can be defined. So these are only
  # created at the start of the session. TT::Lib.reload will not update any
  # changes to the section that defines the API calls. SketchUp needs to be
  # restarted.
  file_loaded( __FILE__ )


  # (i)
  # To obtain a window's owner window, instead of using GetParent, use GetWindow
  # with the GW_OWNER flag. To obtain the parent window and not the owner,
  # instead of using GetParent, use GetAncestor with the GA_PARENT flag.


  #(i)
  # FindWindowLike
  # http://support.microsoft.com/kb/147659

  # EnumChildWindows
  # http://msdn.microsoft.com/en-us/library/ms633494%28v=vs.85%29.aspx
  # http://stackoverflow.com/questions/3327666/win32s-findwindow-can-find-a-particular-window-with-the-exact-title-but-what


  # If one creates too many Win32::API::Callbacks one get the following error:
  # +Error: #<Win32::API::Error: too many callbacks are defined.>+ This presents
  # a problem when you need to enumerate windows.
  #
  # Use this class to avoid this.
  #
  #  param = 'SketchUpMainWindow'
  #  enumWindowsProc = EnumWindowsProc.new(param) { |handle|
  #    # Return 0 (FALSE) to stop enumeration or 1 (TRUE) to proceed.
  #  }
  #  EnumThreadWindows.call(threadId, enumWindowsProc.callback, param)
  class EnumWindowsProc

    attr_reader( :param )

    @@enumThreadWndProc = API::Callback.new('LP', 'I'){ |hwnd, param|
      if @@callbacks.key?( param )
        @@callbacks[ param ].call( hwnd )
      end
    }
    @@callbacks = {}

    # @param [String] param - the +param+ argument this callback should repond to.
    def initialize( param, &block )
      @param = param.dup
      @@callbacks[ param ] = block
    end

    def callback
      @@enumThreadWndProc
    end

    def destroy_callback( param )
      @@callbacks.delete?( param )
    end

  end if RUBY_VERSION.to_i < 2 # class EnumThreadWndProc


  # Returns the window handle of the SketchUp window for the input queue of the
  # calling ruby method.
  #
  # @return [Integer] Returns a window handle on success or +nil+ on failure
  # @since 2.5.0
  if RUBY_VERSION.to_i < 2
    def self.get_sketchup_window
      threadId = GetCurrentThreadId.call
      hwnd = 0
      param = 'SketchUpMainWindow'
      enumWindowsProc = EnumWindowsProc.new( param ) { |handle|
        hwnd = GetAncestor.call( handle, GA_ROOTOWNER )
        0
      }
      EnumThreadWindows.call( threadId, enumWindowsProc.callback, param )
      hwnd
    end
  else
    def self.get_sketchup_window
      Shim.get_main_window_handle
    end
  end


  # @return [Boolean]
  # @since 2.6.0
  def self.activate_sketchup_window
    hwnd = self.get_sketchup_window
    return false unless hwnd
    SetActiveWindow.call( hwnd )
  end


  # @param [Integer] hwnd
  #
  # @return [String|Nil]
  # @since 2.5.0
  def self.get_window_text(hwnd)
    # Create a string buffer for the window text.
    buf_len = GetWindowTextLength.call(hwnd)
    return nil if buf_len == 0
    str = ' ' * (buf_len + 1)
    # Retreive the text.
    result = GetWindowText.call(hwnd, str, str.length)
    return nil if result == 0
    str.strip
  end


  # @example TT::Win32.get_folder_path( TT::Win32::CSIDL_LOCAL_APPDATA )
  #
  # @param [Integer] csidl
  #
  # @return [String|Nil]
  # @since 2.9.0
  def self.get_folder_path( csidl )
    path = self.get_folder_path_utf16( csidl )
    self.utf16_to_utf8( path )
  end


  # @example TT::Win32.get_folder_path_ansi( TT::Win32::CSIDL_LOCAL_APPDATA )
  #
  # @param [Integer] csidl
  #
  # @return [String|Nil]
  # @since 2.9.0
  def self.get_folder_path_ansi( csidl )
    path = self.get_folder_path_utf16( csidl )
    self.utf16_to_ansi( path )
  end


  # @example TT::Win32.get_short_folder_path( TT::Win32::CSIDL_LOCAL_APPDATA )
  #
  # @param [Integer] csidl
  #
  # @return [String|Nil]
  # @since 2.9.0
  def self.get_short_folder_path( csidl )
    path = self.get_short_folder_path_utf16( csidl )
    self.utf16_to_utf8( path )
  end


  # @example TT::Win32.get_short_folder_path_ansi( TT::Win32::CSIDL_LOCAL_APPDATA )
  #
  # @param [Integer] csidl
  #
  # @return [String|Nil]
  # @since 2.9.0
  def self.get_short_folder_path_ansi( csidl )
    path = self.get_short_folder_path_utf16( csidl )
    self.utf16_to_ansi( path )
  end


  # @private
  #
  # @param [Integer] csidl
  #
  # @return [String|Nil]
  # @since 2.9.0
  def self.get_folder_path_utf16( csidl )
    lpszLongPath = ' ' * ( MAX_PATH * 2 )
    SHGetFolderPath.call( nil, csidl, nil, SHGFP_TYPE_CURRENT, lpszLongPath )
    lpszLongPath
  end


  # @private
  #
  # @param [Integer] csidl
  #
  # @return [String|Nil]
  # @since 2.9.0
  def self.get_short_folder_path_utf16( csidl )
    lpszLongPath = self.get_folder_path_utf16( csidl )
    lpszShortPath_len = GetShortPathName.call( lpszLongPath, nil, 0 )
    lpszShortPath = ' ' * ( lpszShortPath_len * 2 )
    GetShortPathName.call( lpszLongPath, lpszShortPath, lpszShortPath_len )
    lpszShortPath
  end


  # @param [String] utf8_string
  #
  # @return [String|Nil]
  # @since 2.9.0
  def self.utf8_to_utf16( utf8_string )
    utf16_string_len = MultiByteToWideChar.call( CP_UTF8, 0,
      "#{utf8_string}\0", -1, nil, 0 )
    utf16_string = ' ' * ( utf16_string_len * 2 )
    MultiByteToWideChar.call( CP_UTF8, 0,
      "#{utf8_string}\0", -1, utf16_string, utf16_string_len )
    utf16_string
  end


  # @param [String] utf16_string
  #
  # @return [String|Nil]
  # @since 2.9.0
  def self.utf16_to_ansi( utf16_string )
    self.utf16_to_codepage( utf16_string, CP_ACP )
  end


  # @param [String] utf16_string
  #
  # @return [String|Nil]
  # @since 2.9.0
  def self.utf16_to_utf8( utf16_string )
    self.utf16_to_codepage( utf16_string, CP_UTF8 )
  end


  # @private
  #
  # @param [Integer] codepage
  #
  # @return [String|Nil]
  # @since 2.9.0
  def self.utf16_to_codepage( utf16_string, codepage )
    num_bytes = WideCharToMultiByte.call( codepage, 0,
      utf16_string, -1, nil, 0, nil, nil )
    out_buffer = ' ' * num_bytes
    WideCharToMultiByte.call( codepage, 0,
      utf16_string, -1, out_buffer, num_bytes, nil, nil )
    out_buffer.strip.strip # First strip doesn't strip NULL character.
  end


  # Call after webdialog.show to change the window into a toolwindow. Spesify the
  # window title so the method can verify it changes the correct window.
  #
  # @param [String] window_title
  #
  # @return [Nil]
  # @since 2.5.0
  def self.make_toolwindow_frame(window_title)
    # Retrieves the window handle to the active window attached to the calling
    # thread's message queue.
    hwnd = GetActiveWindow.call
    return nil if hwnd == 0

    # Verify window text as extra security to ensure it's the correct window.
    buf_len = GetWindowTextLength.call(hwnd)
    return nil if buf_len == 0

    str = ' ' * (buf_len + 1)
    result = GetWindowText.call(hwnd, str, str.length)
    return nil if result == 0

    return nil unless str.strip == window_title.strip

    # Set frame to Toolwindow
    style = GetWindowLong.call(hwnd, GWL_EXSTYLE)
    return nil if style == 0

    new_style = style | WS_EX_TOOLWINDOW
    result = SetWindowLong.call(hwnd, GWL_EXSTYLE, new_style)
    return nil if result == 0

    # Remove and disable minimze and maximize
    # http://support.microsoft.com/kb/137033
    style = GetWindowLong.call(hwnd, GWL_STYLE)
    return nil if style == 0

    style = style & ~WS_MINIMIZEBOX
    style = style & ~WS_MAXIMIZEBOX
    result = SetWindowLong.call(hwnd, GWL_STYLE,  style)
    return nil if result == 0

    # Refresh the window frame
    # (!) SWP_NOZORDER | SWP_NOOWNERZORDER
    flags = SWP_FRAMECHANGED | SWP_NOSIZE | SWP_NOMOVE | SWP_NOACTIVATE
    result = SetWindowPos.call(hwnd, 0, 0, 0, 0, 0, flags)
    result != 0
  end


  # Removes the Min and Max button.
  #
  # Call after webdialog.show to change the window into a toolwindow. Spesify the
  # window title so the method can verify it changes the correct window.
  #
  # @param [String] window_title
  #
  # @return [Nil]
  # @since 2.6.0
  def self.window_no_resize( window_title )
    # Retrieves the window handle to the active window attached to the calling
    # thread's message queue.
    hwnd = GetActiveWindow.call
    return nil if hwnd == 0

    # Verify window text as extra security to ensure it's the correct window.
    buf_len = GetWindowTextLength.call(hwnd)
    return nil if buf_len == 0

    str = ' ' * (buf_len + 1)
    result = GetWindowText.call(hwnd, str, str.length)
    return nil if result == 0

    return nil unless str.strip == window_title.strip

    # Remove and disable minimze and maximize
    # http://support.microsoft.com/kb/137033
    style = GetWindowLong.call(hwnd, GWL_STYLE)
    return nil if style == 0

    style = style & ~WS_MINIMIZEBOX
    style = style & ~WS_MAXIMIZEBOX
    result = SetWindowLong.call(hwnd, GWL_STYLE,  style)
    return nil if result == 0

    # Refresh the window frame
    # (!) SWP_NOZORDER | SWP_NOOWNERZORDER
    flags = SWP_FRAMECHANGED | SWP_NOSIZE | SWP_NOMOVE | SWP_NOACTIVATE
    result = SetWindowPos.call(hwnd, 0, 0, 0, 0, 0, flags)
    result != 0
  end


  # Allows the SketchUp process to process it's queued messages. Avoids whiteout.
  #
  # @return [Boolean]
  # @since 2.5.0
  def self.refresh_sketchup
    PeekMessage.call( nil, nil, 0, 0, PM_NOREMOVE ) != 0
  end


  # @param [String] file
  #
  # @return [String]
  # @since 2.9.0
  def self.is_virtualized?( file )
    virtualfile = self.get_virtual_path( file )
    !virtualfile.nil? && File.exist?( virtualfile )
  end


  # @param [String] file
  #
  # @return [String, Nil]
  # @since 2.9.0
  def self.get_virtual_file( file )
    filename = File.basename( file )
    filepath = File.dirname( file )
    # Verify file exists.
    unless File.exist?( file )
      raise IOError, "The file '#{file}' does not exist."
    end
    if ENV['LOCALAPPDATA'].nil?
      return nil
    end
    virtualstore = File.join( ENV['LOCALAPPDATA'], 'VirtualStore' )
    path = filepath.split(':')[1]
    virtual_path = File.join( virtualstore, path, filename )
    File.expand_path( virtual_path )
  end

end if TT::System.is_windows? # module TT::Win32
