-- | Module containing some specialized functions to deal with tags.
--   This Module follows certain conventions. Stick with them.
--
--   More concrete: all functions in this module assume that the tags are
--   located in the @tags@ field, and separated by commas. An example file
--   @foo.markdown@ could look like:
--
--   > ---
--   > author: Philip K. Dick
--   > title: Do androids dream of electric sheep?
--   > tags: future, science fiction, humanoid
--   > ---
--   > The novel is set in a post-apocalyptic near future, where the Earth and
--   > its populations have been damaged greatly by Nuclear...
--
--   All the following functions would work with such a format. In addition to
--   tags, Hakyll also supports categories. The convention when using categories
--   is to place pages in subdirectories.
--
--   An example, the page @posts\/coding\/2010-01-28-hakyll-categories.markdown@
--   Tags or categories are read using the @readTagMap@ and @readCategoryMap@
--   functions. Because categories are implemented using tags - categories can
--   be seen as tags, with the restriction that a page can only have one
--   category - all functions for tags also work with categories.
--
--   When reading a @TagMap@ (which is also used for category maps) using the
--   @readTagMap@ or @readCategoryMap@ function, you also have to give a unique
--   identifier to it. This identifier is simply for caching reasons, so Hakyll
--   can tell different maps apart; it has no other use.
--
module Text.Hakyll.Tags
    ( TagMap
    , readTagMap
    , readCategoryMap
    , withTagMap
    , renderTagCloud
    , renderTagLinks
    ) where

import qualified Data.Map as M
import Data.List (intercalate)
import Data.Maybe (fromMaybe, maybeToList)
import Control.Arrow (second, (>>>))
import Control.Applicative ((<$>))
import System.FilePath

import Text.Blaze.Renderer.String (renderHtml)
import Text.Blaze.Html5 ((!), string, stringValue)
import qualified Text.Blaze.Html5 as H
import qualified Text.Blaze.Html5.Attributes as A

import Text.Hakyll.Context (Context (..))
import Text.Hakyll.ContextManipulations (changeValue)
import Text.Hakyll.CreateContext (createPage)
import Text.Hakyll.HakyllMonad (Hakyll)
import Text.Hakyll.Regex
import Text.Hakyll.HakyllAction
import Text.Hakyll.Util
import Text.Hakyll.Internal.Cache

-- | Type for a tag map.
--
--   This is a map associating tags or categories to the appropriate pages
--   using that tag or category. In the case of categories, each path will only
--   appear under one category - this is not the case with tags.
type TagMap = M.Map String [HakyllAction () Context]

-- | Read a tag map. This is a internally used function that can be used for
--   tags as well as for categories.
readMap :: (Context -> [String]) -- ^ Function to get tags from a context.
        -> String -- ^ Unique identifier for the tagmap.
        -> [FilePath]
        -> HakyllAction () TagMap
readMap getTagsFunction identifier paths = HakyllAction
    { actionDependencies = paths
    , actionUrl          = Right id
    , actionFunction     = actionFunction'
    } 
  where
    fileName = "tagmaps" </> identifier

    actionFunction' _ = do
        isCacheMoreRecent' <- isCacheMoreRecent fileName paths
        assocMap <- if isCacheMoreRecent'
                        then M.fromAscList <$> getFromCache fileName
                        else do assocMap' <- readTagMap'
                                storeInCache (M.toAscList assocMap') fileName
                                return assocMap'
        return $ M.map (map createPage) assocMap

    -- TODO: preserve order
    readTagMap' :: Hakyll (M.Map String [FilePath])
    readTagMap' = do
        pairs' <- concat <$> mapM pairs paths
        return $ M.fromListWith (flip (++)) pairs'

    -- | Read a page, and return an association list where every tag is
    -- associated with some paths. Of course, this will always be just one
    -- @FilePath@ here.
    pairs :: FilePath -> Hakyll [(String, [FilePath])]
    pairs path = do
        context <- runHakyllAction $ createPage path
        let tags = getTagsFunction context
        return $ map (\tag -> (tag, [path])) tags

-- | Read a @TagMap@, using the @tags@ metadata field.
readTagMap :: String     -- ^ Unique identifier for the map.
           -> [FilePath] -- ^ Paths to get tags from.
           -> HakyllAction () TagMap
readTagMap = readMap getTagsFunction
  where
    getTagsFunction = map trim . splitRegex ","
                    . fromMaybe [] . M.lookup "tags" . unContext

-- | Read a @TagMap@, using the subdirectories the pages are placed in.
readCategoryMap :: String     -- ^ Unique identifier for the map.
                -> [FilePath] -- ^ Paths to get tags from.
                -> HakyllAction () TagMap
readCategoryMap = readMap $ maybeToList . M.lookup "category" . unContext

-- | Perform a @Hakyll@ action on every item in the tag
--
withTagMap :: HakyllAction () TagMap
           -> (String -> [HakyllAction () Context] -> Hakyll ())
           -> Hakyll ()
withTagMap tagMap function = runHakyllAction (tagMap >>> action)
  where
    action = createHakyllAction (mapM_ (uncurry function) . M.toList)

-- | Render a tag cloud.
renderTagCloud :: (String -> String) -- ^ Function to produce an url for a tag.
               -> Float              -- ^ Smallest font size, in percent.
               -> Float              -- ^ Biggest font size, in percent.
               -> HakyllAction TagMap String
renderTagCloud urlFunction minSize maxSize = createHakyllAction renderTagCloud'
  where
    renderTagCloud' tagMap =
        return $ intercalate " " $ map (renderTag tagMap) (tagCount tagMap)

    renderTag tagMap (tag, count) = renderHtml $
        H.a ! A.style (stringValue $ "font-size: " ++ sizeTag tagMap count)
            ! A.href (stringValue $ urlFunction tag)
            $ string tag
        
    sizeTag tagMap count = show (size' :: Int) ++ "%"
      where
        size' = floor $ minSize + relative tagMap count * (maxSize - minSize)

    minCount = minimum . map snd . tagCount
    maxCount = maximum . map snd . tagCount
    relative tagMap count = (count - minCount tagMap) /
                            (maxCount tagMap - minCount tagMap)

    tagCount = map (second $ fromIntegral . length) . M.toList

-- | Render all tags to links.
--   
--   On your site, it is nice if you can display the tags on a page, but
--   naturally, most people would expect these are clickable.
--
--   So, this function takes a function to produce an url for a given tag, and
--   applies it on all tags.
--
--   Note that it is your own responsibility to ensure a page with such an url
--   exists.
renderTagLinks :: (String -> String) -- ^ Function to produce an url for a tag.
               -> HakyllAction Context Context
renderTagLinks urlFunction = changeValue "tags" renderTagLinks'
  where
    renderTagLinks' = intercalate ", "
                    . map ((\t -> link t $ urlFunction t) . trim)
                    . splitRegex ","