{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE FlexibleContexts    #-}
{-# LANGUAGE PatternGuards       #-}
{-# LANGUAGE LambdaCase          #-}
{-# LANGUAGE ScopedTypeVariables #-}
module Text.CSL.Eval
    ( evalLayout
    , evalSorting
    , module Text.CSL.Eval.Common
    , module Text.CSL.Eval.Output
    ) where
import Prelude
import           Control.Arrow
import qualified Control.Exception      as E
import           Control.Monad.State
import           Data.Char              (isDigit, isLetter, toLower)
import           Data.Maybe
import           Data.Monoid            (Any (..))
import           Data.String            (fromString)
import qualified Data.Text              as T
import           Text.Pandoc.Definition (Inline (Link, Span, Str), nullAttr)
import           Text.Pandoc.Shared     (stringify, escapeURI)
import           Text.Pandoc.Walk       (walk)
import           Text.CSL.Eval.Common
import           Text.CSL.Eval.Date
import           Text.CSL.Eval.Names
import           Text.CSL.Eval.Output
import           Text.CSL.Exception
import           Text.CSL.Output.Plain
import           Text.CSL.Reference
import           Text.CSL.Style         hiding (Any)
import           Text.CSL.Util          (orIfNull, isRange, last', proc,
                                         proc', query, readNum, safeRead)
evalLayout :: Layout   -> EvalMode -> Bool -> [Locale] -> [MacroMap]
           -> [Option] -> Abbreviations -> Maybe Reference -> [Output]
evalLayout (Layout _ _ es) em b l m o a mbr
    = cleanOutput evalOut
    where
      evalOut = case evalState job initSt of
                  x | isNothing mbr -> [noBibDataError cit]
                    | null x        -> []
                    | otherwise     -> suppTC x
      locale = case l of
                 [x] -> x
                 _   -> Locale [] [] [] [] []
      job    = evalElements es
      cit    = case em of
                 EvalCite    c -> c
                 EvalSorting c -> c
                 EvalBiblio  c -> c
      initSt = EvalState (mkRefMap mbr) (Env cit (localeTerms locale) m
                         (localeDate locale) o [] a) [] em b False [] [] False [] [] []
      suppTC = let getLang = take 2 . map toLower in
               case (getLang $ localeLang locale,
                     getLang . unLiteral . language <$> mbr) of
                 (_,  Just "en") -> id
                 (_,  Nothing)   -> id
                 ("en", Just "") -> id
                 _               -> proc' rmTitleCase'
evalSorting :: EvalMode -> [Locale] -> [MacroMap] -> [Option] ->
               [Sort] -> Abbreviations -> Maybe Reference -> [Sorting]
evalSorting m l ms opts ss as mbr
    = map (format . sorting) ss
    where
      render       = renderPlain . formatOutputList . proc removeDelimAndLabel
      removeDelimAndLabel OLabel{} = ONull
      removeDelimAndLabel ODel{}   = ONull
      
      
      
      removeDelimAndLabel OSpace{} = OStr "," emptyFormatting
      removeDelimAndLabel x          = x
      format (s,e) = applaySort s . render $ uncurry eval e
      eval     o e = evalLayout (Layout emptyFormatting [] [e]) m False l ms o as mbr
      applaySort c s
          | Ascending {} <- c = Ascending  s
          | otherwise         = Descending s
      unsetOpts ("et-al-min"                 ,_) = ("et-al-min"           ,"")
      unsetOpts ("et-al-use-first"           ,_) = ("et-al-use-first"     ,"")
      unsetOpts ("et-al-subsequent-min"      ,_) = ("et-al-subsequent-min","")
      unsetOpts ("et-al-subsequent-use-first",_) = ("et-al-subsequent-use-first","")
      unsetOpts  x                               = x
      setOpts s i = if i /= 0 then (s, show i) else ([],[])
      sorting s
          = case s of
              SortVariable str s'     -> (s', ( ("name-as-sort-order","all") : opts
                                              , Variable [str] Long emptyFormatting []))
              SortMacro  str s' a b c -> (s', ( setOpts "et-al-min"       a : ("et-al-use-last",c) :
                                                setOpts "et-al-use-first" b : proc unsetOpts opts
                                              , Macro str emptyFormatting))
evalElements :: [Element] -> State EvalState [Output]
evalElements = concatMapM evalElement
evalElement :: Element -> State EvalState [Output]
evalElement el
    | Const    s   fm       <- el = return $ addSpaces s
                                           $ if fm == emptyFormatting
                                                then [OPan (readCSLString s)]
                                                else [Output [OPan (readCSLString s)] fm]
                                    
                                    
    | Number   s f fm       <- el = if s == "locator"
                                       then getLocVar >>= formatRange fm . snd
                                       else formatNumber f fm s =<<
                                            getStringVar s
    | Variable s f fm d     <- el = addDelim d <$> concatMapM (getVariable f fm) s
    | Group        fm d l   <- el = outputList fm d <$> tryGroup l
    | Date{} <- el = evalDate el
    | Label    s f fm _     <- el = formatLabel f fm True s 
    | Term     s f fm p     <- el = getStringVar "ref-id" >>= \refid ->
                                      formatTerm  f fm p refid  s
    | Names    s n fm d sub <- el = modify (\st -> st { contNum = [] }) >>
                                    ifEmpty (evalNames False s n d)
                                            (withNames s el $ evalElements sub)
                                            (appendOutput fm)
    | Substitute (e:els)    <- el = do
                        res <- consuming $ substituteWith e
                        if null res
                           then if null els
                                   then return [ONull]
                                   else evalElement (Substitute els)
                           else return res
    
    | Choose i ei xs        <- el = do
                        res <- evalIfThen i ei xs
                        evalElements res
    | Macro    s   fm       <- el = do
                        ms <- gets (macros . env)
                        case lookup s ms of
                             Nothing  -> E.throw $ MacroNotFound (show s)
                             Just els -> do
                               res <- concat <$> mapM evalElement els
                               if null res
                                  then return []
                                  else return [Output res fm]
    | otherwise                   = return []
    where
      addSpaces strng = (if take 1 strng == " " then (OSpace:) else id) .
                        (if last' strng == " " then (++[OSpace]) else id)
      substituteWith e =
        gets (names . env) >>= \case
          (Names _ ns fm d _ : _) -> evalElement $ proc replaceNames e
             where
               replaceNames (Names rs [Name NotSet fm'' [] [] []] fm' d' []) =
                  let nfm = mergeFM fm'' $ mergeFM fm' fm in
                  Names rs ns nfm (d' `orIfNull` d) []
               replaceNames x = x
          _ -> return []
      
      
      
      
      
      
      
      
      
      tryGroup l = if getAny $ query hasVar l
                   then do
                     oldState <- get
                     res <- evalElements (rmTermConst l)
                     put oldState
                     let numVars = [s | Number s _ _ <- l]
                     nums <- mapM getStringVar numVars
                     let pluralizeTerm (Term s f fm _) = Term s f fm $
                            case numVars of
                              ["number-of-volumes"] -> "1" `notElem` nums
                              ["number-of-pages"]   -> "1" `notElem` nums
                              _ -> any isRange nums
                         pluralizeTerm x = x
                     if null res
                        then return []
                        else evalElements $ map pluralizeTerm l
                   else evalElements l
      hasVar e
          | Variable {} <- e = Any True
          | Date     {} <- e = Any True
          | Names    {} <- e = Any True
          | Number   {} <- e = Any True
          | otherwise        = Any False
      rmTermConst = proc $ filter (not . isTermConst)
      isTermConst e
          | Term  {} <- e = True
          | Const {} <- e = True
          | otherwise     = False
      ifEmpty p t e = p >>= \r -> if null r then t else return (e r)
      withNames e n f = modify (\s -> s { authSub = e ++ authSub s
                                        , env = (env s)
                                          {names = n : names (env s)}}) >> f >>= \r ->
                         modify (\s -> s { authSub = filter (not . flip elem e) (authSub s)
                                        , env = (env s)
                                          {names = tail $ names (env s)}}) >> return r
      getVariable f fm s
        | isTitleVar s || isTitleShortVar s =
             consumeVariable s >> formatTitle s f fm
        | otherwise =
             case map toLower s of
               "first-reference-note-number"
                             -> do refid <- getStringVar "ref-id"
                                   return [Output [OPan [Span ("",["first-reference-note-number"],[("refid",refid)]) [Str "0"]]] fm]
               "year-suffix" -> getStringVar "ref-id" >>= \k  ->
                                return . return $ OYearSuf [] k [] fm
               "status"      -> do
                  (opts, as) <- gets (env >>> options &&& abbrevs)
                  r <- getVar mempty (getFormattedValue opts as f fm s)
                        "status"
                  consumeVariable s
                  return r
               "page"        -> getStringVar "page" >>= formatRange fm
               "locator"     -> getLocVar >>= formatRange fm . snd
               "url"         -> getStringVar "url" >>= \k ->
                                if null k then return [] else return [Output [OPan [Link nullAttr [Str k] (escapeURI k,"")]] fm]
               "doi"         -> do d <- getStringVar "doi"
                                   let (prefixPart, linkPart) = T.breakOn (T.pack "http") (T.pack (prefix fm))
                                   let u = if T.null linkPart
                                              then "https://doi.org/" ++ d
                                              else T.unpack linkPart ++ d
                                   if null d
                                      then return []
                                      else return [Output [OPan [Link nullAttr [Str (T.unpack linkPart ++ d)] (escapeURI u, "")]]
                                            fm{ prefix = T.unpack prefixPart, suffix = suffix fm }]
               "isbn"        -> getStringVar "isbn" >>= \d ->
                                if null d
                                   then return []
                                   else return [Output [OPan [Link nullAttr [Str d] ("https://worldcat.org/isbn/" ++ escapeURI d, "")]] fm]
               "pmid"        -> getStringVar "pmid" >>= \d ->
                                if null d
                                   then return []
                                   else return [Output [OPan [Link nullAttr [Str d] ("https://www.ncbi.nlm.nih.gov/pubmed/" ++ escapeURI d, "")]] fm]
               "pmcid"       -> getStringVar "pmcid" >>= \d ->
                                if null d
                                   then return []
                                   else return [Output [OPan [Link nullAttr [Str d] ("https://www.ncbi.nlm.nih.gov/pmc/articles/" ++ escapeURI d, "")]] fm]
               _ -> do (opts, as) <- gets (env >>> options &&& abbrevs)
                       r <- getVar []
                              (getFormattedValue opts as f fm s) s
                       consumeVariable s
                       return r
evalIfThen :: IfThen -> [IfThen] -> [Element] -> State EvalState [Element]
evalIfThen (IfThen c' m' el') ei e = whenElse (evalCond m' c') (return el') rest
  where
      rest = case ei of
                  []     -> return e
                  (x:xs) -> evalIfThen x xs e
      evalCond m c = do t <- checkCond chkType         isType          c m
                        v <- checkCond isVarSet        isSet           c m
                        n <- checkCond chkNumeric      isNumeric       c m
                        d <- checkCond chkDate         isUncertainDate c m
                        p <- checkCond chkPosition     isPosition      c m
                        a <- checkCond chkDisambiguate disambiguation  c m
                        l <- checkCond chkLocator      isLocator       c m
                        return $ match m $ concat [t,v,n,d,p,a,l]
      checkCond a f c m = case f c of
                               []  -> case m of
                                           All -> return [True]
                                           _   -> return [False]
                               xs  -> mapM a xs
      chkType         t = let chk = (==) (formatVariable t) . show . fromMaybe NoType . fromValue
                          in  getVar False chk "ref-type"
      chkNumeric      v = do val <- getStringVar v
                             as  <- gets (abbrevs . env)
                             let val' = if null (getAbbreviation as v val)
                                           then val
                                           else getAbbreviation as v val
                             return (isNumericString val')
      chkDate         v = any circa <$> getDateVar v
      chkPosition     s = if s == "near-note"
                          then gets (nearNote . cite . env)
                          else compPosition s <$> gets (citePosition . cite . env)
      chkDisambiguate s = (==) (formatVariable s) . map toLower . show <$> gets disamb
      chkLocator      v = (==) v . fst <$> getLocVar
      isIbid          s = not (s == "first" || s == "subsequent")
      compPosition a b
          | "first"             <- a = b == "first"
          | "subsequent"        <- a = b /= "first"
          | "ibid-with-locator" <- a = b == "ibid-with-locator" ||
                                       b == "ibid-with-locator-c"
          | otherwise                = isIbid b
getFormattedValue :: [Option] -> Abbreviations -> Form -> Formatting -> String -> Value -> [Output]
getFormattedValue o as f fm s val
    | Just (Formatted v) <- fromValue val :: Maybe Formatted =
       case v of
          [] -> []
          _  -> case maybe v (unFormatted . fromString) $ getAbbr (stringify v) of
                  [] -> []
                  ys -> [Output [(if s == "status"
                                     then OStatus
                                     else OPan) $ walk value' ys] fm]
    | Just v <- fromValue val :: Maybe String =
         case value v of
            [] -> []
            xs -> case getAbbr xs of
                    Nothing -> [OStr xs fm]
                    Just ys -> [OStr ys fm]
    | Just (Literal v) <- fromValue val :: Maybe Literal =
         case value v of
            [] -> []
            xs -> case getAbbr xs of
                    Nothing -> [OStr xs fm]
                    Just ys -> [OStr ys fm]
    | Just v <- fromValue val :: Maybe Int       = output  fm (if v == 0 then [] else show v)
    | Just v <- fromValue val :: Maybe CNum      = if v == 0 then [] else [OCitNum (unCNum v) fm]
    | Just v <- fromValue val :: Maybe CLabel    = if v == mempty then [] else [OCitLabel (unCLabel v) fm]
    | Just v <- fromValue val :: Maybe [RefDate] = formatDate (EvalSorting emptyCite) [] [] sortDate v
    | Just v <- fromValue val :: Maybe [Agent]   = concatMap (formatName (EvalSorting emptyCite) True f
                                                              fm nameOpts []) v
    | otherwise                                  = []
    where
      value     = if stripPeriods fm then filter (/= '.') else id
      value' (Str x) = Str (value x)
      value' x       = x
      getAbbr v = if f == Short
                  then case getAbbreviation as s v of
                             [] -> Nothing
                             y  -> Just y
                  else Nothing
      nameOpts = ("name-as-sort-order","all") : o
      sortDate = [ DatePart "year"  "numeric-leading-zeros" "" emptyFormatting
                 , DatePart "month" "numeric-leading-zeros" "" emptyFormatting
                 , DatePart "day"   "numeric-leading-zeros" "" emptyFormatting]
formatTitle :: String -> Form -> Formatting -> State EvalState [Output]
formatTitle s f fm
    | Short <- f
    , isTitleVar      s = try (getIt $ s ++ "-short") $ getIt s
    | isTitleShortVar s = try (getIt s) $ (:[]) . flip OStr fm <$> getTitleShort s
    | otherwise         = getIt s
    where
      try g h = g >>= \r -> if null r then h else return r
      getIt x = do
        o <- gets (options . env)
        a <- gets (abbrevs . env)
        getVar [] (getFormattedValue o a f fm x) x
formatNumber :: NumericForm -> Formatting -> String -> String -> State EvalState [Output]
formatNumber f fm v n
    = gets (abbrevs . env) >>= \as ->
      if isNumericString (getAbbr as n)
      then output fm . flip process (getAbbr as n) <$> gets (terms . env)
      else return . output fm . getAbbr as $ n
    where
      getAbbr       as   = if null (getAbbreviation as v n)
                              then id
                              else getAbbreviation as v
      checkRange'   ts   = if v == "page" then checkRange ts else id
      process       ts   = checkRange' ts . printNumStr . map (renderNumber ts) .
                           breakNumericString . words
      renderNumber  ts x = if isTransNumber x then format ts x else x
      format tm = case f of
                    Ordinal     -> maybe "" (ordinal     tm v) . safeRead
                    LongOrdinal -> maybe "" (longOrdinal tm v) . safeRead
                    Roman       -> maybe ""
                                   (\x -> if x < 6000 then roman x else show x) .
                                   safeRead
                    _           -> maybe "" show . (safeRead :: String -> Maybe Int)
      roman :: Int -> String
      roman     = concat . reverse . zipWith (!!) romanList .
                  map (readNum . return) . take 4 .
                  reverse . show
      romanList = [[ "", "i", "ii", "iii", "iv", "v", "vi", "vii", "viii", "ix" ]
                  ,[ "", "x", "xx", "xxx", "xl", "l", "lx", "lxx", "lxxx", "xc" ]
                  ,[ "", "c", "cc", "ccc", "cd", "d", "dc", "dcc", "dccc", "cm" ]
                  ,[ "", "m", "mm", "mmm", "mmmm", "mmmmm"]
                  ]
checkRange :: [CslTerm] -> String -> String
checkRange _ [] = []
checkRange ts (x:xs) = if x == '-' || x == '\x2013'
                       then pageRange ts ++ checkRange ts xs
                       else x             : checkRange ts xs
printNumStr :: [String] -> String
printNumStr []     = []
printNumStr [x] = x
printNumStr (x:"-":y:xs) = x ++ "-"  ++ y ++ printNumStr xs
printNumStr (x:",":y:xs) = x ++ ", " ++ y ++ printNumStr xs
printNumStr (x:xs)
    | x == "-"  = x ++        printNumStr xs
    | otherwise = x ++ " " ++ printNumStr xs
pageRange :: [CslTerm] -> String
pageRange = maybe "\x2013" termPlural . findTerm "page-range-delimiter" Long
isNumericString :: String -> Bool
isNumericString [] = False
isNumericString s  = all (\c -> isNumber c || isSpecialChar c) $ words s
isTransNumber, isSpecialChar,isNumber :: String -> Bool
isTransNumber = all isDigit
isSpecialChar = all (`elem` "&-,.\x2013")
isNumber   cs = case [c | c <- cs
                        , not (isLetter c)
                        , c `notElem` "&-.,\x2013"] of
                     [] -> False
                     xs -> all isDigit xs
breakNumericString :: [String] -> [String]
breakNumericString [] = []
breakNumericString (x:xs)
    | isTransNumber x = x : breakNumericString xs
    | otherwise       = let (a,b) = break (`elem` "&-\x2013,") x
                            (c,d) = if null b
                                       then ("","")
                                       else span (`elem` "&-\x2013,") b
                        in filter (/= []) $  a : c : breakNumericString (d : xs)
formatRange :: Formatting -> String -> State EvalState [Output]
formatRange _ [] = return []
formatRange fm p = do
  ops <- gets (options . env)
  ts  <- gets (terms . env)
  let opt = getOptionVal "page-range-format" ops
      pages = tupleRange . breakNumericString . words $ p
      tupleRange [] = []
      tupleRange [x, cs]
        | cs `elem` ["-", "--", "\x2013"] = return (x,[])
      tupleRange (x:cs:y:xs)
        | cs `elem` ["-", "--", "\x2013"] = (x, y) : tupleRange xs
      tupleRange (x:      xs) = (x,[]) : tupleRange xs
      joinRange (a, []) = a
      joinRange (a,  b) = a ++ "-" ++ b
      process = checkRange ts . printNumStr . case opt of
                 "expanded"    -> map (joinRange . expandedRange)
                 "chicago"     -> map (joinRange . chicagoRange )
                 "minimal"     -> map (joinRange . minimalRange 1)
                 "minimal-two" -> map (joinRange . minimalRange 2)
                 _             -> map joinRange
  return [OLoc [OStr (process pages) emptyFormatting] fm]
expandedRange :: (String, String) -> (String, String)
expandedRange (sa, []) = (sa,[])
expandedRange (sa, sb)
  | length sb < length sa =
      case (safeRead sa, safeRead sb) of
           
           (Just (_ :: Int), Just (_ :: Int)) ->
             (sa, take (length sa - length sb) sa ++ sb)
           _ -> (sa, sb)
  | otherwise = (sa, sb)
minimalRange :: Int -> (String, String) -> (String, String)
minimalRange minDigits (a:as, b:bs)
  | a == b
  , length as == length bs
  , length bs >= minDigits =
                let (_, bs') = minimalRange minDigits (as, bs)
                in  (a:as, bs')
minimalRange _ (as, bs) = (as, bs)
chicagoRange :: (String, String) -> (String, String)
chicagoRange (sa, sb)
    = case (safeRead sa :: Maybe Int) of
          Just n | n < 100 -> expandedRange (sa, sb)
                 | n `mod` 100 == 0 -> expandedRange (sa, sb)
                 | n >= 1000 -> let (sa', sb') = minimalRange 1 (sa, sb)
                                in  if length sb' >= 3
                                       then expandedRange (sa, sb)
                                       else (sa', sb')
                  | n > 100 -> if n `mod` 100 < 10
                                 then minimalRange 1 (sa, sb)
                                 else minimalRange 2 (sa, sb)
          _ -> expandedRange (sa, sb)