packages feed

manatee-ircclient-0.1.0: Manatee/Extension/IrcClient/IrcBuffer.hs

-- Author:     Andy Stewart <lazycat.manatee@gmail.com>
-- Maintainer: Andy Stewart <lazycat.manatee@gmail.com>
-- 
-- Copyright (C) 2010 ~ 2011 Andy Stewart, all rights reserved.
-- 
-- This program is free software: you can redistribute it and/or modify
-- it under the terms of the GNU General Public License as published by
-- the Free Software Foundation, either version 3 of the License, or
-- any later version.
-- 
-- This program is distributed in the hope that it will be useful,
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-- GNU General Public License for more details.
-- 
-- You should have received a copy of the GNU General Public License
-- along with this program.  If not, see <http://www.gnu.org/licenses/>.

{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE OverloadedStrings, ScopedTypeVariables #-}
{-# LANGUAGE TemplateHaskell #-}
module Manatee.Extension.IrcClient.IrcBuffer where

import Config.Import
import Control.Applicative
import Control.Concurrent.STM
import Control.Monad
import DBus.Client hiding (Signal)
import Data.Array
import Data.Binary
import Data.DeriveTH
import Data.Map (Map)
import Data.Maybe
import Data.Set (Set)
import Data.Typeable
import Graphics.UI.Gtk hiding (Language, eventButton, eventClick, get)
import Graphics.UI.Gtk.Gdk.Events
import Graphics.UI.Gtk.SourceView.SourceBuffer
import Language.Translate.Google
import Manatee.Core.Config
import Manatee.Core.DBus
import Manatee.Core.Dynload
import Manatee.Core.Types
import Manatee.Extension.IrcClient.DBus
import Manatee.Extension.IrcClient.HighlightNick
import Manatee.Extension.IrcClient.PageMode
import Manatee.Extension.IrcClient.Smile
import Manatee.Extension.IrcClient.Types
import Manatee.Toolkit.General.Basic
import Manatee.Toolkit.General.ByteString
import Manatee.Toolkit.General.DBus
import Manatee.Toolkit.General.Map
import Manatee.Toolkit.General.Maybe
import Manatee.Toolkit.General.Process
import Manatee.Toolkit.General.STM
import Manatee.Toolkit.General.Set
import Manatee.Toolkit.General.Time
import Manatee.Toolkit.Gtk.Multiline
import Network.FastIRC.Types
import Network.URI
import System.Posix.Process
import Text.Regex.Posix hiding (after)

import qualified Data.ByteString.Char8 as B
import qualified Data.ByteString.UTF8 as UTF8
import qualified Data.Map as M
import qualified Data.Set as S
import qualified Graphics.UI.Gtk as Gtk

data IrcBuffer = 
    IrcBuffer {ircBufferServer            :: Server
              ,ircBufferPort              :: Port
              ,ircBufferChannel           :: Channel
              ,ircBufferClient            :: Client
              ,ircBufferPageId            :: PageId
              ,ircBufferMode              :: PageMode
              ,ircBufferBuffer            :: SourceBuffer
              ,ircBufferInsertMark        :: TextMark
              ,ircBufferPromptMark        :: TextMark
              ,ircBufferScrollMark        :: TVar (Maybe TextMark)
              ,ircBufferEditableTag       :: TextTag
              ,ircBufferMessageTag        :: TextTag
              ,ircBufferTimeStamp         :: TVar (Map Int (String, Color))
              ,ircBufferNick              :: TVar Nick
              ,ircBufferNickSet           :: TVar (Set NickName)
              ,ircBufferNickColorMap      :: TVar (Map NickName TextTag)
              ,ircBufferTranslateLanguage :: TVar Language
              ,ircBufferBroadcastChannel  :: TChan IrcBufferSignal
              ,ircBufferSmilePixbufs      :: Map String Pixbuf
              ,ircBufferCustomize         :: IrcCustomize
              ,ircBufferState             :: TVar IrcState
              }
    deriving Typeable

data IrcBufferSignal = SwitchTranslateLanguage 
                     | BufferChanged
                       deriving (Show, Eq, Ord)

data IrcState =
    IrcState {ircStateCursor              :: Maybe (Int, Int)
             ,ircStateScrolledPosition    :: (Double, Double)}

-- | Init state.
ircInitState :: IrcState
ircInitState =
    IrcState (Just (1, 0)) (0, 0)

-- | Create buffer new.
ircBufferNew :: String -> [String] -> Client -> PageId -> CustomizeWrap -> IO IrcBuffer
ircBufferNew info _ client pageId c = do
  -- Get customize option.
  let customize = castCustomize c

  -- Get server port channel information.
  (mynick, server, port, channel) <- ircParseInfo info customize
  putStrLn ("Buffer create (server, port, channel) : " ++ show (server, port, channel))

  -- Create source buffer and text mark.
  sourceBuffer <- sourceBufferNew Nothing
  ircBufferInsertPromptStr sourceBuffer
  insertMark <- ircBufferCreateInsertMark sourceBuffer
  promptMark <- ircBufferCreatePromptMark sourceBuffer
  scrollMark <- newTVarIO Nothing

  -- Create editable tag.
  textTag <- textTagNew Nothing
  set textTag [textTagEditable := False]
  tagTable <- textBufferGetTagTable sourceBuffer
  textTagTableAdd tagTable textTag

  -- Create message tag.
  messageTag <- textTagNew Nothing
  set messageTag [textTagEditable := False]
  textTagTableAdd tagTable messageTag

  -- Create color tag.
  ircBufferAddColorTag sourceBuffer (ircCustomizeServerColorTag customize)
  ircBufferAddColorTag sourceBuffer (ircCustomizeSelfColorTag customize)
  ircBufferAddColorTag sourceBuffer (ircCustomizeOtherColorTag customize)
  ircBufferAddColorTag sourceBuffer (ircCustomizeJoinColorTag customize)
  ircBufferAddColorTag sourceBuffer (ircCustomizeQuitColorTag customize)
  ircBufferAddColorTag sourceBuffer (ircCustomizePartColorTag customize)
  ircBufferAddColorTag sourceBuffer (ircCustomizeActionColorTag customize)
  ircBufferAddColorTag sourceBuffer (ircCustomizeUrlColorTag customize)
  let urlColorTag = ircCustomizeUrlColorTag customize
  set urlColorTag [textTagUnderline := UnderlineSingle]

  -- Open url when click.
  urlColorTag `onTextTagEvent` \ event iter -> 
      case event of
        Button {eventClick  = click
               ,eventButton = button} -> 
          when (button == LeftButton && click == SingleClick) $ 
            textBufferGetTagTextWithIter sourceBuffer iter urlColorTag 
              >?>= \ url ->
                mkDaemonSignal client NewTab (NewTabArgs "PageBrowser" url [])
        _ -> return ()

  -- Apply text tag.
  startIter <- textBufferGetStartIter sourceBuffer
  endIter   <- textBufferGetIterAtMark sourceBuffer promptMark
  textBufferApplyTag sourceBuffer textTag startIter endIter

  -- Time stamp.
  timeStamp <- newTVarIO M.empty

  -- Nick set.
  nick <- newTVarIO mynick

  -- Nick set.
  nickSet <- newTVarIO S.empty

  -- Nick color map.
  nickColorMap <- newTVarIO M.empty

  -- Target language.
  targetLanguage <- readTVarIO (ircCustomizeTargetLanguage customize)
  lang <- newTVarIO targetLanguage

  -- Broadcast channel.
  broadcastChannel <- newTChanIO :: IO (TChan IrcBufferSignal)

  -- Get smile pixbufs.
  smilePixbufs <- createSmilePixbufs

  -- Init state.
  state <- newTVarIO ircInitState

  -- Create buffer.
  let buffer = IrcBuffer server port channel
                         client pageId ircMode
                         sourceBuffer insertMark promptMark scrollMark 
                         textTag messageTag 
                         timeStamp nick nickSet nickColorMap lang
                         broadcastChannel smilePixbufs customize state

  -- Add self in nick map.
  ircBufferAddNick buffer (B.pack mynick)


  -- Build DBus match rule.
  processId <- getProcessID
  mkIrcClientMatchRules client
    [(ReceivePrivate,
      \ (ReceivePrivateArgs nick msg) -> 
          ircBufferReceivePrivate buffer nick msg)
    ,(ReceiveJoin,
     \ (ReceiveJoinArgs nick user host) ->
          ircBufferReceiveJoin buffer nick user host)
    ,(ReceiveTopicReply,
      \ (ReceiveTopicReplyArgs msg) ->
          ircBufferReceiveTopicReply buffer msg)
    ,(ReceiveTopicWhoTime,
      \ (ReceiveTopicWhoTimeArgs nick seconds) ->
          ircBufferReceiveTopicWhoTime buffer nick seconds)
    ,(ReceiveChannelUrl,
      \ (ReceiveChannelUrlArgs url) ->
          ircBufferReceiveChannelUrl buffer url)
    ,(ReceiveNames,
      \ (ReceiveNamesArgs nicks) -> 
          ircBufferReceiveNames buffer nicks)
    ,(ReceiveQuit,
      \ (ReceiveQuitArgs nick user host reason) ->
          ircBufferReceiveQuit buffer nick user host reason)
    ,(ReceivePart,
      \ (ReceivePartArgs nick user host reason) ->
          ircBufferReceivePart buffer nick user host reason)
    ,(DaemonProcessStartup,
     \_ -> mkIrcDaemonSignal client Join (JoinArgs server port channel mynick processId))]
    
  -- Check whether daemon process has startup.
  ifM (isBusNameExist ircDaemonBusName)
      -- Send join signal if daemon process has startup.
      (mkIrcDaemonSignal client Join (JoinArgs server port channel mynick processId))
      -- Otherwise startup daemon process, send join signal after daemon process startup complete.
      (do
         putStrLn "No irc daemon process, starting one."
         runProcess_ "manatee-irc-daemon" [show processId])

  -- Add function when main loop left.
  -- Send part signal to daemon process if tab closed.
  quitAdd 0 (do
              putStrLn $ "Irc process "  ++ show processId ++ " quit."
              mkIrcDaemonSignal client Part (PartArgs server channel processId)
              return False)

  -- Record scroll mark before change buffer.
  -- Then we can restore scroll mark position after buffer changed.
  sourceBuffer `on` bufferChanged $ do
    cursorMark <- textBufferGetInsert sourceBuffer
    promptIter <- textBufferGetIterAtMark sourceBuffer promptMark
    insertIter <- textBufferGetInsertIter sourceBuffer
    order <- textIterCompare insertIter promptIter
    case order of
      GT -> writeTVarIO scrollMark (Just cursorMark)
      EQ -> writeTVarIO scrollMark (Just cursorMark)
      _  -> writeTVarIO scrollMark Nothing

  -- Broadcast signal when buffer changed.
  sourceBuffer `after` bufferChanged $
     writeTChanIO broadcastChannel BufferChanged 


  return buffer

-- | Parse "irc:[//[user@][server[:port]][/]][#channel]".
ircParseInfo :: String -> IrcCustomize -> IO (Nick, Server, Port, Channel)
ircParseInfo str (IrcCustomize {ircCustomizeDefaultNick         = nickTVar
                               ,ircCustomizeDefaultServer       = serverTVar
                               ,ircCustomizeDefaultPort         = portTVar
                               ,ircCustomizeDefaultChannel      = channelTVar
                               }) = do
    defaultNick     <- readTVarIO nickTVar
    defaultServer   <- readTVarIO serverTVar
    defaultPort     <- readTVarIO portTVar
    defaultChannel  <- readTVarIO channelTVar
    return $ 
           if null matchTextList
              then (defaultNick, defaultServer, defaultPort, defaultChannel)
              else 
                  let (_ : (nickStr, _) : (serverStr, _) : (portStr, _) : (channelStr, _) : _) = elems $ head matchTextList
                      nick = if null nickStr then defaultNick else init nickStr
                      server = if null serverStr then defaultServer else serverStr
                      port = if null portStr then defaultPort else read $ tail portStr :: Int
                      channel = (\x -> case x of
                                        ('/':c) -> c 
                                        _ -> x
                                ) $ if null channelStr then defaultChannel else channelStr
                  in (nick, server, port, channel)
    where matchTextList =
              matchAllText 
                  (makeRegex ("irc://([^<> \t\n'@:]+@)*([a-zA-Z.]+)*(:[0-9]+)*(/*[$#+!].[^ \t\n,]+)*" :: String) :: Regex) 
                  str
          

-- | Receive private message.
ircBufferReceivePrivate :: IrcBuffer -> NickName -> CommandArg -> IO ()
ircBufferReceivePrivate buffer@(IrcBuffer {ircBufferCustomize = 
                                            IrcCustomize 
                                            {ircCustomizeSelfColorTag     = selfColorTag
                                            ,ircCustomizeOtherColorTag    = otherColorTag
                                            ,ircCustomizeActionColorTag   = actionColorTag}
                                          ,ircBufferNick = mynick}) 
                        nick content = do
  -- Format private message.
  ((msg, msgHeadLen), isAction) <-
        if B.pack "\SOHACTION " `B.isPrefixOf` content 
               && B.pack "\SOH" `B.isSuffixOf` content
           then do
               let prefixLen  = B.length (B.pack "\SOHACTION ")
                   suffixLen  = B.length (B.pack "\SOH")
                   takeLen    = B.length content - prefixLen - suffixLen
                   newContent = B.take takeLen $ B.drop prefixLen content
               args <- ircBufferIndentMessage buffer newContent (B.concat ["* ", nick, " "])
               return (args, True)
           else do 
               args <- ircBufferIndentMessage buffer content (B.concat ["<", nick, "> "]) 
               return (args, False)

  -- Get color with the type of private message.
  mynickname <- readTVarIO mynick
  let messageColor 
          -- Use selfColorTag when it's self message.
          | B.unpack nick == mynickname
              = selfColorTag
          -- User actionColorTag when print action message.
          | isAction 
              = actionColorTag
          -- Otherwise use otherColorTag.
          | otherwise 
              = otherColorTag

  -- Insert private message.
  ircBufferReceiveMsg buffer msg msgHeadLen messageColor

-- | Receive quit message.
ircBufferReceiveQuit :: IrcBuffer -> NickName -> UserName -> HostName -> B.ByteString -> IO ()
ircBufferReceiveQuit buffer@(IrcBuffer {ircBufferNickSet = nickSet
                                       ,ircBufferCustomize =
                                         IrcCustomize 
                                         {ircCustomizeQuitColorTag = quitColorTag}}) 
                     nick user host reason = do
  -- Quit nick map.
  set <- readTVarIO nickSet
  case maybeFindMin set (== nick) of
    Just _ -> do
      -- Remove from set.
      modifyTVarIO nickSet $ \ set ->
          S.delete nick set

      -- Format quit message.
      let adjustMsg = B.pack "*** "
          content   = B.concat [nick, " (", user, "@", host, ") has quit: ", reason]
      (msg, msgHeadLen) <- ircBufferIndentMessage buffer content adjustMsg
      
      -- Insert quit message.
      ircBufferReceiveMsg buffer msg msgHeadLen quitColorTag
    Nothing -> return ()

-- | Receive part message.
ircBufferReceivePart :: IrcBuffer -> NickName -> UserName -> HostName -> B.ByteString -> IO ()
ircBufferReceivePart buffer@(IrcBuffer {ircBufferNickSet        = nickSet
                                       ,ircBufferCustomize =
                                         IrcCustomize 
                                         {ircCustomizePartColorTag   = partColorTag}}) 
                     nick user host reason = do
  -- Remove from set.
  modifyTVarIO nickSet $ \ set ->
      S.delete nick set

  -- Format part message.
  let adjustMsg = B.pack "*** "
      content   = B.concat [nick, " (", user, "@", host, ") has part: ", reason]
  (msg, msgHeadLen) <- ircBufferIndentMessage buffer content adjustMsg
  
  -- Insert part message.
  ircBufferReceiveMsg buffer msg msgHeadLen partColorTag

-- | Receive topic replay message.
ircBufferReceiveNames :: IrcBuffer -> B.ByteString -> IO ()
ircBufferReceiveNames buffer nicks = 
  -- Add nick to color map.
  forM_ (B.words nicks) $ \nick -> 
    ircBufferAddNick buffer nick

-- | Receive topic replay message.
ircBufferReceiveChannelUrl :: IrcBuffer -> B.ByteString -> IO ()
ircBufferReceiveChannelUrl buffer@(IrcBuffer {ircBufferChannel          = channel
                                             ,ircBufferCustomize =
                                               IrcCustomize 
                                               {ircCustomizeServerColorTag   = serverColorTag}})
                           url = do
  -- Format channel url message.
  let adjustMsg = B.pack "*** "
      content   = B.concat [B.pack channel, " URL: ", url]
  (msg, msgHeadLen) <- ircBufferIndentMessage buffer content adjustMsg
      
  -- Insert channel url message.
  ircBufferReceiveMsg buffer msg msgHeadLen serverColorTag

-- | Receive topic replay message.
ircBufferReceiveTopicReply :: IrcBuffer -> CommandArg -> IO ()
ircBufferReceiveTopicReply buffer@(IrcBuffer {ircBufferChannel          = channel
                                             ,ircBufferCustomize =
                                               IrcCustomize 
                                               {ircCustomizeServerColorTag   = serverColorTag}})
                           message = do
  -- Format topic replay message.
  let adjustMsg = B.pack "*** "
      content   = B.concat ["Topic for ", B.pack channel, ": ", message]
  (msg, msgHeadLen) <- ircBufferIndentMessage buffer content adjustMsg
      
  -- Insert topic replay message.
  ircBufferReceiveMsg buffer msg msgHeadLen serverColorTag

-- | Receive topic replay message.
ircBufferReceiveTopicWhoTime :: IrcBuffer -> NickName -> Integer -> IO ()
ircBufferReceiveTopicWhoTime buffer@(IrcBuffer {ircBufferChannel        = channel
                                               ,ircBufferCustomize =
                                                 IrcCustomize 
                                                 {ircCustomizeServerColorTag = serverColorTag}})
                                nick seconds = do
  -- Format topic who time message.
  time <- getSecondsTimeStamp seconds "%H:%M:%S %Y/%m/%d"
  let adjustMsg = B.pack "*** "
      content   = B.concat [B.pack channel, ": topic set by ", nick, ", ", B.pack time]
  (msg, msgHeadLen) <- ircBufferIndentMessage buffer content adjustMsg
      
  -- Insert topic who time message.
  ircBufferReceiveMsg buffer msg msgHeadLen serverColorTag

-- | Receive join message.
ircBufferReceiveJoin :: IrcBuffer -> NickName -> UserName -> HostName -> IO ()
ircBufferReceiveJoin buffer@(IrcBuffer {ircBufferChannel        = channel
                                       ,ircBufferNick           = mynick
                                       ,ircBufferCustomize =
                                         IrcCustomize 
                                         {ircCustomizeJoinColorTag   = joinColorTag}
                                       }) 
                        nick user host = do
  nickname <- readTVarIO mynick
  -- Don't print self join message.
  when (B.unpack nick /= nickname) $ do
    -- Add nick to set.
    ircBufferAddNick buffer nick

    -- Format join message.
    let adjustMsg = B.pack "*** " 
        content   = B.concat [nick, " (", user, "@", host, ") has joined channel ", B.pack channel]
    (msg, msgHeadLen) <- ircBufferIndentMessage buffer content adjustMsg 

    -- Insert join message.
    ircBufferReceiveMsg buffer msg msgHeadLen joinColorTag
  
-- | Receive irc message.
ircBufferReceiveMsg :: IrcBuffer -> B.ByteString -> Int -> TextTag -> IO ()
ircBufferReceiveMsg IrcBuffer {ircBufferBuffer       = buffer
                              ,ircBufferInsertMark   = insertMark
                              ,ircBufferPromptMark   = promptMark
                              ,ircBufferEditableTag  = tag
                              ,ircBufferMessageTag   = messageTag
                              ,ircBufferTimeStamp    = stamp
                              ,ircBufferNickColorMap = nickColorMap
                              ,ircBufferSmilePixbufs = smilePixbufs
                              ,ircBufferCustomize =
                                IrcCustomize 
                                {ircCustomizeUrlColorTag  = urlColorTag}
                              } 
                    message msgHeadLen colorTag = do
  -- Make editable tag can edit temporary for insert irc message.
  set tag [textTagEditable := True]

  -- Update time stamp before insert message.
  insertIter<- textBufferGetIterAtMark buffer insertMark
  line      <- textIterGetLine insertIter
  timeStamp <- getTimeStamp "[%H:%M:%S]"
  color <- Gtk.get colorTag textTagForegroundGdk
  modifyTVarIO stamp $ \s -> 
      M.insert line (timeStamp, color) s

  -- Create new mark to save insert iter position.
  -- To apply color tag after insert message.
  saveMark <- textBufferCreateMark buffer Nothing insertIter True

  -- Filter smile string from message.
  (msg, smileIndex) <- smileMessage message smilePixbufs

  -- Insert irc message before insertMark.
  textBufferInsertByteString buffer insertIter msg

  -- Apply base color tag.
  saveIter   <- textBufferGetIterAtMark buffer saveMark
  insertIter <- textBufferGetIterAtMark buffer insertMark
  textBufferApplyTag buffer colorTag saveIter insertIter

  -- Apply nick color tag.
  colorMap <- readTVarIO nickColorMap
  unless (null $ map fst $ M.toList colorMap) $ 
    -- Regex "[^<> \t\n\'@,.:]+" works fine,
    -- don't build regex match all nickname, 
    -- too big regex will make matchAllText *failed*.
    forM_ (matchAllText (makeRegex (B.pack "[^<> \t\n\'@,.:]+") :: Regex) msg)
        $ \ x -> do
          let ((word, (matchOffset, matchLength)) : _) = elems x
          case findMinMatch colorMap (\ nickName _ -> nickName == word) of
            Just (_, colorTag) -> do
                  matchStartIter <- textIterCopy saveIter
                  matchEndIter   <- textIterCopy saveIter
                  textIterForwardChars matchStartIter matchOffset
                  textIterForwardChars matchEndIter (matchOffset + matchLength)
                  textBufferApplyTag buffer colorTag matchStartIter matchEndIter
            Nothing -> return ()

  -- Apply URL color tag.
  forM_ (matchAllText (makeRegex (B.pack "[^]\\ \t\n`\\[\"^]+") :: Regex) msg)
      $ \ x -> do
        let ((word, (matchOffset, matchLength)) : _) = elems x
        parseURI (UTF8.toString word) ?>= \ uri -> 
          when (isJust $ uriAuthority uri) $ do 
            matchStartIter <- textIterCopy saveIter
            matchEndIter   <- textIterCopy saveIter
            textIterForwardChars matchStartIter matchOffset
            textIterForwardChars matchEndIter (matchOffset + matchLength)
            textBufferApplyTag buffer urlColorTag matchStartIter matchEndIter

  -- Insert smile pixbuf.
  forM_ smileIndex $ \ (offset, pixbuf) -> do
    iter <- textBufferGetIterAtMark buffer saveMark
    textIterForwardChars iter offset
    textBufferInsertPixbuf buffer iter pixbuf

  -- We should apply message tag after all INSERT action.
  -- Otherwise will get rang bound.
  msgStartIter <- textBufferGetIterAtMark buffer saveMark
  msgEndIter   <- textBufferGetIterAtMark buffer insertMark
  textIterForwardChars msgStartIter msgHeadLen -- skip message head (e.g. nick name)
  textIterBackwardChar msgEndIter              -- skip '\n' at last
  textBufferApplyTag buffer messageTag msgStartIter msgEndIter

  -- Apply editable tag to new rang (from start to insertMark).
  startIter <- textBufferGetStartIter buffer
  endIter   <- textBufferGetIterAtMark buffer promptMark
  textBufferApplyTag buffer tag startIter endIter

  -- Make irc message read-only again.
  set tag [textTagEditable := False]

-- | Add nick to nick set.
ircBufferAddNick :: IrcBuffer -> NickName -> IO ()
ircBufferAddNick buffer@(IrcBuffer {ircBufferNickSet = nickSet})
                 nick = do
    -- Add nick to list.
    set <- readTVarIO nickSet
    case maybeFindMin set (== nick) of
      Just _  -> 
          -- Debug.
          -- putStrLn $ "Nick " ++ B.unpack nick ++ " already in nick set."
          return ()
      Nothing -> 
        -- Debug.
        -- putStrLn $ "Nick " ++ B.unpack nick ++ " added to nick set."
        modifyTVarIO nickSet $ \ set -> 
            S.insert nick set

    -- Update nick color.
    ircBufferUpdateNickColor buffer nick

-- | Update nick color to  map.
ircBufferUpdateNickColor :: IrcBuffer -> NickName -> IO ()
ircBufferUpdateNickColor (IrcBuffer {ircBufferBuffer       = buffer
                                    ,ircBufferNickColorMap = nickColorMap})
                         nick = do
  colorMap <- readTVarIO nickColorMap
  case findMinMatch colorMap (\ nickName _ -> nickName == nick) of
    Just _  -> 
      -- Debug
      -- putStrLn $ "Have build color for nick : " ++ B.unpack nick
      return ()
    Nothing -> do
      -- Get color nick.
      let color = nickColorToColor $ nickColor nick

      -- Create color tag.
      textTag <- textTagNew Nothing
      set textTag [textTagForegroundGdk := color
                  ,textTagWeight        := fromEnum WeightBold]

      -- Add color tag to table.
      tagTable <- textBufferGetTagTable buffer
      textTagTableAdd tagTable textTag

      -- Update color map.
      modifyTVarIO nickColorMap $ \map ->
          M.insert nick textTag map

-- | Add color tag.
ircBufferAddColorTag :: TextBufferClass buffer => buffer -> TextTag -> IO ()
ircBufferAddColorTag buffer colorTag = do
  tagTable <- textBufferGetTagTable buffer
  textTagTableAdd tagTable colorTag
      
-- | Insert prompt string.
ircBufferInsertPromptStr :: TextBufferClass buffer => buffer -> IO ()
ircBufferInsertPromptStr buffer = 
    textBufferSetText buffer promptStr

-- | Create insert mark.
ircBufferCreateInsertMark :: TextBufferClass buffer => buffer -> IO TextMark
ircBufferCreateInsertMark buffer = do
  textIter <- textBufferGetStartIter buffer
  textBufferCreateMark buffer Nothing textIter False

-- | Create prompt mark.
ircBufferCreatePromptMark :: TextBufferClass buffer => buffer -> IO TextMark
ircBufferCreatePromptMark buffer = do
  textIter <- textBufferGetIterAtOffset buffer (length promptStr)
  textBufferCreateMark buffer Nothing textIter True
          
-- | Indent message.
ircBufferIndentMessage :: IrcBuffer -> B.ByteString -> B.ByteString -> IO (B.ByteString, Int)
ircBufferIndentMessage buffer content adjustMsg = do
  wrapColumn <- readTVarIO (ircCustomizeWrapColumn $ ircBufferCustomize buffer)                          
  let indentMsg = B.replicate (B.length adjustMsg) ' '
      msgLines  = map B.unwords $ wrapLine (wrapColumn - B.length adjustMsg) (splitWords content)
      concatMsg []     = B.concat [adjustMsg, "\n"]
      concatMsg [x]    = B.concat [adjustMsg, x, "\n"]
      concatMsg (x:xs) = B.concat [B.concat [adjustMsg, x, "\n"]
                                  ,B.concat $ map (\ xx -> B.concat [indentMsg, xx, "\n"]) xs]

  return (concatMsg msgLines, B.length adjustMsg)

-- | Irc customize new.
ircCustomizeNew :: IO CustomizeWrap
ircCustomizeNew =
  fmap CustomizeWrap $ 
       IrcCustomize <$> newTVarIO selfMsgColor
                    <*> newTVarIO serverMsgColor
                    <*> newTVarIO otherMsgColor
                    <*> newTVarIO joinMsgColor
                    <*> newTVarIO quitMsgColor
                    <*> newTVarIO partMsgColor
                    <*> newTVarIO actionMsgColor
                    <*> newTVarIO urlColor
                    <*> newTVarIO wrapColumn
                    <*> ircColorTagNew serverMsgColor
                    <*> ircColorTagNew selfMsgColor
                    <*> ircColorTagNew otherMsgColor
                    <*> ircColorTagNew joinMsgColor
                    <*> ircColorTagNew quitMsgColor
                    <*> ircColorTagNew partMsgColor
                    <*> ircColorTagNew actionMsgColor
                    <*> ircColorTagNew urlColor
                    <*> newTVarIO timeStampPosition
                    <*> newTVarIO sourceLanguage
                    <*> newTVarIO targetLanguage
                    <*> newTVarIO showLineNumber
                    <*> newTVarIO defaultNick
                    <*> newTVarIO defaultServer
                    <*> newTVarIO defaultPort
                    <*> newTVarIO defaultChannel

-- | New color tag.
ircColorTagNew :: Color -> IO TextTag
ircColorTagNew color = do
  colorTag <- textTagNew Nothing
  set colorTag [textTagForegroundGdk := color]
  return colorTag

-- | Write state.
ircBufferWriteState :: IrcBuffer -> FilePath -> IO ()
ircBufferWriteState buffer path = do
  state <- readTVarIO $ ircBufferState buffer
  writeConfigPath path state

-- | Read state.
ircBufferReadState :: IrcBuffer -> FilePath -> IO ()  
ircBufferReadState buffer path = do
  state <- readConfigPath path ircInitState
  writeTVarIO (ircBufferState buffer) state
  
$(derive makeBinary ''IrcState)