-- compile with: ghc -Wall -O2 --make footnotes.hs

module Main (main, doFiles, putFootnotes) where

import System.Environment (getArgs)
import System.Console.GetOpt

import Control.Exception (Exception, catchJust)
import Control.Monad (foldM, guard)
import Data.Array.IArray (Array, array, (!))
import qualified Data.ByteString.Lazy.Char8 as B
import qualified Data.IntMap as M
import Data.List (isSuffixOf)
import Data.Maybe (mapMaybe)

data Flag = Appearance deriving (Show, Eq)
options :: [OptDescr Flag]
options = [Option ['a'] ["reorder-by-appearance"] (NoArg Appearance)
                     "Reorder footnotes by order of appearence"]

-- main: process the command-line options for footnotes.
main :: IO ()
main = do
 args <- getArgs
 case getOpt Permute options args of
   (opts, files, []) ->
       doFiles files $ Appearance `elem` opts
   (_, _, errors)       ->
       ioError $ userError $ concat errors ++ usageInfo header options
           where header = "Usage: footnotes [OPTION...] [files...]"

-- doFiles: read input either from given files or from stdin,
-- if none were given, and print the output to stdout.
doFiles :: [FilePath] -> Bool -> IO ()
doFiles []    useBody = B.getContents >>= putFootnotes useBody
doFiles files useBody = mapM_ doFile files where
   doFile file = B.readFile file >>= putFootnotes useBody

type RefsMap      = M.IntMap Int     -- maps old to new refs.
data RefsMapState = RMS !RefsMap Int -- keeps track of map and counter.
type FootsMap     = Array Int (IO ()) -- sorted footnotes.

sepLine :: B.ByteString         -- the seperator line.
sepLine = B.pack "@footnote:"

emptyRMS :: RefsMapState        -- an empty RefsMap and its counter.
emptyRMS = RMS M.empty 0

-- putFootnotes: print s with renumbered footnotes.
putFootnotes :: Bool -> B.ByteString -> IO ()
putFootnotes True  s = do       -- by order of appearance in body.
 let (body, _ : foots) = break (== sepLine) $ B.lines s
 (RMS m n) <- foldM (putLineSubRefs putNewRef) emptyRMS body
 B.putStrLn sepLine
 let fm = array (1, n) $ mapMaybe (subRef m) foots
 mapM_ (lookupPutFoot fm) [1..n]

putFootnotes False s = do       -- by order of the footnotes.
 let (_ : foots) = dropWhile (/= sepLine) $ B.lines s
 let st@(RMS m _) = foldl insertFootRef emptyRMS foots
 let (body, _ : foots') = break (== sepLine) $ B.lines s
 mapM_ (putLineSubRefs putUnknownRef st) body
 B.putStrLn sepLine
 mapM_ (\l -> maybe (B.putStrLn l) snd $ subRef m l) foots'

-- putLineSubRefs: print body line l with substituted references,
-- evaluate doUnknownRef for references not in m, return new state.
putLineSubRefs :: (Int -> RefsMapState -> IO RefsMapState)
              -> RefsMapState -> B.ByteString -> IO RefsMapState
putLineSubRefs doUnknownRef st@(RMS m _) l =
   case B.elemIndex '[' l of
     Nothing -> B.putStrLn l >> return st
     Just i  -> do
       let (before, after) = B.splitAt (i + 1) l
       B.putStr before
       let parse = do (n, rest) <- B.readInt after
                      (']',  _) <- B.uncons rest
                      return (n, rest)
       case parse of
         Nothing        ->
             putLineSubRefs doUnknownRef st after
         Just (n, rest) ->
             case M.lookup n m of
               Just newN -> do putStr $ show newN
                               putLineSubRefs doUnknownRef st rest
               Nothing   -> do newSt <- doUnknownRef n st
                               putLineSubRefs doUnknownRef newSt rest

-- insertRef: return new RefsMapState with oldN inserted.
insertRef :: RefsMapState -> Int -> RefsMapState
insertRef (RMS m n) oldN = RMS (M.insert oldN n' m) n' where n' = n+1

-- subRef: substitute the reference in footnote line l according to m,
-- return its number and an IO action for printing the new l.
subRef :: RefsMap -> B.ByteString -> Maybe (Int, IO ())
subRef m l = do (oldN, rest) <- parseFoot l
               n <- M.lookup oldN m
               return (n, putStr ('[' : show n) >> B.putStrLn rest)

-- parseFoot: return number and the rest after it from footnote line l.
parseFoot :: B.ByteString -> Maybe (Int, B.ByteString)
parseFoot l = do ('[', l') <- B.uncons l
                (n, rest) <- B.readInt l'
                (']', _)  <- B.uncons rest
                return (n, rest)

-- putNewRef :: print new ref for oldN and return new RefsMapState.
putNewRef :: Int -> RefsMapState -> IO RefsMapState
putNewRef oldN st = putStr (show n) >> return newSt
   where newSt@(RMS _ n) = insertRef st oldN

-- lookupPutFoot: return the IO action for footnote number n in fm.
lookupPutFoot :: FootsMap -> Int -> IO ()
lookupPutFoot fm n = catchJust undefElements (fm ! n) $
                    \_ -> do putStr ('[' : show n)
                             putStrLn "] ### missing footnote ###"

-- undefElements: exception predicate for undefined array elements.
undefElements :: Exception -> Maybe ()
undefElements e = guard $ "undefined array element" `isSuffixOf` show e

-- insertFootRef: insert the ref from footnote line l into st.
insertFootRef :: RefsMapState -> B.ByteString -> RefsMapState
insertFootRef st l = maybe st (insertRef st . fst) $ parseFoot l

-- putUnknownRef: print dummy for a reference without a footnote.
putUnknownRef :: Int -> RefsMapState -> IO RefsMapState
putUnknownRef _ st = putStr "?" >> return st