{-# OPTIONS_GHC -fno-warn-tabs #-}
{-|
Module      : HexChat
Description : HexChat scripting interface
Copyright   : (C) 2017 mniip
License     : MIT
Maintainer  : mniip@mniip.com
Stability   : none
Portability : none

A HexChat script is a @.hs@ file that defines a global identifier @info@ with type 'ModInfo'. This structure contains the necessary metadata about a script as well its initialization and deinitialization functions. From your initialization function you may want to hook some events. Any remaining hooks are automatically unhooked after the deinitializer so you don't have to worry about that.
-}
module HexChat
	(
		-- * Module Info
		ModInfo(..), modInfo,

		-- * Interfacing HexChat
		command,
		print,
		emitPrint,
		EventAttrs(..),
		emitPrintAttrs,
		sendModes,
		nickCmp,
		strip,
		getPrefs,
		getPrefString,
		getPrefInt,
		getPrefBool,
		List,
		listGet,
		listFields,
		listNext,
		listStr,
		listInt,
		listTime,

		-- * Hooks
		-- | All hooks have a priority that determines the order in which HexChat invokes them. Each hook can also affect the propagation of the event to other hooks.
		I.pri_HIGHEST, I.pri_HIGH, I.pri_NORM, I.pri_LOW, I.pri_LOWEST,
		Eat(..),
		Hook,
		hookCommand,
		hookPrint,
		hookPrintAttrs,
		hookServer,
		hookServerAttrs,
		unhook,

		-- * Contexts
		-- | Some HexChat interface functions are specific to a given 'Context', which corresponds to a tab or window. By default whenever a hook is invoked it is running in the event's relevant context. If you need to output text or commands to a different tab you should change the context.
		Context,
		findContext,
		getContext,
		setContext,
		withContext
	)
	where

import Prelude hiding (print)

import Control.Exception
import Control.Monad
import Data.IORef
import Foreign.C.Types

import HexChat.Internal (StaticData, staticData, lHooks, getPlugin, getHandle, withHandle, Hook, Eat(..), EventAttrs(..), Context, List)
import qualified HexChat.Internal as I

-- | This datatype contains metadata about the script. Every script should define an global binding named @info@ of this type. This structure contains internal fields necessary for scripting to work, so when constructing an object please use the default value 'modInfo' with record modification notation, like this:
--
-- @
-- info = 'modInfo' { 'modName' = "a module", 'modAuthor' = "somepony", ... }
-- @
--
-- The type parameter signifies the return type of the initializer and has no special meaning otherwise, you can specialize it to anything you want.
--
-- If you need to pass a lot of data to the deinitializer you could simply let @a ~ 'IO' ()@ and @'modDeinit' = 'id'@, such as in the following:
--
-- @
-- info = 'modInfo'
--     { 'modInit' = do
--           x <- createX
--           'return' $ do
--               destroyX x
--     , 'modDeinit' = 'id'
--     }
-- @
data ModInfo a = ModInfo
	{
		modName :: String, -- ^ Name of the script.
		modVersion :: String, -- ^ Version of the script.
		modAuthor :: String, -- ^ Author of the script.
		modDescription :: String, -- ^ A short description of the script that can fit in the "Plugins and Scripts" list window.
		modInit :: IO a, -- ^ This is the entry point of the script. It will be executed immediately after the script is compiled and loaded. The returned value will be opaquely passed to 'modDeinit'.
		modDeinit :: a -> IO (), -- ^ This function will be executed shortly before the script is unloaded. You can place any cleanup routines here. As argument it receives whatever 'modInit' returned.
		modStaticData :: StaticData -- ^ An internal field that lets the different (to the linker) instances of the HexChat library identify eachother. Please inherit the value of this field from 'modInfo'.
	}

-- | A default value for 'ModInfo'. This has some sensible defaults and also provides the values of internal fields necessary for the operation of the interface.
modInfo :: ModInfo a
modInfo = ModInfo
	{
		modName = "module",
		modVersion = "",
		modAuthor = "",
		modDescription = "No description provided",
		modInit = return $ error "modInit",
		modDeinit = \_ -> return (),
		modStaticData = staticData
	}

-- | Invoke a HexChat command as if the user has typed it in the inputbox. Do not include the leading @/@.
command :: String -> IO ()
command str = do
	plugin <- getPlugin
	I.command plugin str

-- | Print some text to the buffer.
print :: String -> IO ()
print str = do
	plugin <- getPlugin
	I.print plugin str

-- | Output a Text Event to the buffer. First argument is the Text Event identifier (see @Settings -> Text Events@), second is the list of event's parameters.
emitPrint :: String -> [String] -> IO Bool
emitPrint event vs = do
	plugin <- getPlugin
	I.emitPrint plugin event vs

-- | Same as 'emitPrint' but also lets you specify Event Attributes for the event.
emitPrintAttrs :: EventAttrs -> String -> [String] -> IO Bool
emitPrintAttrs attrs event vs = do
	plugin <- getPlugin
	I.emitPrintAttrs plugin attrs event vs

-- | @'sendModes' targets n sign mode@ where @sign@ is @+@ or @-@ and @mode@ is a channel mode character will set the specified channel mode on each of the specified targets, sending @n@ modes per line, or the server's advertised maximum if @n@ is zero.
sendModes :: [String] -> Int -> Char -> Char -> IO ()
sendModes targets modes sign mode = do
	plugin <- getPlugin
	I.sendModes plugin targets modes sign mode

-- this should really be 'IO (String -> String -> Ordering)' but the underlying C API wouldn't let you easily do that...
-- | Compare two nicknames according to the rules of the server.
nickCmp :: String -> String -> IO Ordering
nickCmp s1 s2 = do
	plugin <- getPlugin
	I.nickCmp plugin s1 s2

-- | @'strip' colors format@ will strip colors if @colors@ is 'True', and miscellaneous formatting if @formatting@ is 'True', from the provided string.
strip :: Bool -> Bool -> String -> IO String
strip sc sf str = do
	plugin <- getPlugin
	I.strip plugin sc sf str

-- | Get a HexChat preference value (see @/set@). You should pass 3 continuations one of which will be invoked depending on the type of the actual preference. Returns 'Nothing' if no preference with that name exists.
getPrefs :: String -> (String -> IO a) -> (Int -> IO a) -> (Bool -> IO a) -> IO (Maybe a)
getPrefs key cstr cint cbool = do
	plugin <- getPlugin
	I.getPrefs plugin key cstr cint cbool

-- | Return the value of a preference that is supposedly a String. Returns 'Nothing' if no such preference exists or it is of the wrong type.
getPrefString :: String -> IO (Maybe String)
getPrefString key = join <$> getPrefs key (return . Just) (const $ return Nothing) (const $ return Nothing)

-- | Return the value of a preference that is supposedly an Int. Returns 'Nothing' if no such preference exists or it is of the wrong type.
getPrefInt :: String -> IO (Maybe Int)
getPrefInt key = join <$> getPrefs key (const $ return Nothing) (return . Just) (const $ return Nothing)

-- | Return the value of a preference that is supposedly a Bool. Returns 'Nothing' if no such preference exists or it is of the wrong type.
getPrefBool :: String -> IO (Maybe Bool)
getPrefBool key = join <$> getPrefs key (const $ return Nothing) (const $ return Nothing) (return . Just)

listGet :: String -> IO (Maybe List)
listGet key = do
	plugin <- getPlugin
	I.listGet plugin key

listFields :: String -> IO [String]
listFields key = do
	plugin <- getPlugin
	I.listFields plugin key

listNext :: List -> IO Bool
listNext list = do
	plugin <- getPlugin
	I.listNext plugin list

listStr :: List -> String -> IO String
listStr list key = do
	plugin <- getPlugin
	I.listStr plugin list key

listInt :: List -> String -> IO Int
listInt list key = do
	plugin <- getPlugin
	I.listInt plugin list key

listTime :: List -> String -> IO CTime
listTime list key = do
	plugin <- getPlugin
	I.listTime plugin list key

-- | @'hookCommand' cmd priority description f@ registers a command named @cmd@ (with description @description@). The given @f@ will be passed a list of command's arguments (proper words) and a list of "leftovers" for every position in the word list (with the exact original whitespace).
--
-- If @cmd@ is @""@ then instead the hook will be invoked whenever the user types anything not beginning with @/@.
hookCommand :: String -> CInt -> String -> ([String] -> [String] -> IO Eat) -> IO Hook
hookCommand cmd pri desc f = do
	plugin <- getPlugin
	handle <- getHandle
	hook <- I.hookCommand plugin cmd pri desc $ \w we -> withHandle handle $ f w we
	readIORef lHooks >>= flip modifyIORef ((handle, hook) :)
	return hook

-- | @'hookPrint' event priority f@ hooks @f@ to be invoked whenever a Text Event @event@ is to be displayed on screen. The given @f@ will be passed a list of event's parameters.
hookPrint :: String -> CInt -> ([String] -> IO Eat) -> IO Hook
hookPrint cmd pri f = do
	plugin <- getPlugin
	handle <- getHandle
	hook <- I.hookPrint plugin cmd pri $ \w -> withHandle handle $ f w
	readIORef lHooks >>= flip modifyIORef ((handle, hook) :)
	return hook

-- | @'hookPrintAttrs' event priority f@ hooks @f@ to be invoked whenever a Text Event @event@ is to be displayed on screen. The given @f@ will be passed a list of event's parameters, and the attributes.
hookPrintAttrs :: String -> CInt -> ([String] -> EventAttrs -> IO Eat) -> IO Hook
hookPrintAttrs cmd pri f = do
	plugin <- getPlugin
	handle <- getHandle
	hook <- I.hookPrintAttrs plugin cmd pri $ \w attrs -> withHandle handle $ f w attrs
	readIORef lHooks >>= flip modifyIORef ((handle, hook) :)
	return hook

-- | @'hookServer' word priority f@ hooks @f@ to be invoked whenever a @word@ command arrives from the IRC server. The given @f@ will be passed a list of command's arguments (proper words) and a list of "leftovers" for every position in the word list (with the exact original whitespace). Processing of @:@ long arguments is *NOT* done.
--
-- If @cmd@ is @"RAW LINE"@ then the hook will be invoked for all commands received from the server.
hookServer :: String -> CInt -> ([String] -> [String] -> IO Eat) -> IO Hook
hookServer cmd pri f = do
	plugin <- getPlugin
	handle <- getHandle
	hook <- I.hookServer plugin cmd pri $ \w we -> withHandle handle $ f w we
	readIORef lHooks >>= flip modifyIORef ((handle, hook) :)
	return hook

-- | @'hookServer' word priority f@ hooks @f@ to be invoked whenever a @word@ command arrives from the IRC server. The given @f@ will be passed a list of command's arguments (proper words), a list of "leftovers" for every position in the word list (with the exact original whitespace), and the event attributes. Processing of @:@ long arguments is *NOT* done.
--
-- If @cmd@ is @"RAW LINE"@ then the hook will be invoked for all commands received from the server.
hookServerAttrs :: String -> CInt -> ([String] -> [String] -> EventAttrs -> IO Eat) -> IO Hook
hookServerAttrs cmd pri f = do
	plugin <- getPlugin
	handle <- getHandle
	hook <- I.hookServerAttrs plugin cmd pri $ \w we attrs -> withHandle handle $ f w we attrs
	readIORef lHooks >>= flip modifyIORef ((handle, hook) :)
	return hook

-- | Remove the given hook. All hooks are automatically removed when the script is unloaded.
unhook :: Hook -> IO ()
unhook hook = do
	plugin <- getPlugin
	readIORef lHooks >>= flip modifyIORef (filter ((/= hook) . snd))
	I.unhook plugin hook

-- | @'findContext' mserver mtabname@ finds the context corresponding to the given tab name (or to the front tab if 'Nothing') in the given server (or in any of the servers if 'Nothing').
findContext :: Maybe String -> Maybe String -> IO (Maybe Context)
findContext server channel = do
	plugin <- getPlugin
	I.findContext plugin server channel

-- | Obtains the current context.
getContext :: IO Context
getContext = do
	plugin <- getPlugin
	I.getContext plugin

-- | Sets the current context. The scope of this function is limited to the currently executing hook (or the initializer/deinitializer).
setContext :: Context -> IO Bool
setContext context = do
	plugin <- getPlugin
	I.setContext plugin context

-- | Execute a given IO action in a given context a-la 'bracket'.
withContext :: Context -> IO a -> IO a
withContext context f = bracket (swap context) swap (const f)
	where swap context = do
		old <- getContext
		b <- setContext context
		unless b $ error "Could not set context"
		return old