module Docvim.Printer.Vim (vimHelp) where

import Control.Arrow ((***))
import Control.Monad (join)
import Control.Monad.Reader
import Control.Monad.State
import Data.Char (isSpace, toLower, toUpper)
import Data.List (intercalate, isSuffixOf, span, sort)
import Data.List.Split (splitOn)
import Data.Maybe (fromJust, fromMaybe)
import Data.Tuple (swap)
import Docvim.AST
import Docvim.Parse (rstrip)
import Docvim.Visitor.Plugin (getPluginName)
import Docvim.Visitor.Symbol (getSymbols)

-- TODO: add indentation here (using local, or just stick it in Context)

-- Instead of building up a [Char], we build up a list of operations, which
-- allows us a mechanism of implementing rollback and therefore hard-wrapping
-- (eg. append whitespace " ", then on next node, realize that we will exceed
-- line length limit, so rollback the " " and instead append "\n" etc).
data Operation = Append String
               | Delete Int -- unconditional delete count of Char
               | Slurp String -- delete string if present
data Metadata = Metadata { symbols :: [String]
                         , pluginName :: Maybe String
                         }
data Context = Context { lineBreak :: String
                       , partialLine :: String
                       }
type Env = ReaderT Metadata (State Context) [Operation]

textwidth :: Int
textwidth = 78

vimHelp :: Node -> String
vimHelp n = suppressTrailingWhitespace output ++ "\n"
  where metadata = Metadata (getSymbols n) (getPluginName n)
        context = Context defaultLineBreak ""
        operations = evalState (runReaderT (node n) metadata) context
        output = foldl reduce "" operations
        reduce acc (Append atom) = acc ++ atom
        reduce acc (Delete count) = take (length acc - count) acc
        reduce acc (Slurp atom) = if atom `isSuffixOf` acc
                                  then take (length acc - length atom) acc
                                  else acc
        suppressTrailingWhitespace str = rstrip $ intercalate "\n" (map rstrip (splitOn "\n" str))

-- | Helper function that appends and updates `partialLine` context,
-- hard-wrapping if necessary to remain under `textwidth`.
append :: String -> Env
append string = append' string textwidth

-- | Helper function that appends and updates `partialLine` context
-- uncontitionally (no hard-wrapping).
appendNoWrap :: String -> Env
appendNoWrap string = append' string (maxBound :: Int)

append' :: String -> Int -> Env
append' string width = do
  context <- get
  -- TODO obviously tidy this up
  let (ops, line) = if renderedWidth (partialLine context) + renderedWidth leading >= width
                    then ( [ Delete (length $ snd $ hardwrap $ partialLine context)
                           , Slurp " "
                           , Append (lineBreak context)
                           , Append (snd $ hardwrap $ partialLine context)
                           , Append string
                           ]
                         , lineBreak context ++ snd (hardwrap $ partialLine context) ++ string
                         )
                    else ([Append string], partialLine context ++ string)
  put (Context (lineBreak context) (end line))
  return ops
  where
    leading = takeWhile (/= '\n') string
    trailing str = length $ takeWhile isSpace (reverse str)
    end l = reverse $ takeWhile (/= '\n') (reverse l)

-- http://stackoverflow.com/a/9723976/2103996
mapTuple = join (***)

-- Given a string, hardwraps it into two parts by splitting it at the rightmost
-- whitespace.
hardwrap :: String -> (String, String)
hardwrap str = swap $ mapTuple reverse split
  where
    split = break isSpace (reverse str)

-- Helper function that deletes `count` elements from the end of the
--`partialLine` context.
delete :: Int -> Env
delete count = do
  context <- get
  put (Context (lineBreak context) (partial context))
  return [Delete count]
  where
    partial context = take (length (partialLine context) - count) (partialLine context)

-- Helper function to conditionally remove a string if it appears at the end of
-- the output.
slurp :: String -> Env
slurp str = do
  context <- get
  put (Context (lineBreak context) (partial context))
  return [Slurp str]
  where
    -- eg. (partialLine context) | str        | result
    --     ----------------------|------------|-------
    --     ""                    | "\n"       | ""
    --     "foo"                 | "\n"       | "foo"
    --     "foo"                 | "bar"      | "foo"
    --     "abc"                 | "bc"       | "a"
    --     "abc"                 | "foo\nabc" | ""
    --
    -- Note: That last one is unsafe, because we can't guarantee that "foo" is
    -- there. Caveat emptor!
    partial context = if str `isSuffixOf` partialLine context
                      then take (length (partialLine context) - length str) (partialLine context)
                      else partialLine context

defaultLineBreak :: String
defaultLineBreak = "\n"

nodes :: [Node] -> Env
nodes ns = concat <$> mapM node ns

node :: Node -> Env
node n = case n of
  Blockquote b               -> blockquote b >>= nl >>= nl
  BreakTag                   -> breaktag
  Code c                     -> append $ "`" ++ c ++ "`"
  CommandAnnotation {}       -> command n
  CommandsAnnotation         -> heading "commands"
  DocBlock d                 -> nodes d
  Fenced f                   -> fenced f
  FunctionsAnnotation        -> heading "functions"
  FunctionDeclaration {}     -> nodes $ functionBody n
  HeadingAnnotation h        -> heading h
  Link l                     -> append $ link l
  LinkTargets l              -> linkTargets l True
  List ls                    -> nodes ls >>= nl
  ListItem l                 -> listitem l
  MappingAnnotation m        -> mapping m
  MappingsAnnotation         -> heading "mappings"
  OptionAnnotation {}        -> option n
  OptionsAnnotation          -> heading "options"
  Paragraph p                -> nodes p >>= nl >>= nl
  Plaintext p                -> plaintext p
  PluginAnnotation name desc -> plugin name desc
  Project p                  -> nodes p
  Separator                  -> append $ "---" ++ "\n\n"
  SubheadingAnnotation s     -> append $ s ++ " ~\n\n"
  TOC t                      -> toc t
  Unit u                     -> nodes u
  Whitespace                 -> whitespace
  _                          -> append ""

