-- |
-- Module      :  System.Dzen.Internal
-- Copyright   :  (c) 2009 Felipe A. Lessa
-- License     :  GPL 3 (see the LICENSE file in the distribution)
--
-- Maintainer  :  felipe.lessa@gmail.com
-- Stability   :  experimental
-- Portability :  semi-portable (MPTC and type families)
--
-- Internal data types and functions that are not exported
-- to the outside world.

module System.Dzen.Internal
    (-- * State
     DSt(..)
    ,DColour

     -- * DString
    ,DString(..)
    ,rawStr
    ,toString
    ,size
    ,mkCmd

     -- * Printer
    ,Printer(..)
    ,apply
    ,Transform(..)
    ) where


import Control.Arrow
import Control.Monad
import Data.Colour
import Data.String
import Data.Monoid


-- | The internal state we maintain. Currently it only contains
--   the foreground and the background colours and if we are
--   ignoring the background or not.
--
--   This state is passed around like a @Reader@ monad, each
--   function receives it and does whatever it want, and not
--   like a @State@ monad!
data DSt = S {sFg :: !(Maybe DColour)
             ,sBg :: !(Maybe DColour)
             ,sIgnoreBg :: !Bool}

-- | Our colours.
type DColour = Colour Double

-- | Empty state.
emptyState :: DSt
emptyState = S Nothing Nothing True





-- | A @DString@ is used for constant string output, see 'str'.
--   The @D@ on @DString@ stands for @dzen@, as these strings
--   may change depending on the state (and that's why you
--   shouldn't rely on 'Show', as it just uses an empty state)
newtype DString = DS {unDS :: DSt -> (String -> String, Maybe Int)}
-- A differencial list of chars (i.e. ShowS) and the number of chars.
--
-- Note that we use the @DStrings@ by themselves (i.e. concatenating
-- with @Printers@) and for output of the @Printers@, but state is
-- relevant only on the former. The @DString@s returned by Printers
-- always get passed the @emptyState@. Of course it would be better to
-- create two distinct data types, but we'll stick to this semantic
-- hole for now.

instance IsString DString where
    fromString = DS . const . escape 0

instance Show DString where
    show (DS ds) = concat ["<with empty state: ",
                           show ((fst $ ds emptyState) ""), ">"]

instance Monoid DString where
    mempty = DS $ const (id, Just 0)
    mappend (DS ds1) (DS ds2) = DS $ \st -> ds1 st # ds2 st
                        -- Note how we duplicate 'st' above
        where (s1,n1) # (s2,n2) = (s1 . s2, liftM2 (+) n1 n2)

escape :: Int -> String -> (String -> String, Maybe Int)
escape n s | n `seq` s `seq` False = error "escape: never here"
escape n ('^':xs) = first (t.t) $ escape (n+1) xs where t = (.) ('^':)
escape n ( x :xs) = first x'    $ escape (n+1) xs where x' = (.) (x:)
escape n []       = (id, Just n)


-- | Converts a @String@ into a @DString@ without escaping anything.
--   You /really/ don't need to use this, trust me!
rawStr :: String -> DString
rawStr str = DS $ const ((str ++), Just $ length str)

-- | Converts a @DString@ back into a @String@. Note that
--   @(toString . rawStr)@ is not @id@, otherwise @toString@
--   would not work in some cases.
--   Probably you don't need to use this, unless you want
--   something like a static bar and nothing else.
toString :: DString -> String
toString = ("^ib(1)" ++) . ($ "") . fst . ($ emptyState) . unDS

-- | Tries to get the number of characters of the @DString@.
--   May return @Nothing@ when there are graphical objects.
--   Probably you don't need to use this function.
size :: DString -> Maybe Int
size = snd . ($ emptyState) . unDS
--   We apply a new empty state but that shouldn't be a problem
--   because currently all functions that depend on the state
--   do not change the size.

-- | @mkCmd graph cmd arg@ creates a command string like
--   @\"^cmd(arg)\"@. If @graph@ is @False@ then we give length zero
--   to the resulting @DString@, otherwise we don't give a length
--   (which propagates for strings concatenated to this). You should
--   use @False@ whenever possible.
mkCmd :: Bool -> String -> String -> DString
mkCmd graph cmd arg = DS $ const (str, len)
    where str = ('^':).(cmd++).('(':).(arg++).(')':)
          len = if graph then Nothing else Just 0


-- | A printer is used when the output depends on an input, so a
--   @Printer a@ generates a 'DString' based on some input of
--   type @a@ (and possibly updates some internal state).
newtype Printer a = P {unP :: DSt -> a -> (DString, Printer a)}
-- We don't use a Reader just because we already have
-- to do a lot of pumbling ourselves anyway.

-- | Apply a printer to an appropriate input, returning
--   the output string and the new printer.
apply :: Printer a -> a -> (String, Printer a)
apply p i = first toString . ($ i) . ($ emptyState) . unP $ p
-- We have apply here in Internal because it uses 'emptyState',
-- which we don't want to export because its defaults are not
-- the same as dzen's defaults (we use "ib(1)" by default).


-- | @Transform@ is a specialization of @Functor@ for @DString@s.
--   This class is used for functions that may receive @DString@
--   or @Printer a@ as an argument because they operate only
--   on their outputs and internal states (and not on the inputs).
--
--   So, whenever you see a function of type
--
--   > func :: Transform a => Blah -> Bleh -> a -> a
--
--   it means that @func@ can be used in two ways:
--
--   > func :: Blah -> Bleh -> DString -> DString
--   > func :: Blah -> Bleh -> Printer a -> Printer a  -- Printer of any input!
--
--   Try to have this in mind when reading the types.
--
--   Note: There is also a non-exported @transformSt@ for
--   transforming the state in this class, otherwise it would
--   be meaningless to use a class only for @transform@ (it
--   would be better to make @liftT :: (DString -> DString)
--   -> (Printer a -> Printer a)@).
class Transform a where
    -- | This function is 'id' on @DString@ and
    --   modifies the output of a @Printer a@.
    transform :: (DString -> DString) -> (a -> a)
    transform f = transformSt (\st -> (st, f))

    transformSt :: (DSt -> (DSt, DString -> DString)) -> (a -> a)

instance Transform DString where
    transform = id
    transformSt f ds = DS $ \st -> let (st', dsT) = f st
                                   in unDS (dsT ds) st'

instance Transform (Printer a) where
    transform f = P . (((f *** transform f) .) .) . unP
    transformSt f (P p) = P $ \st i -> let (st', dsT) = f st
                                           (ds, p') = p st' i
                                       in (dsT ds, transformSt f p')