{-# OPTIONS_GHC -fno-warn-unused-do-bind #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE NoMonomorphismRestriction #-}

module Parser where

import           Control.Applicative
import           Control.Monad.Fix
import           Control.Monad.RWS
import           Data.Char
import           Data.CharSet            hiding ( map )
import           Data.Maybe
import qualified Data.Set                      as S
import           Lens.Micro.Platform
import           Parser.Types
import           Text.Parsec             hiding ( many )
import           Text.ParserCombinators.ReadP   ( readP_to_S )
import           Text.Read.Lex                  ( lexChar )

type Warning = String

parseStr :: String -> Either ParseError ([Atom], [[Warning]])
parseStr = fmap (unzip . map normalizeAndWarn) . parse printfStr "" . lexChars
 where
  lexChars x = (`fix` x) $ \f s -> if Prelude.null s
    then []
    else case readP_to_S lexChar s of
      ((c, rest) : _) -> c : f rest
      []              -> error "malformed input"

normalizeAndWarn :: Atom -> (Atom, [Warning])
normalizeAndWarn s@Str{} = (s, [])
normalizeAndWarn (Arg f) = (Arg a, b)
 where
  (_, a, b) = runRWS (warnLength f >> go (spec f)) () f
  go c | c `elem` "aAeEfFgGxXo" = return ()
  go c | c `elem` "csqQ?" = warnSign >> warnPrefix >> warnZero >> warnSpace
  go c | c `elem` "diu"         = warnPrefix
  go 'p'                        = warnSign >> warnPrefix >> warnZero
  go _                          = undefined
  warnFlag
    :: (Eq a, MonadWriter [String] m, MonadState FormatArg m)
    => Lens' FlagSet a
    -> a
    -> a
    -> Char
    -> m ()
  warnFlag lens' bad good flagName = do
    oldVal <- use (flags_ . lens')
    when (oldVal == bad) $ do
      c <- use spec_
      flags_ . lens' .= good
      tell
        [ "`"
          ++ [flagName]
          ++ "` flag has no effect on `"
          ++ [c]
          ++ "` specifier"
        ]
  warnSign   = warnFlag signed_ True False '+'
  warnPrefix = warnFlag prefixed_ True False '#'
  warnSpace  = warnFlag spaced_ True False ' '
  warnZero   = warnFlag adjustment_ (Just ZeroPadded) Nothing '0'
  phonyLengthSpec =
    S.fromList
      $  [ (x, y) | x <- "diuoxX", y <- ["L"] ]
      ++ [ (x, y)
         | x <- "fFeEgGaA"
         , y <- ["hh", "h", "l", "ll", "j", "z", "t"]
         ]
      ++ [ (x, y) | x <- "csqQ", y <- ["hh", "h", "ll", "j", "z", "t", "L"] ]
      ++ map ('p', ) ["hh", "h", "l", "ll", "j", "z", "t", "L"]
  warnLength FormatArg { spec, lengthSpec = Just l }
    | (spec, show l) `S.member` phonyLengthSpec = tell
      [ "`"
        ++ show l
        ++ "` length modifier has no effect when combined with `"
        ++ [spec]
        ++ "` specifier"
      ]
  warnLength _ = return ()

flagSet :: CharSet
flagSet = fromList "-+ #0"

specSet :: CharSet
specSet = fromList "diuoxXfFeEaAgGpcsQq?"

lengthSpecifiers :: [(String, LengthSpecifier)]
lengthSpecifiers =
  [ ("hh", DoubleH)
  , ("h" , H)
  , ("ll", DoubleL)
  , ("l" , L)
  , ("j" , J)
  , ("z" , Z)
  , ("t" , T)
  , ("L" , BigL)
  ]

oneOfSet :: Stream s m Char => CharSet -> ParsecT s u m Char
oneOfSet s = satisfy (`member` s)

printfStr :: Stream s m Char => ParsecT s u m [Atom]
printfStr = do
  atoms <- many $ choice
    [Str "%" <$ try (string "%%"), Arg <$> fmtArg, Str . return <$> noneOf "%"]
  return $ go atoms
 where
  go (Str s : Str s1 : as) = go (Str (s ++ s1) : as)
  go (a              : as) = a : go as
  go []                    = []

fmtArg :: Stream s m Char => ParsecT s u m FormatArg
fmtArg = do
  char '%'
  flags <- do
    fs <- many $ do
      c <- oneOfSet flagSet <?> "flag"
      pure $ case c of
        '-' -> FlagLJust
        '+' -> FlagSigned
        ' ' -> FlagSpaced
        '#' -> FlagPrefixed
        '0' -> FlagZeroPadded
        _   -> error "???"
    let flagSet' = S.fromList fs
    if S.size flagSet' < length fs
      then fail "Duplicate flags specified"
      else pure $ toFlagSet flagSet'
  width <- optionMaybe (choice [Given <$> nat, Need <$ char '*']) <?> "width"
  precision <-
    optionMaybe
        (do
          char '.'
          optionMaybe $ choice [Given <$> nat, Need <$ char '*']
        )
      <?> "precision"
  lengthSpec <- optionMaybe $ choice $ Prelude.map (\(a, b) -> b <$ string a)
                                                   lengthSpecifiers
  spec <- oneOfSet specSet <?> "valid specifier"
  pure $ FormatArg flags
                   width
                   (fromMaybe (Given 0) <$> precision)
                   spec
                   lengthSpec
 where
  nat = do
    c <- many1 $ satisfy isDigit
    return (read c :: Integer)