module Language.LaTeX.Printer where

import Data.Monoid
import Data.List (intersperse)
import Data.Ratio (numerator, denominator)
import Data.Foldable (foldMap)
import Data.Char
import GHC.Float (formatRealFloat, FFFormat(FFFixed))

import Language.LaTeX.Types
import Language.LaTeX.Checker (checkDocument)
import Language.LaTeX.Builder.MonoidUtils

optionals :: [a] -> Arg a
optionals [] = NoArg
optionals xs = Optional xs

text :: String -> ShowS
text = showString

between :: String -> String -> ShowS -> ShowS
between opening closing x = text opening  x  text closing


braces, brackets, parens :: ShowS -> ShowS
braces   = between "{" "}"
brackets = between "[" "]"
parens   = between "(" ")"


nl, nl2, irrNl, backslash, sp :: ShowS
backslash = text "\\"
nl = text "\n"
nl2 = nl  nl
sp = text " "
irrNl = text "%\n"

($$) :: ShowS -> ShowS -> ShowS
($$) x y = x  nl  y

($$$) :: ShowS -> ShowS -> ShowS
($$$) x y = x  nl2  y

vcat, vcat2 :: [ShowS] -> ShowS
vcat  = mconcat . intersperse nl
vcat2 = mconcat . intersperse nl2

ppNamed :: Named ShowS -> ShowS
ppNamed (Named name val)
  | null name = val
  | otherwise = text name  text "="  val

commas :: [ShowS] -> ShowS
commas = mconcat . intersperse (text ",")

ppArg :: Arg ShowS -> ShowS
ppArg NoArg             = id
ppArg StarArg           = text "*"
ppArg (Optional [])     = error "ppArg: impossible: Optional []"
ppArg (NamedOpts [])    = error "ppArg: impossible: NamedOpts []"
ppArg (Mandatory xs)    = braces   . commas $ xs
ppArg (Optional xs)     = brackets . commas $ xs
ppArg (NamedArgs xs)    = braces   . commas . map ppNamed $ xs
ppArg (NamedOpts xs)    = brackets . commas . map ppNamed $ xs
ppArg (Coordinates x y) = parens (x  text " "  y)
ppArg (LiftArg x)       = x
ppArg (RawArg x)        = text x
ppArg (PackageAction _) = id

ppEnv :: String -> [Arg ShowS] -> ShowS -> ShowS
ppEnv envName args contents =
  backslash  begin  braces envNameS  mconcat (map ppArg args)
 $$
  contents
 $$
  backslash  end  braces envNameS

  where envNameS = text envName
        begin    = text "begin"
        end      = text "end"

-- these are not wrapped by braces
ppCmdArgs :: String -> [Arg ShowS] -> ShowS
ppCmdArgs cmdName args = backslash  text cmdName  mconcat (map ppArg args)

ppDecl :: String -> ShowS -> ShowS
ppDecl declName declArgs = backslash  text declName  declArgs  text " " -- or {}

ppTexDecl :: TexDcl -> ShowS
ppTexDecl (TexDcl declName declArgs) = ppDecl declName (foldMap (ppArg . fmap ppAny) declArgs)

ppMathDecl :: MathDcl -> ShowS
ppMathDecl (MathDcl declName) = ppDecl declName ø

pp :: LatexItm -> ShowS
pp (LatexCmdArgs cmdName args) = ppCmdArgs cmdName $ map (fmap pp) args
pp (LatexCmdAnyArgs cmdName args) = ppCmdArgs cmdName $ map (fmap ppAny) args
pp (TexDecls decls) = foldMap ppTexDecl decls
pp (TexCmdNoArg cmdName) = braces $ ppCmdArgs cmdName []
pp (TexCmdArg cmdName contents)
 = braces (backslash  text cmdName  text " "  pp contents)
pp (Environment envName args contents) = ppEnv envName (map (fmap ppAny) args) $ ppAny contents
pp (RawTex s) = text s
-- One produces $...$ since \(...\) is ``fragile''
pp (LatexCast (MathItm m)) = text "$ "  ppMath m  text " $"
pp (LatexCast x) = ppAny x
pp (TexGroup t) = braces $ pp t
pp LatexEmpty = ø
pp (LatexAppend x y) = pp x  pp y
pp (LatexNote key note t) = ppNote key note pp t

ppParMode :: ParItm -> ShowS
ppParMode (ParCast (MathItm m)) = text "\\[ "  ppMath m  text " \\]"
ppParMode (ParCast t) = ppAny t
ppParMode (ParCmdArgs cmdName args) = ppCmdArgs cmdName $ map (fmap ppAny) args
ppParMode (RawParMode x) = text x
ppParMode (ParGroup p) = braces $ ppParMode p
ppParMode (ParEnv envName args contents)
  = ppEnv envName (map (fmap ppAny) args) $ ppAny contents
ppParMode (Tabular specs rows) =
  ppEnv "tabular" [Mandatory . (:[]) . mconcat $ map (ppRowSpec . fmap pp) specs] (ppRows pp rows)
ppParMode (ParConcat contents) = vcat2 $ map ppParMode contents
ppParMode (ParNote key note t) = ppNote key note ppParMode t

ppMath :: MathItm -> ShowS
ppMath (MathDecls decls) = foldMap ppMathDecl decls
ppMath (MathCmdArgs cmdName args) = ppCmdArgs cmdName $ map (fmap ppAny) args
ppMath (RawMath s) = text s
ppMath (MathCast x) = ppAny x
ppMath (MathRat r) | denominator r == 1 = shows (numerator r)
                     | otherwise          = shows (numerator r)  text " / "  shows (denominator r)
