{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE FlexibleContexts  #-}
{-# LANGUAGE PatternGuards     #-}
module Text.CSL.Proc where
import Prelude
import           Control.Applicative    ((<|>))
import           Control.Arrow          (second, (&&&), (>>>))
import           Control.Monad.State    (execState, modify)
import           Data.Aeson
import           Data.Char              (isDigit, isLetter, toLower)
import           Data.List
import           Data.Maybe             (mapMaybe)
import           Data.Ord               (comparing)
import           Text.CSL.Eval
import           Text.CSL.Proc.Collapse
import           Text.CSL.Proc.Disamb
import           Text.CSL.Reference
import           Text.CSL.Style
import           Text.CSL.Util          (proc, proc', query, tr', uncamelize)
import           Text.Pandoc.Definition (Block (Para),
                                         Inline (Note, Space, Str))
data ProcOpts
    = ProcOpts
      { bibOpts       :: BibOpts
      , linkCitations :: Bool
      }
    deriving ( Show, Read, Eq )
data BibOpts
    = Select  [(String, String)] [(String, String)]
    | Include [(String, String)] [(String, String)]
    | Exclude [(String, String)] [(String, String)]
    deriving ( Show, Read, Eq )
newtype FieldVal = FieldVal{
                      unFieldVal :: (String, String)
                    } deriving Show
instance FromJSON FieldVal where
  parseJSON (Object v) = do
    x <- v .: "field"
    y <- v .: "value"
    return $ FieldVal (x,y)
  parseJSON _ = fail "Could not parse FieldVal"
instance FromJSON BibOpts where
  parseJSON (Object v) = do
    quash <- v .:? "quash".!= []
    let quash' = map unFieldVal quash
    (v .: "select" >>= \x -> return $ Select (map unFieldVal x) quash')
     <|>
     (v .: "include" >>= \x -> return $ Include (map unFieldVal x) quash')
     <|>
     (v .: "exclude" >>= \x -> return $ Exclude (map unFieldVal x) quash')
     <|>
     return (Select [] quash')
  parseJSON _ = return $ Select [] []
procOpts :: ProcOpts
procOpts = ProcOpts
      { bibOpts = Select [] []
      , linkCitations = False
      }
processCitations :: ProcOpts -> Style -> [Reference] -> Citations -> [Formatted]
processCitations ops s rs
    = citations . citeproc ops s rs
processBibliography :: ProcOpts -> Style -> [Reference] -> [Formatted]
processBibliography ops s rs
    = bibliography $ citeproc ops s rs [map (\r -> emptyCite { citeId = unLiteral $ refId r}) rs]
citeproc :: ProcOpts -> Style -> [Reference] -> Citations -> BiblioData
citeproc ops s rs cs
    = BD citsOutput biblioOutput $ map (unLiteral . refId) biblioRefs
    where
      
      
      biblioRefs   = procRefs s . mapMaybe (getReference rs) .
                     nubBy (\a b -> citeId a == citeId b) . concat $ cs
      biblioOutput = if "disambiguate-add-year-suffix" `elem` getCitDisambOptions s
                     then map (formatOutputList .
                               proc (updateYearSuffixes yearS) . map addYearSuffix) $
                          procBiblio (bibOpts ops) s biblioRefs
                     else map formatOutputList $
                          tr' "citeproc:after procBiblio" $
                          procBiblio (bibOpts ops) s biblioRefs
      citsAndRefs  = processCites biblioRefs cs
      (yearS,citG) = disambCitations s biblioRefs cs $ map (procGroup s) citsAndRefs
      citsOutput   = map (formatCitLayout s) .
                     tr' "citeproc:collapsed" .
                     collapseCitGroups s .
                     (if linkCitations ops && styleClass s == "in-text"
                         then proc addLink
                         else id) .
                     tr' "citeproc:citG" $
                     citG
      addLink :: (Cite, Output) -> (Cite, Output)
      addLink (cit, outp) = (cit, proc (addLink' (citeId cit)) outp)
      addLink' citeid (OYear y _ f) =
         OYear y citeid f{hyperlink = "#ref-" ++ citeid}
      addLink' citeid (OYearSuf y _ d f) =
         OYearSuf y citeid d f{hyperlink = "#ref-" ++ citeid}
      addLink' citeid (OCitNum n f) =
         OCitNum n f{hyperlink = "#ref-" ++ citeid}
      addLink' citeid (OCitLabel l f) =
         OCitLabel l f{hyperlink = "#ref-" ++ citeid}
      addLink' citeid (Output xs@(OStr _ _: _) f) =
         Output xs f{hyperlink = "#ref-" ++ citeid}
      addLink' _ x = x
procRefs :: Style -> [Reference] -> [Reference]
procRefs Style {biblio = mb, csMacros = ms , styleLocale = l, styleAbbrevs = as, csOptions = opts} rs
    = maybe (setCNum rs) process mb
    where
      opts'   b = mergeOptions (bibOptions b) opts
      setCNum   = map (\(x,y) -> x { citationNumber = fromIntegral y }) . flip zip ([1..] :: [Int])
      sort_   b = evalSorting (EvalSorting emptyCite {citePosition = "first"}) l ms (opts' b) (bibSort b) as
      process b = setCNum . sortItems . map (id &&& sort_ b . Just) $ rs
sortItems :: Show a => [(a,[Sorting])] -> [a]
sortItems [] = []
sortItems l
    = case head . concatMap (map snd) $ result of
        [] -> concatMap (map fst) result
        _  -> if any ((<) 1 . length) result
              then concatMap sortItems result
              else concatMap (map fst) result
    where
      result = process l
      process = sortBy (comparing $ take 1 . snd)                 >>>
                groupBy (\a b -> take 1 (snd a) == take 1 (snd b)) >>>
                map (map $ second (drop 1))
procBiblio :: BibOpts -> Style -> [Reference] -> [[Output]]
procBiblio bos Style {biblio = mb, csMacros = ms , styleLocale = l,
                       styleAbbrevs = as, csOptions = opts} rs
    = map addSpaceAfterCitNum $ maybe [] process mb
    where
      
      addSpaceAfterCitNum [Output (OCitNum n f : xs) f']
        | secondFieldAlign == Just "flush"  =
            [Output (OCitNum n f : OSpace : xs) f']
        | secondFieldAlign == Just "margin" =
            [Output (OCitNum n f : OSpace : xs) f']
        | otherwise = [Output (OCitNum n f : xs) f']
      addSpaceAfterCitNum xs = xs
      secondFieldAlign = lookup "second-field-align" $ maybe [] bibOptions mb
      process :: Bibliography -> [[Output]]
      process b   = map (formatBiblioLayout (layFormat $ bibLayout b) (layDelim $ bibLayout b)) $ render b
      render :: Bibliography -> [[Output]]
      render  b   = subsequentAuthorSubstitute b . map (evalBib b) . filterRefs bos $ rs
      evalBib :: Bibliography -> Reference -> [Output]
      evalBib b = evalLayout (bibLayout b) (EvalBiblio emptyCite {citePosition = "first"}) False l ms (mergeOptions (bibOptions b) opts) as . Just
subsequentAuthorSubstitute :: Bibliography -> [[Output]] -> [[Output]]
subsequentAuthorSubstitute b = if null subAuthStr then id else chkCreator
    where
      subAuthStr  = getOptionVal "subsequent-author-substitute"      (bibOptions b)
      subAuthRule = getOptionVal "subsequent-author-substitute-rule" (bibOptions b)
      queryContrib = proc' rmLabel . query contribsQ
      getContrib = if null subAuthStr
                   then const []
                   else case subAuthRule of
                          "partial-first" -> take 1  . query namesQ  . queryContrib
                          "partial-each"  ->          query namesQ  . queryContrib
                          _               ->                          queryContrib
      getPartialEach x xs = concat . take 1 . map fst .
                            sortBy (flip (comparing $ length . snd)) . filter ((<) 0 . length . snd) .
                            zip xs . map (takeWhile id .  zipWith (==) x) $ xs
      chkCreator = if subAuthRule == "partial-each" then chPartialEach [] else chkCr []
      chkCr _ []     = []
      chkCr a (x:xs) = let contribs = getContrib x in
                       if  contribs `elem` a
                       then substituteAuth []
                            x : chkCr             a  xs
                       else x : chkCr (contribs : a) xs
      chPartialEach _ [] = []
      chPartialEach a (x:xs) = let contribs = getContrib x
                                   partial  = getPartialEach contribs a in
                               if not $ null partial
                               then substituteAuth partial x :
                                    if length partial < length contribs
                                    then chPartialEach (contribs : a) xs
                                    else chPartialEach             a  xs
                               else x  : chPartialEach (contribs : a) xs
      substituteAuth a = if subAuthRule == "complete-each"
                         then proc chNamas else proc (updateContribs a)
      updateContribs a o@(OContrib i r y ds os)
          = if r == "author" || r == "authorsub" then OContrib i r upCont ds os else o
          where
            upCont = case subAuthRule of
                       "partial-first" -> rmFirstName      y
                       "partial-each"  -> rmSelectedName a y
                       _               -> OStr subAuthStr emptyFormatting : proc rmNames y
      updateContribs _ o = o
      contribsQ o
          | OContrib _ r c _ _ <- o = if r == "author" || r == "authorsub" then c else []
          | otherwise               = []
      namesQ o
          | OName {} <- o = [o]
          | otherwise     = []
      rmSelectedName _ [] = []
      rmSelectedName a (o:os)
          | OName {} <- o = (if o `elem` a then OStr subAuthStr emptyFormatting else o) : rmSelectedName a os
          | otherwise     = o : rmSelectedName a os
      rmFirstName [] = []
      rmFirstName (o:os)
          | OName {} <- o = OStr subAuthStr emptyFormatting : os
          | otherwise     = o : rmFirstName os
      chNamas o
          | OName s _ os f <- o = OName s [OStr subAuthStr emptyFormatting] os f
          | otherwise           = o
      rmNames o
          | OName {} <- o = ONull
          | OStr  {} <- o = ONull
          | ODel  {} <- o = ONull
          | otherwise     = o
      rmLabel [] = []
      rmLabel (o:os)
          | OLabel {} <- o =     rmLabel os
          | otherwise      = o : rmLabel os
filterRefs :: BibOpts -> [Reference] -> [Reference]
filterRefs bos refs
    | Select  s q <- bos = filter (select  s) . filter (quash q) $ refs
    | Include i q <- bos = filter (include i) . filter (quash q) $ refs
    | Exclude e q <- bos = filter (exclude e) . filter (quash q) $ refs
    | otherwise          = refs
    where
      quash  [] _ = True
      quash   q r = not $ all (lookup_ r) q
      select  s r =       all (lookup_ r) s
      include i r =       any (lookup_ r) i
      exclude e r =       all (not . lookup_ r) e
      lookup_ r (f, v) = case f of
                          "type"       -> look "ref-type"
                          "id"         -> look "ref-id"
                          "categories" -> look "categories"
                          x            -> look x
          where
            look s = case lookup s (mkRefMap (Just r)) of
                       Just x | Just v' <- (fromValue x :: Maybe RefType  ) -> v == uncamelize (show v')
                              | Just v' <- (fromValue x :: Maybe String   ) -> v  == v'
                              | Just v' <- (fromValue x :: Maybe [String] ) -> v `elem` v'
                              | Just v' <- (fromValue x :: Maybe [Agent]  ) -> null v && null v' || v == show v'
                              | Just v' <- (fromValue x :: Maybe [RefDate]) -> null v && null v' || v == show v'
                       _                                                    -> False
procGroup :: Style -> [(Cite, Maybe Reference)] -> CitationGroup
procGroup Style {citation = ct, csMacros = ms , styleLocale = l,
                  styleAbbrevs = as, csOptions = opts} cr
    = CG authIn (layFormat $ citLayout ct) (layDelim $ citLayout ct) (authIn ++ co)
    where
      (co, authIn) = case cr of
                       (c:_) -> if authorInText (fst c)
                                then (filter (eqCites (/=) c) result,
                                      take 1 .  filter (eqCites (==) c) $ result)
                                else (result, [])
                       _     -> (result, [])
      eqCites eq c = fst >>> citeId &&& citeHash >>> eq (citeId &&& citeHash $ fst c)
      opts'        = mergeOptions (citOptions ct) opts
      format (c,r) = (c,  evalLayout (citLayout ct) (EvalCite c) False l ms opts' as r)
      sort_  (c,r) = evalSorting (EvalSorting c) l ms opts' (citSort ct) as r
      process      = map (second (flip Output emptyFormatting) . format &&& sort_)
      result       = concatMap sortItems $ toChunks $ process cr
      
      
      
      
      
      toChunks  xs = reverse $ execState (toChunks' xs) []
      toChunks' xs = do
        case break hasPrefix xs of
                ([], [])   -> return ()
                ([], y:ys) -> modify ([y]:) >> toChunks' ys
                (zs, ys)   -> modify (zs:) >> toChunks' ys
      hasPrefix ((c,_),_) = citePrefix c /= mempty
formatBiblioLayout :: Formatting -> Delimiter -> [Output] -> [Output]
formatBiblioLayout  f d = appendOutput f . addDelim d
formatCitLayout :: Style -> CitationGroup -> Formatted
formatCitLayout s (CG co f d cs)
    | [a] <- co = combine (formatAuth a)
                  (formatCits $
                   (fst >>> citeId &&& citeHash >>> setAsSupAu $ a) cs)
    | otherwise = formatCits cs
    where
      isNote    = styleClass s == "note"
      toNote (Formatted []) = mempty
      toNote (Formatted xs) = Formatted [Note [Para xs]]
      combine (Formatted []) ys = ys
      combine xs ys =
        case ys of
             Formatted []           -> xs
             Formatted (Note _ : _) -> xs <> ys
             Formatted (Str [c]:_)  | c `elem` (", ;:" :: String) -> xs <> ys
             _                      -> xs <> Formatted [Space] <> ys
      formatAuth   = formatOutput . localMod
      formatCits   = (if isNote then toNote else id) .
                     formatOutputList . appendOutput formatting . addAffixes f .
                     addDelim d .
                     map (fst &&& localMod >>> uncurry addCiteAffixes)
      formatting   = f{ prefix = [], suffix = [],
                        verticalAlign = if isAuthorInText cs
                                           then ""
                                           else verticalAlign f }
      isAuthorInText []        = False
      isAuthorInText ((c,_):_) = authorInText c
      localMod     = uncurry $ localModifiers s (not $ null co)
      setAsSupAu h = map $ \(c,o) -> if (citeId c, citeHash c) == h
                                     then (c { authorInText   = False
                                             , suppressAuthor = True }, o)
                                     else (c, o)
addAffixes :: Formatting -> [Output] -> [Output]
addAffixes f os
    | []      <- os            = []
    | [ONull] <- os            = []
    | [Output [ONull] _] <- os = []
    | otherwise                = pref ++ suff
    where
      pref = if not (null (prefix f))
             then OStr (prefix f) emptyFormatting : os
             else os
      suff = case suffix f of
                  []     -> []
                  (c:cs)
                    | isLetter c || isDigit c || c == '(' || c == '[' ->
                         [OSpace, OStr (c:cs) emptyFormatting]
                    | otherwise -> [OStr (c:cs) emptyFormatting]
localModifiers :: Style -> Bool -> Cite -> Output -> Output
localModifiers s b c
    | authorInText   c = check . return . contribOnly s
    | suppressAuthor c = check . rmContrib . return
    | otherwise        = id
    where
      isPunct' [] = False
      isPunct' xs = all (`elem` (".,;:!? " :: String)) xs
      check o = case cleanOutput o of
                  [] -> ONull
                  x  -> case trim' x of
                          [] -> ONull
                          x' -> Output x' emptyFormatting
      hasOutput o
          | Output [] _ <- o = [False]
          | ODel      _ <- o = [False]
          | OSpace      <- o = [False]
          | ONull       <- o = [False]
          | otherwise        = [True]
      trim' [] = []
      trim' (o:os)
          | Output ot f <- o, p <- prefix f,  p /= []
          , isPunct' p        = trim' $ Output ot f { prefix = []} : os
          | Output ot f <- o  = if or (query hasOutput ot)
                                then Output (trim' ot) f : os
                                else Output       ot  f : trim' os
          | ODel _      <- o  = trim' os
          | OSpace      <- o  = trim' os
          | OStr    x f <- o  = OStr x (if isPunct' (prefix f)
                                        then f { prefix = []} else f) : os
          | otherwise         = o:os
      rmCitNum o
          | OCitNum {} <- o = ONull
          | otherwise       = o
      rmContrib [] = []
      rmContrib o
          | b, isNumStyle o = proc rmCitNum o
          | otherwise       = rmContrib' o
      rmContrib' [] = []
      rmContrib' (o:os)
          | Output ot f <- o = Output (rmContrib' ot) f : rmContrib' os
          | ODel _ <- o
          , OContrib _ "author"
                     _ _ _ : xs <- os = rmContrib' xs
          | ODel _ <- o
          , OContrib _ "authorsub"
                     _ _ _ : xs <- os = rmContrib' xs
          | OContrib _ "author" _ _ _ <- o
          , ODel _ : xs <- os =     rmContrib' xs
          | OContrib _ "authorsub" _ _ _ <- o
          , ODel _ : xs <- os =     rmContrib' xs
          | OContrib _ "author"
                  _ _ _ <- o =     rmContrib' os
          | OContrib _ "authorsub"
                  _ _ _ <- o =     rmContrib' os
          | OStr x _ <- o
          , "ibid" <- filter (/= '.') (map toLower x) = rmContrib' os
          | otherwise        = o : rmContrib' os
contribOnly :: Style -> Output -> Output
contribOnly s o
    | isNumStyle [o]
    , OCitNum n f <- o = Output [ OCitNum n f{
                                       verticalAlign = "",
                                       prefix = "",
                                       suffix = "" } ] emptyFormatting
    | OContrib _ "author"
            _ _ _ <- o = o
    | OContrib _ "authorsub"
            _ _ _ <- o = o
    | Output ot f <- o = Output (cleanOutput $ map (contribOnly s) ot)
                         f{ verticalAlign = "",
                            prefix = "",
                            suffix = "" }
    | OStr    x _ <- o
    , "ibid" <- filter (/= '.')
       (map toLower x) = o
    | otherwise        = ONull