{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE ViewPatterns      #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE FlexibleContexts #-}
{-# OPTIONS_GHC -fno-warn-unused-do-bind #-}
-----------------------------------------------------------------------------
-- |
-- Module      :  Text.CSL.Input.Bibtex
-- Copyright   :  (c) John MacFarlane
-- License     :  BSD-style (see LICENSE)
--
-- Maintainer  :  John MacFarlane <fiddlosopher@gmail.com>
-- Stability   :  unstable-- Portability :  unportable
--
-----------------------------------------------------------------------------

module Text.CSL.Input.Bibtex
    ( readBibtex
    , readBibtexString
    , Lang(..)
    , langToLocale
    , getLangFromEnv
    )
    where

import Prelude
import           Control.Applicative
import qualified Control.Exception      as E
import           Control.Monad
import           Control.Monad.RWS      hiding ((<>))
import qualified Data.ByteString        as B
import           Data.Char              (isAlphaNum, isDigit, isUpper, toLower,
                                         toUpper)
import           Data.List              (foldl', intercalate)
import           Data.List.Split        (splitOn, splitWhen, wordsBy)
import qualified Data.Map               as Map
import           Data.Maybe
import           Data.Text              (Text)
import qualified Data.Text              as T
import           Data.Text.Encoding     (decodeUtf8)
import           System.Environment     (getEnvironment)
import           Text.CSL.Compat.Pandoc (readLaTeX)
import           Text.CSL.Exception     (CiteprocException (ErrorReadingBib, ErrorReadingBibFile))
import           Text.CSL.Parser        (parseLocale)
import           Text.CSL.Reference
import           Text.CSL.Style         (Agent (..), emptyAgent, CslTerm (..),
                                         Formatted (..), Locale (..))
import           Text.CSL.Util          (onBlocks, protectCase, safeRead,
                                         splitWhen, splitStrWhen, trim,
                                         unTitlecase, addSpaceAfterPeriod)
import           Text.Pandoc.Definition
import qualified Text.Pandoc.Walk       as Walk
import Text.Parsec hiding (State, many, (<|>))

blocksToFormatted  :: [Block]  -> Bib Formatted
blocksToFormatted bs =
  case bs of
       [Plain xs] -> inlinesToFormatted xs
       [Para  xs] -> inlinesToFormatted xs
       _          -> inlinesToFormatted $ Walk.query (:[]) bs

adjustSpans :: Lang -> Inline -> [Inline]
adjustSpans _ (Span ("",[],[]) xs) = xs
adjustSpans lang (RawInline (Format "latex") s)
  | s == "\\hyphen" || s == "\\hyphen " = [Str "-"]
  | otherwise = Walk.walk (concatMap (adjustSpans lang))
                $ parseRawLaTeX lang s
adjustSpans _ x = [x]

parseRawLaTeX :: Lang -> Text -> [Inline]
parseRawLaTeX lang (T.stripPrefix "\\" -> Just xs) =
  case latex' contents of
       [Para ys]  -> f command ys
       [Plain ys] -> f command ys
       _          -> []
   where (command', contents') = T.break (=='{') xs
         command  = trim command'
         contents = T.drop 1 $ T.dropEnd 1 contents'
         f "mkbibquote"    ils = [Quoted DoubleQuote ils]
         f "mkbibemph"     ils = [Emph ils]
         f "mkbibitalic"   ils = [Emph ils] -- TODO: italic/=emph
         f "mkbibbold"     ils = [Strong ils]
         f "mkbibparens"   ils = [Str "("] ++ ils ++ [Str ")"] -- TODO: ...
         f "mkbibbrackets" ils = [Str "["] ++ ils ++ [Str "]"] -- TODO: ...
         -- ... both should be nestable & should work in year fields
         f "autocap"    ils    = ils  -- TODO: should work in year fields
         f "textnormal" ils    = [Span ("",["nodecor"],[]) ils]
         f "bibstring" [Str s] = [Str $ resolveKey' lang s]
         f _            ils    = [Span nullAttr ils]
parseRawLaTeX _ _ = []

inlinesToFormatted :: [Inline] -> Bib Formatted
inlinesToFormatted ils = do
  lang <- gets localeLanguage
  return $ Formatted $ Walk.walk (concatMap (adjustSpans lang)) ils

data Item = Item{ identifier :: Text
                , entryType  :: Text
                , fields     :: Map.Map Text Text
                }

-- | Get 'Lang' from the environment variable LANG, defaulting to en-US.
getLangFromEnv :: IO Lang
getLangFromEnv = do
  env <- getEnvironment
  return $ case lookup "LANG" env of
    Just x  -> case Text.CSL.Util.splitWhen (\c -> c == '_' || c == '-')
                    (T.takeWhile (/='.') (T.pack x)) of
                 (w:z:_) -> Lang w z
                 [w]     | not (T.null w) -> Lang w mempty
                 _       -> Lang "en" "US"
    Nothing -> Lang "en" "US"

-- | Parse a BibTeX or BibLaTeX file into a list of 'Reference's.
-- The first parameter is a predicate to filter identifiers.
-- If the second parameter is true, the file will be treated as
-- BibTeX; otherwise as BibLaTeX.  If the third parameter is
-- true, an "untitlecase" transformation will be performed.
readBibtex :: (Text -> Bool) -> Bool -> Bool -> FilePath -> IO [Reference]
readBibtex idpred isBibtex caseTransform f = do
  contents <- decodeUtf8 <$> B.readFile f
  E.catch (readBibtexString idpred isBibtex caseTransform contents)
        (\e -> case e of
                  ErrorReadingBib es -> E.throwIO $ ErrorReadingBibFile f es
                  _                  -> E.throwIO e)

-- | Like 'readBibtex' but operates on Text rather than a file.
readBibtexString :: (Text -> Bool) -> Bool -> Bool -> Text
                 -> IO [Reference]
readBibtexString idpred isBibtex caseTransform contents = do
  lang <- getLangFromEnv
  locale <- parseLocale (langToLocale lang)
  case runParser (bibEntries <* eof) Map.empty "stdin" contents of
                      -- drop 8 to remove "stdin" + space
          Left err -> E.throwIO $ ErrorReadingBib $ drop 8 $ show err
          Right xs -> return $ mapMaybe
            (itemToReference lang locale isBibtex caseTransform)
            (filter (idpred . identifier)
              (resolveCrossRefs isBibtex
                xs))

type BibParser = Parsec Text (Map.Map Text Text)

bibEntries :: BibParser [Item]
bibEntries = do
  skipMany nonEntry
  many (bibItem <* skipMany nonEntry)
 where nonEntry = bibSkip <|>
                  try (char '@' >>
                       (bibComment <|> bibPreamble <|> bibString))

bibSkip :: BibParser ()
bibSkip = skipMany1 (satisfy (/='@'))

bibComment :: BibParser ()
bibComment = do
  cistring "comment"
  spaces
  void inBraces <|> bibSkip <|> return ()

bibPreamble :: BibParser ()
bibPreamble = do
  cistring "preamble"
  spaces
  void inBraces

bibString :: BibParser ()
bibString = do
  cistring "string"
  spaces
  char '{'
  spaces
  (k,v) <- entField
  char '}'
  updateState (Map.insert k v)
  return ()

inBraces :: BibParser Text
inBraces = try $ do
  char '{'
  res <- manyTill
         (  (T.pack <$> many1 (noneOf "{}\\"))
        <|> (char '\\' >> (  (char '{' >> return "\\{")
                         <|> (char '}' >> return "\\}")
                         <|> return "\\"))
        <|> (braced <$> inBraces)
         ) (char '}')
  return $ T.concat res

braced :: Text -> Text
braced = T.cons '{' . flip T.snoc '}'

inQuotes :: BibParser Text
inQuotes = do
  char '"'
  T.concat <$> manyTill (  (T.pack <$> many1 (noneOf "\"\\{"))
                       <|> (char '\\' >> T.cons '\\' . T.singleton <$> anyChar)
                       <|> braced <$> inBraces
                        ) (char '"')

fieldName :: BibParser Text
fieldName = resolveAlias . T.toLower . T.pack
  <$> many1 (letter <|> digit <|> oneOf "-_:+")

isBibtexKeyChar :: Char -> Bool
isBibtexKeyChar c = isAlphaNum c || c `elem` (".:;?!`'()/*@_+=-[]*&" :: String)

bibItem :: BibParser Item
bibItem = do
  char '@'
  enttype <- map toLower <$> many1 letter
  spaces
  char '{'
  spaces
  entid <- many1 (satisfy isBibtexKeyChar)
  spaces
  char ','
  spaces
  entfields <- entField `sepEndBy` (char ',' >> spaces)
  spaces
  char '}'
  return $ Item (T.pack entid) (T.pack enttype) (Map.fromList entfields)

entField :: BibParser (Text, Text)
entField = do
  k <- fieldName
  spaces
  char '='
  spaces
  vs <- (expandString <|> inQuotes <|> inBraces <|> rawWord) `sepBy`
            try (spaces >> char '#' >> spaces)
  spaces
  return (k, T.concat vs)

resolveAlias :: Text -> Text
resolveAlias "archiveprefix" = "eprinttype"
resolveAlias "primaryclass" = "eprintclass"
resolveAlias s = s

rawWord :: BibParser Text
rawWord = T.pack <$> many1 alphaNum

expandString :: BibParser Text
expandString = do
  k <- fieldName
  strs <- getState
  case Map.lookup k strs of
       Just v  -> return v
       Nothing -> return k -- return raw key if not found

cistring :: Text -> BibParser Text
cistring s = try (go s)
 where go t = case T.uncons t of
         Nothing     -> return ""
         Just (c,cs) -> do
           x <- char (toLower c) <|> char (toUpper c)
           xs <- go cs
           return (T.cons x xs)

resolveCrossRefs :: Bool -> [Item] -> [Item]
resolveCrossRefs isBibtex entries =
  map (resolveCrossRef isBibtex entries) entries

splitKeys :: Text -> [Text]
splitKeys = filter (not . T.null) . T.split (\c -> c == ' ' || c == ',')

getXrefFields :: Bool -> Item -> [Item] -> Text -> [(Text, Text)]
getXrefFields isBibtex baseEntry entries keys = do
  let keys' = splitKeys keys
  xrefEntry <- [e | e <- entries, identifier e `elem` keys']
  (k, v) <- Map.toList $ fields xrefEntry
  if k == "crossref" || k == "xdata"
     then do
       xs <- mapM (getXrefFields isBibtex baseEntry entries)
                   (splitKeys v)
       (x, y) <- xs
       guard $ isNothing $ Map.lookup x $ fields xrefEntry
       return (x, y)
     else do
       k' <- if isBibtex
                then return k
                else transformKey (entryType xrefEntry) (entryType baseEntry) k
       guard $ isNothing $ Map.lookup k' $ fields baseEntry
       return (k',v)

resolveCrossRef :: Bool -> [Item] -> Item -> Item
resolveCrossRef isBibtex entries entry =
  Map.foldrWithKey go entry (fields entry)
  where go key val entry' =
          if key == "crossref" || key == "xdata"
          then entry'{ fields = fields entry' <>
                          Map.fromList (getXrefFields isBibtex
                                        entry entries val) }
          else entry'

-- transformKey source target key
-- derived from Appendix C of bibtex manual
transformKey :: Text -> Text -> Text -> [Text]
transformKey _ _ "ids"            = []
transformKey _ _ "crossref"       = []
transformKey _ _ "xref"           = []
transformKey _ _ "entryset"       = []
transformKey _ _ "entrysubtype"   = []
transformKey _ _ "execute"        = []
transformKey _ _ "label"          = []
transformKey _ _ "options"        = []
transformKey _ _ "presort"        = []
transformKey _ _ "related"        = []
transformKey _ _ "relatedoptions" = []
transformKey _ _ "relatedstring"  = []
transformKey _ _ "relatedtype"    = []
transformKey _ _ "shorthand"      = []
transformKey _ _ "shorthandintro" = []
transformKey _ _ "sortkey"        = []
transformKey x y "author"
  | x `elem` ["mvbook", "book"] &&
    y `elem` ["inbook", "bookinbook", "suppbook"] = ["bookauthor", "author"]
-- note: this next clause is not in the biblatex manual, but it makes
-- sense in the context of CSL conversion:
transformKey x y "author"
  | x == "mvbook" && y == "book" = ["bookauthor", "author"]
transformKey "mvbook" y z
  | y `elem` ["book", "inbook", "bookinbook", "suppbook"] = standardTrans z
transformKey x y z
  | x `elem` ["mvcollection", "mvreference"] &&
    y `elem` ["collection", "reference", "incollection", "inreference",
               "suppcollection"] = standardTrans z
transformKey "mvproceedings" y z
  | y `elem` ["proceedings", "inproceedings"] = standardTrans z
transformKey "book" y z
  | y `elem` ["inbook", "bookinbook", "suppbook"] = bookTrans z
transformKey x y z
  | x `elem` ["collection", "reference"] &&
    y `elem` ["incollection", "inreference", "suppcollection"] = bookTrans z
transformKey "proceedings" "inproceedings" z = bookTrans z
transformKey "periodical" y z
  | y `elem` ["article", "suppperiodical"] =
  case z of
       "title"          -> ["journaltitle"]
       "subtitle"       -> ["journalsubtitle"]
       "shorttitle"     -> []
       "sorttitle"      -> []
       "indextitle"     -> []
       "indexsorttitle" -> []
       _                -> [z]
transformKey _ _ x                = [x]

standardTrans :: Text -> [Text]
standardTrans z =
  case z of
       "title"          -> ["maintitle"]
       "subtitle"       -> ["mainsubtitle"]
       "titleaddon"     -> ["maintitleaddon"]
       "shorttitle"     -> []
       "sorttitle"      -> []
       "indextitle"     -> []
       "indexsorttitle" -> []
       _                -> [z]

bookTrans :: Text -> [Text]
bookTrans z =
  case z of
       "title"          -> ["booktitle"]
       "subtitle"       -> ["booksubtitle"]
       "titleaddon"     -> ["booktitleaddon"]
       "shorttitle"     -> []
       "sorttitle"      -> []
       "indextitle"     -> []
       "indexsorttitle" -> []
       _                -> [z]

-- | A representation of a language and localization.
data Lang = Lang Text Text  -- e.g. "en" "US"

-- | Prints a 'Lang' in BCP 47 format.
langToLocale :: Lang -> Text
langToLocale (Lang x y) = x <> (if T.null y then "" else T.cons '-' y)

-- Biblatex Localization Keys (see Biblatex manual)
-- Currently we only map a subset likely to be used in Biblatex *databases*
-- (in fields such as `type`, and via `\bibstring{}` commands).

resolveKey :: Lang -> Formatted -> Formatted
resolveKey lang (Formatted ils) = Formatted (Walk.walk go ils)
  where go (Str s) = Str $ resolveKey' lang s
        go x       = x

-- biblatex localization keys, from files at
-- http://github.com/plk/biblatex/tree/master/tex/latex/biblatex/lbx
-- Some keys missing in these were added from csl locale files at
-- http://github.com/citation-style-language/locales -- labeled "csl"
resolveKey' :: Lang -> Text -> Text
resolveKey' (Lang "ca" "AD") k =
    case T.toLower k of
       "inpreparation" -> "en preparació"
       "submitted"     -> "enviat"
       "forthcoming"   -> "disponible en breu"
       "inpress"       -> "a impremta"
       "prepublished"  -> "pre-publicat"
       "mathesis"      -> "tesi de màster"
       "phdthesis"     -> "tesi doctoral"
       "candthesis"    -> "tesi de candidatura"
       "techreport"    -> "informe tècnic"
       "resreport"     -> "informe de recerca"
       "software"      -> "programari"
       "datacd"        -> "CD de dades"
       "audiocd"       -> "CD d’àudio"
       "patent"        -> "patent"
       "patentde"      -> "patent alemana"
       "patenteu"      -> "patent europea"
       "patentfr"      -> "patent francesa"
       "patentuk"      -> "patent britànica"
       "patentus"      -> "patent estatunidenca"
       "patreq"        -> "soŀlicitud de patent"
       "patreqde"      -> "soŀlicitud de patent alemana"
       "patreqeu"      -> "soŀlicitud de patent europea"
       "patreqfr"      -> "soŀlicitud de patent francesa"
       "patrequk"      -> "soŀlicitud de patent britànica"
       "patrequs"      -> "soŀlicitud de patent estatunidenca"
       "countryde"     -> "Alemanya"
       "countryeu"     -> "Unió Europea"
       "countryep"     -> "Unió Europea"
       "countryfr"     -> "França"
       "countryuk"     -> "Regne Unit"
       "countryus"     -> "Estats Units d’Amèrica"
       "newseries"     -> "sèrie nova"
       "oldseries"     -> "sèrie antiga"
       _               -> k
resolveKey' (Lang "da" "DK") k =
    case T.toLower k of
       -- "inpreparation" -> "" -- missing
       -- "submitted"     -> "" -- missing
       "forthcoming" -> "kommende" -- csl
       "inpress"     -> "i tryk"   -- csl
       -- "prepublished"  -> "" -- missing
       "mathesis"    -> "speciale"
       "phdthesis"   -> "ph.d.-afhandling"
       "candthesis"  -> "kandidatafhandling"
       "techreport"  -> "teknisk rapport"
       "resreport"   -> "forskningsrapport"
       "software"    -> "software"
       "datacd"      -> "data-cd"
       "audiocd"     -> "lyd-cd"
       "patent"      -> "patent"
       "patentde"    -> "tysk patent"
       "patenteu"    -> "europæisk patent"
       "patentfr"    -> "fransk patent"
       "patentuk"    -> "britisk patent"
       "patentus"    -> "amerikansk patent"
       "patreq"      -> "ansøgning om patent"
       "patreqde"    -> "ansøgning om tysk patent"
       "patreqeu"    -> "ansøgning om europæisk patent"
       "patreqfr"    -> "ansøgning om fransk patent"
       "patrequk"    -> "ansøgning om britisk patent"
       "patrequs"    -> "ansøgning om amerikansk patent"
       "countryde"   -> "Tyskland"
       "countryeu"   -> "Europæiske Union"
       "countryep"   -> "Europæiske Union"
       "countryfr"   -> "Frankrig"
       "countryuk"   -> "Storbritanien"
       "countryus"   -> "USA"
       "newseries"   -> "ny serie"
       "oldseries"   -> "gammel serie"
       _             -> k
resolveKey' (Lang "de" "DE") k =
    case T.toLower k of
       "inpreparation" -> "in Vorbereitung"
       "submitted"     -> "eingereicht"
       "forthcoming"   -> "im Erscheinen"
       "inpress"       -> "im Druck"
       "prepublished"  -> "Vorveröffentlichung"
       "mathesis"      -> "Magisterarbeit"
       "phdthesis"     -> "Dissertation"
       -- "candthesis" -> "" -- missing
       "techreport"    -> "Technischer Bericht"
       "resreport"     -> "Forschungsbericht"
       "software"      -> "Computer-Software"
       "datacd"        -> "CD-ROM"
       "audiocd"       -> "Audio-CD"
       "patent"        -> "Patent"
       "patentde"      -> "deutsches Patent"
       "patenteu"      -> "europäisches Patent"
       "patentfr"      -> "französisches Patent"
       "patentuk"      -> "britisches Patent"
       "patentus"      -> "US-Patent"
       "patreq"        -> "Patentanmeldung"
       "patreqde"      -> "deutsche Patentanmeldung"
       "patreqeu"      -> "europäische Patentanmeldung"
       "patreqfr"      -> "französische Patentanmeldung"
       "patrequk"      -> "britische Patentanmeldung"
       "patrequs"      -> "US-Patentanmeldung"
       "countryde"     -> "Deutschland"
       "countryeu"     -> "Europäische Union"
       "countryep"     -> "Europäische Union"
       "countryfr"     -> "Frankreich"
       "countryuk"     -> "Großbritannien"
       "countryus"     -> "USA"
       "newseries"     -> "neue Folge"
       "oldseries"     -> "alte Folge"
       _               -> k
resolveKey' (Lang "en" "US") k =
  case T.toLower k of
       "audiocd"        -> "audio CD"
       "by"             -> "by"
       "candthesis"     -> "Candidate thesis"
       "countryde"      -> "Germany"
       "countryep"      -> "European Union"
       "countryeu"      -> "European Union"
       "countryfr"      -> "France"
       "countryuk"      -> "United Kingdom"
       "countryus"      -> "United States of America"
       "datacd"         -> "data CD"
       "edition"        -> "ed."
       "forthcoming"    -> "forthcoming"
       "inpreparation"  -> "in preparation"
       "inpress"        -> "in press"
       "introduction"   -> "introduction"
       "jourser"        -> "ser."
       "mathesis"       -> "Master’s thesis"
       "newseries"      -> "new series"
       "nodate"         -> "n. d."
       "number"         -> "no."
       "numbers"        -> "nos."
       "oldseries"      -> "old series"
       "patent"         -> "patent"
       "patentde"       -> "German patent"
       "patenteu"       -> "European patent"
       "patentfr"       -> "French patent"
       "patentuk"       -> "British patent"
       "patentus"       -> "U.S. patent"
       "patreq"         -> "patent request"
       "patreqde"       -> "German patent request"
       "patreqeu"       -> "European patent request"
       "patreqfr"       -> "French patent request"
       "patrequk"       -> "British patent request"
       "patrequs"       -> "U.S. patent request"
       "phdthesis"      -> "PhD thesis"
       "prepublished"   -> "pre-published"
       "pseudonym"      -> "pseud."
       "recorded"       -> "recorded"
       "resreport"      -> "research report"
       "reviewof"       -> "Review of"
       "revisededition" -> "rev. ed."
       "software"       -> "computer software"
       "submitted"      -> "submitted"
       "techreport"     -> "technical report"
       "volume"         -> "vol."
       _                -> k
resolveKey' (Lang "es" "ES") k =
    case T.toLower k of
       -- "inpreparation" -> "" -- missing
       -- "submitted"     -> "" -- missing
       "forthcoming" -> "previsto"    -- csl
       "inpress"     -> "en imprenta" -- csl
       -- "prepublished"  -> "" -- missing
       "mathesis"    -> "Tesis de licenciatura"
       "phdthesis"   -> "Tesis doctoral"
       -- "candthesis" -> "" -- missing
       "techreport"  -> "informe técnico"
       -- "resreport"  -> "" -- missing
       -- "software"   -> "" -- missing
       -- "datacd"     -> "" -- missing
       -- "audiocd"    -> "" -- missing
       "patent"      -> "patente"
       "patentde"    -> "patente alemana"
       "patenteu"    -> "patente europea"
       "patentfr"    -> "patente francesa"
       "patentuk"    -> "patente británica"
       "patentus"    -> "patente americana"
       "patreq"      -> "solicitud de patente"
       "patreqde"    -> "solicitud de patente alemana"
       "patreqeu"    -> "solicitud de patente europea"
       "patreqfr"    -> "solicitud de patente francesa"
       "patrequk"    -> "solicitud de patente británica"
       "patrequs"    -> "solicitud de patente americana"
       "countryde"   -> "Alemania"
       "countryeu"   -> "Unión Europea"
       "countryep"   -> "Unión Europea"
       "countryfr"   -> "Francia"
       "countryuk"   -> "Reino Unido"
       "countryus"   -> "Estados Unidos de América"
       "newseries"   -> "nueva época"
       "oldseries"   -> "antigua época"
       _             -> k
resolveKey' (Lang "fi" "FI") k =
    case T.toLower k of
       -- "inpreparation" -> ""      -- missing
       -- "submitted"     -> ""      -- missing
       "forthcoming" -> "tulossa"  -- csl
       "inpress"     -> "painossa" -- csl
       -- "prepublished"  -> ""      -- missing
       "mathesis"    -> "tutkielma"
       "phdthesis"   -> "tohtorinväitöskirja"
       "candthesis"  -> "kandidat"
       "techreport"  -> "tekninen raportti"
       "resreport"   -> "tutkimusraportti"
       "software"    -> "ohjelmisto"
       "datacd"      -> "data-CD"
       "audiocd"     -> "ääni-CD"
       "patent"      -> "patentti"
       "patentde"    -> "saksalainen patentti"
       "patenteu"    -> "Euroopan Unionin patentti"
       "patentfr"    -> "ranskalainen patentti"
       "patentuk"    -> "englantilainen patentti"
       "patentus"    -> "yhdysvaltalainen patentti"
       "patreq"      -> "patenttihakemus"
       "patreqde"    -> "saksalainen patenttihakemus"
       "patreqeu"    -> "Euroopan Unionin patenttihakemus"
       "patreqfr"    -> "ranskalainen patenttihakemus"
       "patrequk"    -> "englantilainen patenttihakemus"
       "patrequs"    -> "yhdysvaltalainen patenttihakemus"
       "countryde"   -> "Saksa"
       "countryeu"   -> "Euroopan Unioni"
       "countryep"   -> "Euroopan Unioni"
       "countryfr"   -> "Ranska"
       "countryuk"   -> "Iso-Britannia"
       "countryus"   -> "Yhdysvallat"
       "newseries"   -> "uusi sarja"
       "oldseries"   -> "vanha sarja"
       _             -> k

resolveKey' (Lang "fr" "FR") k =
    case T.toLower k of
       "inpreparation" -> "en préparation"
       "submitted"     -> "soumis"
       "forthcoming"   -> "à paraître"
       "inpress"       -> "sous presse"
       "prepublished"  -> "prépublié"
       "mathesis"      -> "mémoire de master"
       "phdthesis"     -> "thèse de doctorat"
       "candthesis"    -> "thèse de candidature"
       "techreport"    -> "rapport technique"
       "resreport"     -> "rapport scientifique"
       "software"      -> "logiciel"
       "datacd"        -> "cédérom"
       "audiocd"       -> "disque compact audio"
       "patent"        -> "brevet"
       "patentde"      -> "brevet allemand"
       "patenteu"      -> "brevet européen"
       "patentfr"      -> "brevet français"
       "patentuk"      -> "brevet britannique"
       "patentus"      -> "brevet américain"
       "patreq"        -> "demande de brevet"
       "patreqde"      -> "demande de brevet allemand"
       "patreqeu"      -> "demande de brevet européen"
       "patreqfr"      -> "demande de brevet français"
       "patrequk"      -> "demande de brevet britannique"
       "patrequs"      -> "demande de brevet américain"
       "countryde"     -> "Allemagne"
       "countryeu"     -> "Union européenne"
       "countryep"     -> "Union européenne"
       "countryfr"     -> "France"
       "countryuk"     -> "Royaume-Uni"
       "countryus"     -> "États-Unis"
       "newseries"     -> "nouvelle série"
       "oldseries"     -> "ancienne série"
       _               -> k
resolveKey' (Lang "it" "IT") k =
    case T.toLower k of
       -- "inpreparation" -> "" -- missing
       -- "submitted"     -> "" -- missing
       "forthcoming" -> "futuro" -- csl
       "inpress"     -> "in stampa"
       -- "prepublished"  -> "" -- missing
       "mathesis"    -> "tesi di laurea magistrale"
       "phdthesis"   -> "tesi di dottorato"
       -- "candthesis" -> "" -- missing
       "techreport"  -> "rapporto tecnico"
       "resreport"   -> "rapporto di ricerca"
       -- "software"   -> "" -- missing
       -- "datacd"     -> "" -- missing
       -- "audiocd"    -> "" -- missing
       "patent"      -> "brevetto"
       "patentde"    -> "brevetto tedesco"
       "patenteu"    -> "brevetto europeo"
       "patentfr"    -> "brevetto francese"
       "patentuk"    -> "brevetto britannico"
       "patentus"    -> "brevetto americano"
       "patreq"      -> "brevetto richiesto"
       "patreqde"    -> "brevetto tedesco richiesto"
       "patreqeu"    -> "brevetto europeo richiesto"
       "patreqfr"    -> "brevetto francese richiesto"
       "patrequk"    -> "brevetto britannico richiesto"
       "patrequs"    -> "brevetto U.S.A. richiesto"
       "countryde"   -> "Germania"
       "countryeu"   -> "Unione Europea"
       "countryep"   -> "Unione Europea"
       "countryfr"   -> "Francia"
       "countryuk"   -> "Regno Unito"
       "countryus"   -> "Stati Uniti d’America"
       "newseries"   -> "nuova serie"
       "oldseries"   -> "vecchia serie"
       _             -> k
resolveKey' (Lang "nl" "NL") k =
    case T.toLower k of
       "inpreparation" -> "in voorbereiding"
       "submitted"     -> "ingediend"
       "forthcoming"   -> "onderweg"
       "inpress"       -> "in druk"
       "prepublished"  -> "voorpublicatie"
       "mathesis"      -> "masterscriptie"
       "phdthesis"     -> "proefschrift"
       -- "candthesis" -> "" -- missing
       "techreport"    -> "technisch rapport"
       "resreport"     -> "onderzoeksrapport"
       "software"      -> "computersoftware"
       "datacd"        -> "cd-rom"
       "audiocd"       -> "audio-cd"
       "patent"        -> "patent"
       "patentde"      -> "Duits patent"
       "patenteu"      -> "Europees patent"
       "patentfr"      -> "Frans patent"
       "patentuk"      -> "Brits patent"
       "patentus"      -> "Amerikaans patent"
       "patreq"        -> "patentaanvraag"
       "patreqde"      -> "Duitse patentaanvraag"
       "patreqeu"      -> "Europese patentaanvraag"
       "patreqfr"      -> "Franse patentaanvraag"
       "patrequk"      -> "Britse patentaanvraag"
       "patrequs"      -> "Amerikaanse patentaanvraag"
       "countryde"     -> "Duitsland"
       "countryeu"     -> "Europese Unie"
       "countryep"     -> "Europese Unie"
       "countryfr"     -> "Frankrijk"
       "countryuk"     -> "Verenigd Koninkrijk"
       "countryus"     -> "Verenigde Staten van Amerika"
       "newseries"     -> "nieuwe reeks"
       "oldseries"     -> "oude reeks"
       _               -> k
resolveKey' (Lang "pl" "PL") k =
    case T.toLower k of
       "inpreparation" -> "przygotowanie"
       "submitted"     -> "prezentacja"
       "forthcoming"   -> "przygotowanie"
       "inpress"       -> "wydrukowane"
       "prepublished"  -> "przedwydanie"
       "mathesis"      -> "praca magisterska"
       "phdthesis"     -> "praca doktorska"
       "techreport"    -> "sprawozdanie techniczne"
       "resreport"     -> "sprawozdanie naukowe"
       "software"      -> "oprogramowanie"
       "datacd"        -> "CD-ROM"
       "audiocd"       -> "audio CD"
       "patent"        -> "patent"
       "patentde"      -> "patent Niemiec"
       "patenteu"      -> "patent Europy"
       "patentfr"      -> "patent Francji"
       "patentuk"      -> "patent Wielkiej Brytanji"
       "patentus"      -> "patent USA"
       "patreq"        -> "podanie na patent"
       "patreqeu"      -> "podanie na patent Europy"
       "patrequs"      -> "podanie na patent USA"
       "countryde"     -> "Niemcy"
       "countryeu"     -> "Unia Europejska"
       "countryep"     -> "Unia Europejska"
       "countryfr"     -> "Francja"
       "countryuk"     -> "Wielka Brytania"
       "countryus"     -> "Stany Zjednoczone Ameryki"
       "newseries"     -> "nowa serja"
       "oldseries"     -> "stara serja"
       _               -> k
resolveKey' (Lang "pt" "PT") k =
    case T.toLower k of
       -- "candthesis" -> "" -- missing
       "techreport"  -> "relatório técnico"
       "resreport"   -> "relatório de pesquisa"
       "software"    -> "software"
       "datacd"      -> "CD-ROM"
       "patent"      -> "patente"
       "patentde"    -> "patente alemã"
       "patenteu"    -> "patente européia"
       "patentfr"    -> "patente francesa"
       "patentuk"    -> "patente britânica"
       "patentus"    -> "patente americana"
       "patreq"      -> "pedido de patente"
       "patreqde"    -> "pedido de patente alemã"
       "patreqeu"    -> "pedido de patente européia"
       "patreqfr"    -> "pedido de patente francesa"
       "patrequk"    -> "pedido de patente britânica"
       "patrequs"    -> "pedido de patente americana"
       "countryde"   -> "Alemanha"
       "countryeu"   -> "União Europeia"
       "countryep"   -> "União Europeia"
       "countryfr"   -> "França"
       "countryuk"   -> "Reino Unido"
       "countryus"   -> "Estados Unidos da América"
       "newseries"   -> "nova série"
       "oldseries"   -> "série antiga"
       -- "inpreparation" -> "" -- missing
       "forthcoming" -> "a publicar" -- csl
       "inpress"     -> "na imprensa"
       -- "prepublished"  -> "" -- missing
       "mathesis"    -> "tese de mestrado"
       "phdthesis"   -> "tese de doutoramento"
       "audiocd"     -> "CD áudio"
       _             -> k
resolveKey' (Lang "pt" "BR") k =
    case T.toLower k of
       -- "candthesis" -> "" -- missing
       "techreport"    -> "relatório técnico"
       "resreport"     -> "relatório de pesquisa"
       "software"      -> "software"
       "datacd"        -> "CD-ROM"
       "patent"        -> "patente"
       "patentde"      -> "patente alemã"
       "patenteu"      -> "patente européia"
       "patentfr"      -> "patente francesa"
       "patentuk"      -> "patente britânica"
       "patentus"      -> "patente americana"
       "patreq"        -> "pedido de patente"
       "patreqde"      -> "pedido de patente alemã"
       "patreqeu"      -> "pedido de patente européia"
       "patreqfr"      -> "pedido de patente francesa"
       "patrequk"      -> "pedido de patente britânica"
       "patrequs"      -> "pedido de patente americana"
       "countryde"     -> "Alemanha"
       "countryeu"     -> "União Europeia"
       "countryep"     -> "União Europeia"
       "countryfr"     -> "França"
       "countryuk"     -> "Reino Unido"
       "countryus"     -> "Estados Unidos da América"
       "newseries"     -> "nova série"
       "oldseries"     -> "série antiga"
       "inpreparation" -> "em preparação"
       "forthcoming"   -> "aceito para publicação"
       "inpress"       -> "no prelo"
       "prepublished"  -> "pré-publicado"
       "mathesis"      -> "dissertação de mestrado"
       "phdthesis"     -> "tese de doutorado"
       "audiocd"       -> "CD de áudio"
       _               -> k
resolveKey' (Lang "sv" "SE") k =
    case T.toLower k of
       -- "inpreparation" -> "" -- missing
       -- "submitted"     -> "" -- missing
       "forthcoming" -> "kommande" -- csl
       "inpress"     -> "i tryck"  -- csl
       -- "prepublished"  -> "" -- missing
       "mathesis"    -> "examensarbete"
       "phdthesis"   -> "doktorsavhandling"
       "candthesis"  -> "kandidatavhandling"
       "techreport"  -> "teknisk rapport"
       "resreport"   -> "forskningsrapport"
       "software"    -> "datorprogram"
       "datacd"      -> "data-cd"
       "audiocd"     -> "ljud-cd"
       "patent"      -> "patent"
       "patentde"    -> "tyskt patent"
       "patenteu"    -> "europeiskt patent"
       "patentfr"    -> "franskt patent"
       "patentuk"    -> "brittiskt patent"
       "patentus"    -> "amerikanskt patent"
       "patreq"      -> "patentansökan"
       "patreqde"    -> "ansökan om tyskt patent"
       "patreqeu"    -> "ansökan om europeiskt patent"
       "patreqfr"    -> "ansökan om franskt patent"
       "patrequk"    -> "ansökan om brittiskt patent"
       "patrequs"    -> "ansökan om amerikanskt patent"
       "countryde"   -> "Tyskland"
       "countryeu"   -> "Europeiska unionen"
       "countryep"   -> "Europeiska unionen"
       "countryfr"   -> "Frankrike"
       "countryuk"   -> "Storbritannien"
       "countryus"   -> "USA"
       "newseries"   -> "ny följd"
       "oldseries"   -> "gammal följd"
       _             -> k
resolveKey' _ k = resolveKey' (Lang "en" "US") k

parseMonth :: Text -> Maybe Int
parseMonth s =
  case T.toLower s of
         "jan" -> Just 1
         "feb" -> Just 2
         "mar" -> Just 3
         "apr" -> Just 4
         "may" -> Just 5
         "jun" -> Just 6
         "jul" -> Just 7
         "aug" -> Just 8
         "sep" -> Just 9
         "oct" -> Just 10
         "nov" -> Just 11
         "dec" -> Just 12
         _     -> safeRead s

data BibState = BibState{
           untitlecase    :: Bool
         , localeLanguage :: Lang
         }

type Bib = RWST Item () BibState Maybe

notFound :: Text -> Bib a
notFound f = Prelude.fail $ T.unpack f ++ " not found"

getField :: Text -> Bib Formatted
getField f = do
  fs <- asks fields
  case Map.lookup f fs of
       Just x  -> latex x
       Nothing -> notFound f

getPeriodicalTitle :: Text -> Bib Formatted
getPeriodicalTitle f = do
  fs <- asks fields
  case Map.lookup f fs of
       Just x  -> blocksToFormatted $ onBlocks protectCase $ latex' $ trim x
       Nothing -> notFound f

getTitle :: Text -> Bib Formatted
getTitle f = do
  fs <- asks fields
  case Map.lookup f fs of
       Just x  -> latexTitle x
       Nothing -> notFound f

getShortTitle :: Bool -> Text -> Bib Formatted
getShortTitle requireColon f = do
  fs <- asks fields
  utc <- gets untitlecase
  let processTitle = if utc then onBlocks unTitlecase else id
  case Map.lookup f fs of
       Just x  -> case processTitle $ latex' x of
                       bs | not requireColon || containsColon bs ->
                                  blocksToFormatted $ upToColon bs
                          | otherwise -> return mempty
       Nothing -> notFound f

containsColon :: [Block] -> Bool
containsColon [Para  xs] = Str ":" `elem` xs
containsColon [Plain xs] = containsColon [Para xs]
containsColon _          = False

upToColon :: [Block] -> [Block]
upToColon [Para  xs] = [Para $ takeWhile (/= Str ":") xs]
upToColon [Plain xs] = upToColon [Para xs]
upToColon bs         = bs

getDates :: Text -> Bib [RefDate]
getDates f = parseEDTFDate <$> getRawField f

isNumber :: Text -> Bool
isNumber t = case T.uncons t of
  Just ('-', ds) -> T.all isDigit ds
  Just _         -> T.all isDigit t
  Nothing        -> False

-- A negative (BC) year might be written with -- or --- in bibtex:
fixLeadingDash :: Text -> Text
fixLeadingDash t = case T.uncons t of
  Just (c, ds) | (c == '–' || c == '—') && firstIsDigit ds -> T.cons '–' ds
  _ -> t
 where firstIsDigit = maybe False (isDigit . fst) . T.uncons

getOldDates :: Text -> Bib [RefDate]
getOldDates prefix = do
  year' <- fixLeadingDash <$> getRawField (prefix <> "year")
           <|> return ""
  month' <- (parseMonth <$> getRawField (prefix <> "month"))
            <|> return Nothing
  day' <- (safeRead <$> getRawField (prefix <> "day"))
          <|> return Nothing
  endyear' <- (fixLeadingDash <$> getRawField (prefix <> "endyear"))
            <|> return ""
  endmonth' <- (parseMonth <$> getRawField (prefix <> "endmonth"))
            <|> return Nothing
  endday' <- (safeRead <$> getRawField (prefix <> "endday"))
             <|> return Nothing
  let start' = RefDate { year   = safeRead year'
                       , month  = month'
                       , season = Nothing
                       , day    = day'
                       , other  = Literal $ if isNumber year' then "" else year'
                       , circa  = False
                       }
  let end' = RefDate { year     = safeRead endyear'
                       , month  = endmonth'
                       , day    = endday'
                       , season = Nothing
                       , other  = Literal $ if isNumber endyear' then "" else endyear'
                       , circa  = False
                       }
  let hasyear r = isJust (year r)
  return $ filter hasyear [start', end']

getRawField :: Text -> Bib Text
getRawField f = do
  fs <- asks fields
  case Map.lookup f fs of
       Just x  -> return x
       Nothing -> notFound f

getAuthorList :: Options -> Text -> Bib [Agent]
getAuthorList opts  f = do
  fs <- asks fields
  case Map.lookup f fs of
       Just x  -> latexAuthors opts x
       Nothing -> notFound f

getLiteralList :: Text -> Bib [Formatted]
getLiteralList f = do
  fs <- asks fields
  case Map.lookup f fs of
       Just x  -> toLiteralList $ latex' x
       Nothing -> notFound f

-- separates items with semicolons
getLiteralList' :: Text -> Bib Formatted
getLiteralList' f = Formatted . intercalate [Str ";", Space] . map unFormatted
  <$> getLiteralList f

splitByAnd :: [Inline] -> [[Inline]]
splitByAnd = splitOn [Space, Str "and", Space]

toLiteralList :: [Block] -> Bib [Formatted]
toLiteralList [Para xs] =
  mapM inlinesToFormatted $ splitByAnd xs
toLiteralList [Plain xs] = toLiteralList [Para xs]
toLiteralList _ = mzero

toAuthorList :: Options -> [Block] -> Bib [Agent]
toAuthorList opts [Para xs] =
  filter (/= emptyAgent) <$> mapM (toAuthor opts . addSpaceAfterPeriod)
                                    (splitByAnd xs)
toAuthorList opts [Plain xs] = toAuthorList opts [Para xs]
toAuthorList _ _ = mzero

toAuthor :: Options -> [Inline] -> Bib Agent
toAuthor _ [Str "others"] =
  return
    Agent { givenName       = []
          , droppingPart    = mempty
          , nonDroppingPart = mempty
          , familyName      = mempty
          , nameSuffix      = mempty
          , literal         = Formatted [Str "others"]
          , commaSuffix     = False
          , parseNames      = False
          }
toAuthor _ [Span ("",[],[]) ils] =
  return -- corporate author
    Agent { givenName       = []
          , droppingPart    = mempty
          , nonDroppingPart = mempty
          , familyName      = mempty
          , nameSuffix      = mempty
          , literal         = Formatted ils
          , commaSuffix     = False
          , parseNames      = False
          }
 -- extended BibLaTeX name format - see #266
toAuthor _ ils@(Str ys:_) | T.any (== '=') ys = do
  let commaParts = Data.List.Split.splitWhen (== Str ",")
                   . splitStrWhen (\c -> c == ',' || c == '=' || c == '\160')
                   $ ils
  let addPart ag (Str "given" : Str "=" : xs) =
        ag{ givenName = givenName ag ++ [Formatted xs] }
      addPart ag (Str "family" : Str "=" : xs) =
        ag{ familyName = Formatted xs }
      addPart ag (Str "prefix" : Str "=" : xs) =
        ag{ droppingPart = Formatted xs }
      addPart ag (Str "useprefix" : Str "=" : Str "true" : _) =
        ag{ nonDroppingPart = droppingPart ag, droppingPart = mempty }
      addPart ag (Str "suffix" : Str "=" : xs) =
        ag{ nameSuffix = Formatted xs }
      addPart ag (Space : xs) = addPart ag xs
      addPart ag _ = ag
  return $ foldl' addPart emptyAgent commaParts

-- First von Last
-- von Last, First
-- von Last, Jr ,First
-- NOTE: biblatex and bibtex differ on:
-- Drummond de Andrade, Carlos
-- bibtex takes "Drummond de" as the von;
-- biblatex takes the whole as a last name.
-- See https://github.com/plk/biblatex/issues/236
-- Here we implement the more sensible biblatex behavior.
toAuthor opts ils = do
  let useprefix = optionSet "useprefix" opts
  let usecomma  = optionSet "juniorcomma" opts
  let bibtex    = optionSet "bibtex" opts
  let words' = wordsBy (\x -> x == Space || x == Str "\160")
  let commaParts = map words' $ Data.List.Split.splitWhen (== Str ",")
                              $ splitStrWhen (\c -> c == ',' || c == '\160') ils
  let (first, vonlast, jr) =
          case commaParts of
               --- First is the longest sequence of white-space separated
               -- words starting with an uppercase and that is not the
               -- whole string. von is the longest sequence of whitespace
               -- separated words whose last word starts with lower case
               -- and that is not the whole string.
               [fvl]      -> let (caps', rest') = span isCapitalized fvl
                             in  if null rest' && not (null caps')
                                 then (init caps', [last caps'], [])
                                 else (caps', rest', [])
               [vl,f]     -> (f, vl, [])
               (vl:j:f:_) -> (f, vl, j )
               []         -> ([], [], [])

  let (von, lastname) =
         if bibtex
            then case span isCapitalized $ reverse vonlast of
                        ([],w:ws) -> (reverse ws, [w])
                        (vs, ws)    -> (reverse ws, reverse vs)
            else case break isCapitalized vonlast of
                        (vs@(_:_), []) -> (init vs, [last vs])
                        (vs, ws)       -> (vs, ws)
  let prefix = Formatted $ intercalate [Space] von
  let family = Formatted $ intercalate [Space] lastname
  let suffix = Formatted $ intercalate [Space] jr
  let givens = map Formatted first

  return Agent
          { givenName       = givens
          , droppingPart    = if useprefix then mempty else prefix
          , nonDroppingPart = if useprefix then prefix else mempty
          , familyName      = family
          , nameSuffix      = suffix
          , literal         = mempty
          , commaSuffix     = usecomma
          , parseNames      = False
          }

isCapitalized :: [Inline] -> Bool
isCapitalized (Str (T.uncons -> Just (c,cs)) : rest)
  | isUpper c = True
  | isDigit c = isCapitalized (Str cs : rest)
  | otherwise = False
isCapitalized (_:rest) = isCapitalized rest
isCapitalized [] = True

optionSet :: Text -> Options -> Bool
optionSet key opts = case lookup key opts of
                      Just "true" -> True
                      Just s      -> s == mempty
                      _           -> False

latex' :: Text -> [Block]
latex' s = Walk.walk removeSoftBreak bs
  where Pandoc _ bs = readLaTeX s

removeSoftBreak :: Inline -> Inline
removeSoftBreak SoftBreak = Space
removeSoftBreak x         = x

latex :: Text -> Bib Formatted
latex s = blocksToFormatted $ latex' $ trim s

latexTitle :: Text -> Bib Formatted
latexTitle s = do
  utc <- gets untitlecase
  let processTitle = if utc then onBlocks unTitlecase else id
  blocksToFormatted $ processTitle $ latex' s

latexAuthors :: Options -> Text -> Bib [Agent]
latexAuthors opts = toAuthorList opts . latex' . trim

bib :: Bib Reference -> Item -> Maybe Reference
bib m entry = fst Control.Applicative.<$> evalRWST m entry (BibState True (Lang "en" "US"))

toLocale :: Text -> Text
toLocale "english"         = "en-US" -- "en-EN" unavailable in CSL
toLocale "usenglish"       = "en-US"
toLocale "american"        = "en-US"
toLocale "british"         = "en-GB"
toLocale "ukenglish"       = "en-GB"
toLocale "canadian"        = "en-US" -- "en-CA" unavailable in CSL
toLocale "australian"      = "en-GB" -- "en-AU" unavailable in CSL
toLocale "newzealand"      = "en-GB" -- "en-NZ" unavailable in CSL
toLocale "afrikaans"       = "af-ZA"
toLocale "arabic"          = "ar"
toLocale "basque"          = "eu"
toLocale "bulgarian"       = "bg-BG"
toLocale "catalan"         = "ca-AD"
toLocale "croatian"        = "hr-HR"
toLocale "czech"           = "cs-CZ"
toLocale "danish"          = "da-DK"
toLocale "dutch"           = "nl-NL"
toLocale "estonian"        = "et-EE"
toLocale "finnish"         = "fi-FI"
toLocale "canadien"        = "fr-CA"
toLocale "acadian"         = "fr-CA"
toLocale "french"          = "fr-FR"
toLocale "francais"        = "fr-FR"
toLocale "austrian"        = "de-AT"
toLocale "naustrian"       = "de-AT"
toLocale "german"          = "de-DE"
toLocale "germanb"         = "de-DE"
toLocale "ngerman"         = "de-DE"
toLocale "greek"           = "el-GR"
toLocale "polutonikogreek" = "el-GR"
toLocale "hebrew"          = "he-IL"
toLocale "hungarian"       = "hu-HU"
toLocale "icelandic"       = "is-IS"
toLocale "italian"         = "it-IT"
toLocale "japanese"        = "ja-JP"
toLocale "latvian"         = "lv-LV"
toLocale "lithuanian"      = "lt-LT"
toLocale "magyar"          = "hu-HU"
toLocale "mongolian"       = "mn-MN"
toLocale "norsk"           = "nb-NO"
toLocale "nynorsk"         = "nn-NO"
toLocale "farsi"           = "fa-IR"
toLocale "polish"          = "pl-PL"
toLocale "brazil"          = "pt-BR"
toLocale "brazilian"       = "pt-BR"
toLocale "portugues"       = "pt-PT"
toLocale "portuguese"      = "pt-PT"
toLocale "romanian"        = "ro-RO"
toLocale "russian"         = "ru-RU"
toLocale "serbian"         = "sr-RS"
toLocale "serbianc"        = "sr-RS"
toLocale "slovak"          = "sk-SK"
toLocale "slovene"         = "sl-SL"
toLocale "spanish"         = "es-ES"
toLocale "swedish"         = "sv-SE"
toLocale "thai"            = "th-TH"
toLocale "turkish"         = "tr-TR"
toLocale "ukrainian"       = "uk-UA"
toLocale "vietnamese"      = "vi-VN"
toLocale "latin"           = "la"
toLocale x                 = x

concatWith :: Char -> [Formatted] -> Formatted
concatWith sep = Formatted . foldl' go mempty . map unFormatted
  where go :: [Inline] -> [Inline] -> [Inline]
        go accum [] = accum
        go accum s  = case reverse accum of
                           []    -> s
                           (Str x:_)
                             | not (T.null x) && T.last x `elem` ("!?.,:;" :: String)
                                          -> accum ++ (Space : s)
                           _     -> accum ++ (Str (T.singleton sep) : Space : s)

type Options = [(Text, Text)]

parseOptions :: Text -> Options
parseOptions = map breakOpt . T.splitOn ","
  where breakOpt x = case T.break (=='=') x of
                          (w,v) -> (T.toLower $ trim w,
                                    T.toLower $ trim $ T.drop 1 v)

ordinalize :: Locale -> Text -> Text
ordinalize locale n =
  case [termSingular c | c <- terms, cslTerm c == ("ordinal-" <> pad0 n)] ++
       [termSingular c | c <- terms, cslTerm c == "ordinal"] of
       (suff:_) -> n <> suff
       []       -> n
    where pad0 s = case T.uncons s of
            Just (c,"") -> T.pack ['0', c]
            _           -> s
          terms = localeTerms locale

itemToReference :: Lang -> Locale -> Bool -> Bool -> Item -> Maybe Reference
itemToReference lang locale bibtex caseTransform = bib $ do
  modify $ \st -> st{ localeLanguage = lang,
                      untitlecase = case lang of
                                         Lang "en" _ -> caseTransform
                                         _           -> False }
  id' <- asks identifier
  otherIds <- (map trim . T.splitOn "," <$> getRawField "ids")
                <|> return []
  et <- asks entryType
  guard $ et /= "xdata"
  opts <- (parseOptions <$> getRawField "options") <|> return []
  let getAuthorList' = getAuthorList
         (("bibtex", T.toLower . T.pack $ show bibtex):opts)
  st <- getRawField "entrysubtype" <|> return mempty
  isEvent <- (True <$ (getRawField "eventdate"
                     <|> getRawField "eventtitle"
                     <|> getRawField "venue")) <|> return False
  reftype' <- resolveKey lang <$> getField "type" <|> return mempty
  let (reftype, refgenre) = case et of
       "article"
         | st == "magazine"  -> (ArticleMagazine,mempty)
         | st == "newspaper" -> (ArticleNewspaper,mempty)
         | otherwise         -> (ArticleJournal,mempty)
       "book"            -> (Book,mempty)
       "booklet"         -> (Pamphlet,mempty)
       "bookinbook"      -> (Chapter,mempty)
       "collection"      -> (Book,mempty)
       "dataset"         -> (Dataset,mempty)
       "electronic"      -> (Webpage,mempty)
       "inbook"          -> (Chapter,mempty)
       "incollection"    -> (Chapter,mempty)
       "inreference"     -> (EntryEncyclopedia,mempty)
       "inproceedings"   -> (PaperConference,mempty)
       "manual"          -> (Book,mempty)
       "mastersthesis"   -> (Thesis, if reftype' == mempty
                                        then Formatted [Str $ resolveKey' lang "mathesis"]
                                        else reftype')
       "misc"            -> (NoType,mempty)
       "mvbook"          -> (Book,mempty)
       "mvcollection"    -> (Book,mempty)
       "mvproceedings"   -> (Book,mempty)
       "mvreference"     -> (Book,mempty)
       "online"          -> (Webpage,mempty)
       "patent"          -> (Patent,mempty)
       "periodical"
         | st == "magazine"  -> (ArticleMagazine,mempty)
         | st == "newspaper" -> (ArticleNewspaper,mempty)
         | otherwise         -> (ArticleJournal,mempty)
       "phdthesis"       -> (Thesis, if reftype' == mempty
                                        then Formatted [Str $ resolveKey' lang "phdthesis"]
                                        else reftype')
       "proceedings"     -> (Book,mempty)
       "reference"       -> (Book,mempty)
       "report"          -> (Report,mempty)
       "software"        -> (Book,mempty)         -- no "software" type in CSL
       "suppbook"        -> (Chapter,mempty)
       "suppcollection"  -> (Chapter,mempty)
       "suppperiodical"
         | st == "magazine"  -> (ArticleMagazine,mempty)
         | st == "newspaper" -> (ArticleNewspaper,mempty)
         | otherwise         -> (ArticleJournal,mempty)
       "techreport"      -> (Report,mempty)
       "thesis"          -> (Thesis,mempty)
       "unpublished"     -> (if isEvent then Speech else Manuscript,mempty)
       "www"             -> (Webpage,mempty)
       -- biblatex, "unsupported"
       "artwork"         -> (Graphic,mempty)
       "audio"           -> (Song,mempty)         -- for audio *recordings*
       "commentary"      -> (Book,mempty)
       "image"           -> (Graphic,mempty)      -- or "figure" ?
       "jurisdiction"    -> (LegalCase,mempty)
       "legislation"     -> (Legislation,mempty)  -- or "bill" ?
       "legal"           -> (Treaty,mempty)
       "letter"          -> (PersonalCommunication,mempty)
       "movie"           -> (MotionPicture,mempty)
       "music"           -> (Song,mempty)         -- for musical *recordings*
       "performance"     -> (Speech,mempty)
       "review"          -> (Review,mempty)       -- or "review-book" ?
       "standard"        -> (Legislation,mempty)
       "video"           -> (MotionPicture,mempty)
       -- biblatex-apa:
       "data"            -> (Dataset,mempty)
       "letters"         -> (PersonalCommunication,mempty)
       "newsarticle"     -> (ArticleNewspaper,mempty)
       _                 -> (NoType,mempty)

  -- hyphenation:
  let defaultHyphenation = case lang of
                                Lang x y -> x <> "-" <> y
  let getLangId = do
           langid <- trim . T.toLower <$> getRawField "langid"
           idopts <- trim . T.toLower <$>
                         getRawField "langidopts" <|> return ""
           case (langid, idopts) of
                ("english","variant=british")    -> return "british"
                ("english","variant=american")   -> return "american"
                ("english","variant=us")         -> return "american"
                ("english","variant=usmax")      -> return "american"
                ("english","variant=uk")         -> return "british"
                ("english","variant=australian") -> return "australian"
                ("english","variant=newzealand") -> return "newzealand"
                (x,_)                            -> return x
  hyphenation <- (toLocale . T.toLower <$>
                   (getLangId <|> getRawField "hyphenation"))
                <|> return mempty

  -- authors:
  author' <- getAuthorList' "author" <|> return []
  containerAuthor' <- getAuthorList' "bookauthor" <|> return []
  translator' <- getAuthorList' "translator" <|> return []
  editortype <- getRawField "editortype" <|> return mempty
  editor'' <- getAuthorList' "editor" <|> return []
  director'' <- getAuthorList' "director" <|> return []
  let (editor', director') = case editortype of
                                  "director" -> ([], editor'')
                                  _          -> (editor'', director'')
  -- FIXME: add same for editora, editorb, editorc

  -- titles
  let isArticle = et `elem` ["article", "periodical", "suppperiodical", "review"]
  let isPeriodical = et == "periodical"
  let isChapterlike = et `elem`
         ["inbook","incollection","inproceedings","inreference","bookinbook"]
  hasMaintitle <- (True <$ getRawField "maintitle") <|> return False
  let hyphenation' = if T.null hyphenation
                     then defaultHyphenation
                     else hyphenation
  let la = case T.splitOn "-" hyphenation' of
                      (x:_) -> x
                      []    -> mempty
  modify $ \s -> s{ untitlecase = caseTransform && la == "en" }

  title' <- (guard isPeriodical >> getTitle "issuetitle")
            <|> (guard hasMaintitle >> guard (not isChapterlike) >> getTitle "maintitle")
            <|> getTitle "title"
            <|> return mempty
  subtitle' <- (guard isPeriodical >> getTitle "issuesubtitle")
               <|> (guard hasMaintitle >> guard (not isChapterlike) >> getTitle "mainsubtitle")
               <|> getTitle "subtitle"
               <|> return mempty
  titleaddon' <- (guard hasMaintitle >> guard (not isChapterlike) >> getTitle "maintitleaddon")
                 <|> getTitle "titleaddon"
                 <|> return mempty
  volumeTitle' <- (guard hasMaintitle >> guard (not isChapterlike) >> getTitle "title")
                  <|> (guard hasMaintitle >> guard isChapterlike >> getTitle "booktitle")
                  <|> return mempty
  volumeSubtitle' <- (guard hasMaintitle >> guard (not isChapterlike) >> getTitle "subtitle")
                     <|> (guard hasMaintitle >> guard isChapterlike >> getTitle "booksubtitle")
                     <|> return mempty
  volumeTitleAddon' <- (guard hasMaintitle >> guard (not isChapterlike) >> getTitle "titleaddon")
                       <|> (guard hasMaintitle >> guard isChapterlike >> getTitle "booktitleaddon")
                       <|> return mempty
  containerTitle' <- (guard isPeriodical >> getPeriodicalTitle "title")
                     <|> (guard isChapterlike >> getTitle "maintitle")
                     <|> (guard isChapterlike >> getTitle "booktitle")
                     <|> getPeriodicalTitle "journaltitle"
                     <|> getPeriodicalTitle "journal"
                     <|> return mempty
  containerSubtitle' <- (guard isPeriodical >> getPeriodicalTitle "subtitle")
                        <|> (guard isChapterlike >> getTitle "mainsubtitle")
                        <|> (guard isChapterlike >> getTitle "booksubtitle")
                        <|> getPeriodicalTitle "journalsubtitle"
                        <|> return mempty
  containerTitleAddon' <- (guard isPeriodical >> getPeriodicalTitle "titleaddon")
                          <|> (guard isChapterlike >> getTitle "maintitleaddon")
                          <|> (guard isChapterlike >> getTitle "booktitleaddon")
                          <|> return mempty
  containerTitleShort' <- (guard isPeriodical >> guard (not hasMaintitle)
                       >> getField "shorttitle")
                        <|> getPeriodicalTitle "shortjournal"
                        <|> return mempty
  -- change numerical series title to e.g. 'series 3'
  let fixSeriesTitle (Formatted [Str xs]) | T.all isDigit xs =
         Formatted [Str (ordinalize locale xs),
                    Space, Str (resolveKey' lang "ser.")]
      fixSeriesTitle x = x
  seriesTitle' <- fixSeriesTitle . resolveKey lang <$>
                      getTitle "series" <|> return mempty
  shortTitle' <- (guard (not hasMaintitle || isChapterlike) >>
                        getTitle "shorttitle")
               <|> if (subtitle' /= mempty || titleaddon' /= mempty) &&
                      not hasMaintitle
                      then getShortTitle False "title"
                      else getShortTitle True  "title"
               <|> return mempty

  eventTitle' <- getTitle "eventtitle" <|> return mempty
  origTitle' <- getTitle "origtitle" <|> return mempty

  -- publisher
  pubfields <- mapM (\f -> Just `fmap`
                       (if bibtex || f == "howpublished"
                        then getField f
                        else getLiteralList' f)
                      <|> return Nothing)
         ["school","institution","organization", "howpublished","publisher"]
  let publisher' = concatWith ';' $ catMaybes pubfields
  origpublisher' <- getField "origpublisher" <|> return mempty

-- places
  venue' <- getField "venue" <|> return mempty
  address' <- (if bibtex
               then getField "address"
               else getLiteralList' "address"
                     <|> (guard (et /= "patent") >>
                          getLiteralList' "location"))
              <|> return mempty
  origLocation' <- (if bibtex
                    then getField "origlocation"
                    else getLiteralList' "origlocation")
                  <|> return mempty
  jurisdiction' <- if et == "patent"
                   then (concatWith ';' . map (resolveKey lang) <$>
                           getLiteralList "location") <|> return mempty
                   else return mempty

  -- locators
  pages' <- getField "pages" <|> return mempty
  volume' <- getField "volume" <|> return mempty
  part' <- getField "part" <|> return mempty
  volumes' <- getField "volumes" <|> return mempty
  pagetotal' <- getField "pagetotal" <|> return mempty
  chapter' <- getField "chapter" <|> return mempty
  edition' <- getField "edition" <|> return mempty
  version' <- getField "version" <|> return mempty
  (number', collectionNumber', issue') <-
     (getField "number" <|> return mempty) >>= \x ->
       if et `elem` ["book","collection","proceedings","reference",
                     "mvbook","mvcollection","mvproceedings", "mvreference",
                     "bookinbook","inbook", "incollection","inproceedings",
                     "inreference", "suppbook","suppcollection"]
       then return (mempty,x,mempty)
       else if isArticle
            then (getField "issue" >>= \y ->
                                    return (mempty,mempty,concatWith ',' [x,y]))
               <|> return (mempty,mempty,x)
            else return (x,mempty,mempty)

  -- dates
  issued' <- getDates "date" <|> getOldDates mempty <|> return []
  eventDate' <- getDates "eventdate" <|> getOldDates "event"
              <|> return []
  origDate' <- getDates "origdate" <|> getOldDates "orig"
              <|> return []
  accessed' <- getDates "urldate" <|> getOldDates "url" <|> return []

  -- url, doi, isbn, etc.:
  -- note that with eprinttype = arxiv, we take eprint to be a partial url
  -- archivePrefix is an alias for eprinttype
  url' <- (guard (et == "online" || lookup "url" opts /= Just "false")
           >> getRawField "url")
       <|> (do etype <- getRawField "eprinttype"
               eprint <- getRawField "eprint"
               let baseUrl =
                     case T.toLower etype of
                       "arxiv"       -> "http://arxiv.org/abs/"
                       "jstor"       -> "http://www.jstor.org/stable/"
                       "pubmed"      -> "http://www.ncbi.nlm.nih.gov/pubmed/"
                       "googlebooks" -> "http://books.google.com?id="
                       _             -> ""
               if T.null baseUrl
                  then mzero
                  else return $ baseUrl <> eprint)
       <|> return mempty
  doi' <- (guard (lookup "doi" opts /= Just "false") >> getRawField "doi")
         <|> return mempty
  isbn' <- getRawField "isbn" <|> return mempty
  issn' <- getRawField "issn" <|> return mempty
  pmid' <- getRawField "pmid" <|> return mempty
  pmcid' <- getRawField "pmcid" <|> return mempty
  callNumber' <- getRawField "library" <|> return mempty

  -- notes
  annotation' <- getField "annotation" <|> getField "annote"
                   <|> return mempty
  abstract' <- getField "abstract" <|> return mempty
  keywords' <- getField "keywords" <|> return mempty
  note' <- if et == "periodical"
           then return mempty
           else getField "note" <|> return mempty
  addendum' <- if bibtex
               then return mempty
               else getField "addendum"
                 <|> return mempty
  pubstate' <- resolveKey lang `fmap`
                 (  getField "pubstate"
                <|> case issued' of
                         (x:_) | other x == Literal "forthcoming" ->
                                     return (Formatted [Str "forthcoming"])
                         _ -> return mempty
                 )

  let convertEnDash (Str s) = Str (T.map (\c -> if c == '–' then '-' else c) s)
      convertEnDash x       = x

  let takeDigits (Str xs : _) =
         case T.takeWhile isDigit xs of
              "" -> []
              ds -> [Str ds]
      takeDigits x             = x

  return $ emptyReference
         { refId               = Literal id'
         , refOtherIds         = map Literal otherIds
         , refType             = reftype
         , author              = author'
         , editor              = editor'
         , translator          = translator'
         -- , recipient           = undefined -- :: [Agent]
         -- , interviewer         = undefined -- :: [Agent]
         -- , composer            = undefined -- :: [Agent]
         , director            = director'
         -- , illustrator         = undefined -- :: [Agent]
         -- , originalAuthor      = undefined -- :: [Agent]
         , containerAuthor     = containerAuthor'
         -- , collectionEditor    = undefined -- :: [Agent]
         -- , editorialDirector   = undefined -- :: [Agent]
         -- , reviewedAuthor      = undefined -- :: [Agent]

         , issued              = issued'
         , eventDate           = eventDate'
         , accessed            = accessed'
         -- , container           = undefined -- :: [RefDate]
         , originalDate        = origDate'
         -- , submitted           = undefined -- :: [RefDate]
         , title               = concatWith '.' [
                                    concatWith ':' [title', subtitle']
                                  , titleaddon' ]
         , titleShort          = shortTitle'
         -- , reviewedTitle       = undefined -- :: String
         , containerTitle      = concatWith '.' [
                                      concatWith ':' [ containerTitle'
                                                     , containerSubtitle']
                                    , containerTitleAddon' ]
         , collectionTitle     = seriesTitle'
         , volumeTitle         = concatWith '.' [
                                      concatWith ':' [ volumeTitle'
                                                     , volumeSubtitle']
                                    , volumeTitleAddon' ]
         , containerTitleShort = containerTitleShort'
         , collectionNumber    = collectionNumber'
         , originalTitle       = origTitle'
         , publisher           = publisher'
         , originalPublisher   = origpublisher'
         , publisherPlace      = address'
         , originalPublisherPlace = origLocation'
         , jurisdiction        = jurisdiction'
         , event               = eventTitle'
         , eventPlace          = venue'
         , page                = Formatted $
                                 Walk.walk convertEnDash $ unFormatted pages'
         , pageFirst           = Formatted $ takeDigits $ unFormatted pages'
         , numberOfPages       = pagetotal'
         , version             = version'
         , volume              = Formatted $ intercalate [Str "."]
                                 $ filter (not . null)
                                 [unFormatted volume', unFormatted part']
         , numberOfVolumes     = volumes'
         , issue               = issue'
         , chapterNumber       = chapter'
         -- , medium              = undefined -- :: String
         , status              = pubstate'
         , edition             = edition'
         -- , section             = undefined -- :: String
         -- , source              = undefined -- :: String
         , genre               = if refgenre == mempty
                                    then reftype'
                                    else refgenre
         , note                = concatWith '.' [note', addendum']
         , annote              = annotation'
         , abstract            = abstract'
         , keyword             = keywords'
         , number              = number'
         , url                 = Literal url'
         , doi                 = Literal doi'
         , isbn                = Literal isbn'
         , issn                = Literal issn'
         , pmcid               = Literal pmcid'
         , pmid                = Literal pmid'
         , language            = Literal hyphenation
         , callNumber          = Literal callNumber'
         }