ppMath (MathArray specs rows) = 
  ppEnv "array" [Mandatory . (:[]) . mconcat $ map (ppRowSpec . fmap ppMath) specs] (ppRows ppMath rows)
ppMath (MathGroup m) = braces $ ppMath m
ppMath (MathConcat ms) = mconcat $ map ppMath ms
ppMath (MathUnOp op m) = text op  sp  ppMath m
ppMath (MathBinOp op l r) = parens (ppMath l  sp  text op  sp  ppMath r)
ppMath (MathNote key note m) = ppNote key note ppMath m

ppAny :: AnyItm -> ShowS
ppAny (PreambleItm x) = ppPreamble x
ppAny (LatexItm    x) = pp x
ppAny (MathItm     x) = ppMath x
ppAny (ParItm      x) = ppParMode x
ppAny (LocSpecs locs) = text . map locSpecChar $ locs
ppAny (Key key)       = text . getKey $ key
ppAny (Length len)    = ppTexLength len
ppAny (Coord (MkCoord x y)) = ppTexLength x  text " "  ppTexLength y
ppAny (SaveBin bin) = text $ "\\hlatexSaveBin" ++ (map enc . show $ unsafeGetSaveBin bin)
  where enc i = chr (ord 'a' + digitToInt i) -- hackish but numbers are prohibited
ppAny (PackageName pkg) = text $ getPkgName pkg

ppRowSpec :: RowSpec ShowS -> ShowS
ppRowSpec Rc        = text "c"
ppRowSpec Rl        = text "l"
ppRowSpec Rr        = text "r"
ppRowSpec Rvline    = text "|"
ppRowSpec (Rtext x) = text "@"  braces x


ppRows :: (a -> ShowS) -> [Row a] -> ShowS
ppRows _ []
  = ø
ppRows ppCell (Cells cells : rows)
  = (mconcat . intersperse (text " & ") . map ppCell $ cells)
  (if null rows then ø else backslash  backslash $$ ppRows ppCell rows)
ppRows ppCell (Hline : rows)
  = backslash  text "hline "  ppRows ppCell rows
ppRows ppCell (Cline c1 c2 : rows)
  -- No braces here around the \cline, intentionally
  = ppCmdArgs "cline" [Mandatory . (:[]) . text $ show c1 ++ "-" ++ show c2]  ppRows ppCell rows

unitName :: TexUnit -> String
unitName u =
  case u of
    Cm -> "cm"
    Mm -> "mm"
    Em -> "em"
    Ex -> "ex"
    Pt -> "pt"
    Pc -> "pc"
    In -> "in"
    Sp -> "sp"
    Bp -> "bp"
    Dd -> "dd"
    Cc -> "cc"
    Mu -> "mu"

ppTexLength :: LatexLength -> ShowS
ppTexLength s =
  case s of
    LengthCmd cmd          -> ppCmdArgs cmd []
    LengthCmdRatArg  cmd r -> braces $ ppCmdArgs cmd [Mandatory . (:[]) $ showr r]
    LengthScaledBy _ (LengthScaledBy _ _) ->
      error "broken invariant: nested LengthScaledBy"
    LengthScaledBy r l     -> showr r  ppTexLength l
    LengthCst munit r      -> showr r  foldMap (text . unitName) munit
  where showr r | denominator r == 1 = shows $ numerator r
                | otherwise          = text $ formatRealFloat FFFixed (Just 2) (fromRational r :: Double)

ppPreamble :: PreambleItm -> ShowS
ppPreamble (PreambleCmdArgs cmdName args) = ppCmdArgs cmdName $ map (fmap ppAny) args
ppPreamble (PreambleEnv envName args contents) = ppEnv envName (map (fmap ppAny) args) (ppAny contents)
ppPreamble (PreambleCast x) = ppAny x
ppPreamble (PreambleConcat ps) = vcat $ map ppPreamble ps
ppPreamble (RawPreamble raw) = text raw
ppPreamble (PreambleNote key note p) = ppNote key note ppPreamble p

ppNote :: Key -> Note -> (a -> ShowS) -> a -> ShowS
ppNote (MkKey key) note ppElt elt = irrNl  comment (key  ": "  showNote note)  ppElt elt  irrNl
  where showNote (TextNote s) = s
        showNote (IntNote  i) = show i
        showNote (LocNote  loc) = showLoc loc
        comment = mconcat . map (text . ('%':) . ( "\n") . stripRight) . lines
        stripRight = reverse . dropWhile isSpace . reverse

showLoc :: Loc -> String
showLoc (Loc fp line char) = unwords [fp, ":", show line, ":", show char]

showDocClassKind :: DocumentClassKind -> String
showDocClassKind Article = "article"
showDocClassKind Book    = "book"
showDocClassKind Report  = "report"
showDocClassKind Letter  = "letter"
showDocClassKind (OtherDocumentClassKind x) = x

preambOfDocClass :: DocumentClss -> PreambleItm
preambOfDocClass (DocClass kind opts) =
  PreambleCmdArgs "documentclass"
    [optionals opts, Mandatory . (:[]) . LatexItm . RawTex $ showDocClassKind kind]

ppDocument :: Document -> ShowS
ppDocument (Document docClass preamb doc) =
  ppPreamble (preambOfDocClass docClass) $$$
  ppPreamble preamb $$$
  ppEnv "document" [] (nl  ppParMode doc  nl)  nl

showsLaTeX :: LatexM Document -> Either ErrorMessage ShowS
showsLaTeX mdoc = do
  doc <- runLatexM mdoc
  maybe (return ()) Left $ checkDocument doc
  return . ppDocument $ doc

showLaTeX :: LatexM Document -> Either ErrorMessage String
showLaTeX = fmap ($"") . showsLaTeX