-- TODO: add {name}.txt to the symbol table?
plugin :: String -> String -> Env
plugin name desc = append $
  "*" ++ name ++ ".txt*" ++
  "    " ++ desc ++ "      " ++
  "*" ++ name ++ "*" ++ "\n\n"

-- | Append a newline.
nl :: [Operation] -> Env
nl os = liftM2 (++) (return os) (append "\n")

breaktag :: Env
breaktag = do
  state <- get
  append $ lineBreak state

listitem :: [Node] -> Env
listitem l = do
  context <- get
  -- TODO: consider using lenses to modify records
  put (Context customLineBreak (partialLine context))
  item <- liftM2 (++) (append "- ") (nodes l) >>= nl
  put (Context defaultLineBreak (partialLine context))
  return item
  where
    customLineBreak = "\n  "

toc :: [String] -> Env
toc t = do
  metadata <- ask
  toc' $ fromJust $ pluginName metadata
  where
    toc' p = do
      h <- heading "contents"
      entries <- append $ intercalate "\n" format ++ "\n\n"
      return (h ++ entries)
      where
        format                = map pad numbered
        longest               = maximum (map (length . snd) numbered )
        numbered              = map prefix number
        number                = zip3 [1..] t (map (\x -> normalize $ p ++ "-" ++ x) t)
        prefix (num, desc, l) = (show num ++ ". " ++ desc ++ "  ", l)
        pad (lhs, rhs)        = lhs ++ replicate (longest - length lhs) ' ' ++ link rhs
  -- TODO: consider doing this for markdown format too

command :: Node -> Env
command (CommandAnnotation name params) = do
  lhs <- append $ concat [":", name, " ", fromMaybe "" params]
  ws <- append " "
  target <- linkTargets [":" ++ name] False
  trailing <- append "\n"
  return $ concat [lhs, ws, target, trailing]
-- TODO indent what follows until next annotation...
-- will require us to hoist it up inside CommandAnnotation
-- (and do similar for other sections)
-- once that is done, drop the extra newline above

mapping :: String -> Env
mapping name = linkTargets [name] True

option :: Node -> Env
option (OptionAnnotation n t d) = do
  targets <- linkTargets [n] True
  opt <- appendNoWrap $ link n
  ws <- appendNoWrap " "
  context <- get
  meta <- appendNoWrap $ aligned context
  return $ concat [targets, opt, ws, meta]
  where
    aligned context = rightAlign context rhs
    rhs = t ++ " (default: " ++ fromMaybe "none" d ++ ")\n\n"

whitespace :: Env
whitespace = append " "

blockquote :: [Node] -> Env
blockquote ps = do
  context <- get
  put (Context customLineBreak (partialLine context))
  ps' <- mapM paragraph ps
  put (Context defaultLineBreak (partialLine context))
  liftM2 (++) (append "    ") (liftM2 intercalate customParagraphBreak (return ps'))
  where
    -- Strip off trailing newlines from each paragraph.
    paragraph p = fmap trim (node p)
    trim contents = take (length contents - 2) contents
    customLineBreak = "\n    "
    customParagraphBreak = append "\n\n    "

plaintext :: String -> Env
plaintext = append

fenced :: [String] -> Env
fenced f = do
  cut <- slurp "\n"
  prefix <- append ">\n"
  body <- if null f
          then append ""
          else appendNoWrap $ "    " ++ intercalate "\n    " f ++ "\n"
  suffix <- append "<\n"
  return $ concat [cut, prefix, body, suffix]

heading :: String -> Env
heading h = do
  metadata <- ask
  heading' <- appendNoWrap $ map toUpper h ++ " "
  target <- maybe (append "\n") (\x -> linkTargets [target x] False) (pluginName metadata)
  trailing <- append "\n"
  return $ concat [heading', target, trailing]
  where
    target x = normalize $ x ++ "-" ++ h

normalize :: String -> String
normalize = map (toLower . sanitize)

sanitize :: Char -> Char
sanitize x = if isSpace x then '-' else x

link :: String -> String
link l = "|" ++ l ++ "|"

-- TODO: be prepared to wrap these if there are a lot of them
-- TODO: fix code smell of passing in `wrap` bool here
linkTargets :: [String] -> Bool -> Env
linkTargets ls wrap = do
  context <- get
  if wrap
  then append $ aligned context
  else appendNoWrap $ aligned context
  where
    aligned context = rightAlign context (targets ++ "\n")
    targets = unwords (map linkify $ sort ls)
    linkify l = "*" ++ l ++ "*"

rightAlign :: Context -> String -> String
rightAlign context string = align (partialLine context)
  where
    align used = replicate (count used string) ' ' ++ string
    count used xs = maximum [textwidth - renderedWidth xs - renderedWidth used, 0]

-- Crude approximation for calculating rendered width, that does so by not
-- counting the relatively rare |, *, ` and "\n" -- all of which usually get
-- concealed in the rendered output.
renderedWidth :: String -> Int
renderedWidth = foldr reduce 0
  where reduce char acc = if char `elem` "\n|*`"
                        then acc
                        else acc + 1