{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE Trustworthy #-}

-- |
-- module: System.Win32.Error.Foreign
-- copyright: (c) Michael Steele, 2014
-- license: BSD3
-- maintainer: mikesteele81@gmail.com
-- stability: experimental
-- portability: Windows
-- This module provides functions which can be used as drop-in replacements
-- for Win32 when writing wrappers to foreign imports.
-- You will likely need to import modules from Win32 as well. To avoid
-- accidentally calling the standard error handling functions it's a good idea
-- to hide a few names:
-- > import qualified System.Win32.Error.Foreign as E
-- > import System.Win32 hiding (failIfFalse_, failIf, failUnlessSuccess, failWith)
-- Handling error conditions in Windows revolves around a thread-local
-- global variable representing the most recent error condition. Functions
-- indicate that an error occurred in various ways. The C++ programmer will
-- observe that a function failed, and immediately call GetLastError to
-- retrieve details on the possible cause or to get a localized error message
-- which can be relayed to a human in some way.
-- There are some cases where an error code may mean different things
-- depending on varying context, but in general these codes are globally
-- unique. Microsoft documents which error codes may be expected for any
-- given function.
-- When working with functions exported by Win32, error conditions are dealt
-- with using the `IOError` exception type. Most native Win32 functions return
-- an error code which can be used to determine whether something went wrong
-- during its execution. By convention these functions are all named something
-- of the form "c_DoSomething" where "DoSomething" matches the name given by
-- Microsoft. A haskell wrapper function named "doSomething" will typically,
-- among other things, check this error code. Based on its value the operating
-- system will be queried for additional error information, and a Haskell
-- exception will be thrown.
-- Consider the `System.Win32.File.createFile` function used to
-- open existing files which may or may not actually exist.
-- > createFile "c:\\nofilehere.txt" gENERIC_READ
-- >            fILE_SHARE_NONE Nothing oPEN_EXISTING 0 Nothing
-- If no file by that name exists the underlying `Win32.c_CreateFile` call will
-- return `Win32.iNVALID_HANDLE_VALUE`. This will result in an `IOError` exception
-- being thrown with a `String` value indicating the function and file name.
-- Internally, the `IOError` will also contain the error code, which will be
-- converted to a general Haskell value.
-- The Win32-errors package works similarly. A (simplified) wrapper around
-- c_CreateFile could be written as follows. Source code
-- from the Win32 package often provides a good starting point:
-- > createFile name access mode = withTString name $ \ c_name ->
-- >     E.failIf (== E.toDWORD E.InvalidHandle) "CreateFile" $
-- >     c_CreateFile c_name access fILE_SHARE_NONE nullPtr
-- >                  mode 0 nullPtr
module System.Win32.Error.Foreign
  ( failIf
  , failIfFalse_
  , failIfNull
  , failUnlessSuccess
  , failWith
  , errorWin
  ) where

import Control.Exception
import Data.Char
import Data.Text as T
import Data.Text.Foreign as T
import Foreign
import Numeric
import System.Win32 (DWORD, LPTSTR)
import qualified System.Win32 as Win32

import System.Win32.Error.Types

#include "windows_cconv.h"

-- |This function mirrors the Win32 package's 'System.Win32.Types.failIfFalse_'
-- function.
failIfFalse_ :: Text -> IO Bool -> IO ()
failIfFalse_ wh act = failIf not wh act >> return ()

-- |Copied from the Win32 package. Use this to throw a Win32 exception
-- when an action returns a value satisfying the given predicate.
-- The exception thrown will depend on a thead-local global error condition.
-- The supplied `Text` value should be set to the human-friendly name of the
-- action that triggered the error.
failIf :: (a -> Bool) -> Text -> IO a -> IO a
failIf p wh act = do
    v <- act
    if p v then errorWin wh else return v

-- |This function mirrors the Win32 package's 'System.Win32.Types.failIfNull'
-- function.
failIfNull :: Text -> IO (Ptr a) -> IO (Ptr a)
failIfNull = failIf (== nullPtr)

-- |Perform the supplied action, and throw a `Win32Exception` exception if the
-- return code is anything other than `Success`. The supplied action returns
-- a `DWORD` instead of an `ErrCode` so that foreign imports can be used more
-- conveniently.
failUnlessSuccess :: Text -> IO DWORD -> IO ()
failUnlessSuccess fn_name act = do
    r <- act
    if r == toDWORD Success then return () else failWith' fn_name r

-- |Windows maintains a thread-local value representing the previously triggered
-- error code. Calling `errorWin` will look up the value, and throw a `Win32Exception`
-- exception. The supplied `Text` argument should be set to the name of the function
-- which triggered the error condition.
-- Calling this action when no error has occurred (0x00000000 -- ERROR_SUCCESS) will
-- result in an exception being thrown for the `Success` error code.
errorWin :: Text -> IO a
errorWin fn_name = Win32.getLastError >>= failWith' fn_name

-- |Like failWith, but avoid multiple conversions to and from 'ErrCode'.
failWith' :: Text -> DWORD -> IO a
failWith' fn_name err_code = do
    msg <- formatMessage err_code
    -- drop trailing \n
    let msg' = T.reverse . T.dropWhile isSpace . T.reverse $ msg
    throw $ Win32Exception fn_name (fromDWORD err_code) msg'

-- |Throw a `Win32Exception` exception for the given function name and error code.
failWith :: Text -> ErrCode -> IO a
failWith fn_name err_code = failWith' fn_name $ toDWORD err_code

#define FORMAT_MESSAGE_FROM_SYSTEM     0x00001000

-- |This function doesn't belong in Win32-errors, which is why it isn't
-- exported. FormatMessage is required to get the standard system message
-- associated with an error code.
formatMessage :: DWORD -> IO Text
formatMessage err =
    -- Specifying FORMAT_MESSAGE_ALLOCATE_BUFFER changes the lpBuffer argument
    -- to a pointer to LPTSTR
    alloca $ \ ppBuffer -> do
                             nullPtr err 0 (castPtr ppBuffer) 0 nullPtr
    pBuffer <- peek ppBuffer
    if (len == 0 || pBuffer == nullPtr)
       then return $ "Error 0x" `T.append` T.pack (Numeric.showHex err "")
       else fromPtr pBuffer (fromIntegral len)

-- DWORD WINAPI FormatMessage(
--   _In_      DWORD dwFlags,
--   _In_opt_  LPCVOID lpSource,
--   _In_      DWORD dwMessageId,
--   _In_      DWORD dwLanguageId,
--   _Out_     LPTSTR lpBuffer,
--   _In_      DWORD nSize,
--   _In_opt_  va_list *Arguments
-- );
foreign import WINDOWS_CCONV "Windows.h FormatMessageW"
    c_FormatMessage :: DWORD -> Ptr () -> DWORD -> DWORD -> LPTSTR -> DWORD
                    -> Ptr () -> IO DWORD