{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE LambdaCase #-}

module Main where

import           Data.Either
import           Data.SCargot
import           Data.SCargot.Comments
import           Data.SCargot.Repr
import           Data.Semigroup
import qualified Data.Text as T
import qualified Data.Text.IO as TIO
import           System.Exit
import           Test.HUnit
import           Text.Parsec as P
import           Text.Parsec.Text (Parser)
import           Text.Printf ( printf )


main = do
  putStrLn "Parsing a large S-expression"
  srcs <- mapM (\n -> (,) n <$> TIO.readFile n) [ "test/small-sample.sexp"
                                                , "test/med-sample.sexp"
                                                , "test/med2-sample.sexp"
                                                , "test/big-sample.sexp"
                                                ]
  counts <- runTestTT $ TestList $
            let
              -- l2p = list of 2 pairs
              l2p = (SCons
                      (SCons
                        (SAtom (AIdent "hi"))
                        (SAtom (AIdent "hallo")))
                      (SCons
                        (SAtom (AIdent "world"))
                        (SAtom (AIdent "welt"))))
              -- l3sp = list of 3 starting in a pair
              l3sp = (SCons
                       (SCons
                         (SAtom (AIdent "hi"))
                         (SAtom (AIdent "world")))
                       (SCons
                         (SAtom (AIdent "hallo"))
                         (SCons
                           (SAtom (AIdent "welt"))
                           SNil)))
              -- l3ep = list of 3 ending in a pair
              l3ep = (SCons
                       (SAtom (AIdent "hi"))
                       (SCons
                         (SAtom (AIdent "world"))
                         (SCons
                           (SAtom (AIdent "hallo"))
                           (SAtom (AIdent "welt")))))
              -- l3 = list of 3
              l3 = (SCons
                     (SAtom (AIdent "hi"))
                     (SCons
                       (SAtom (AIdent "world"))
                       (SCons
                         (SAtom (AIdent "hallo"))
                         SNil)))
              -- l5p = list of 5 pairs
              l5p = (SCons
                      (SCons
                        (SAtom (AIdent "hi"))
                        (SAtom (AIdent "world")))
                      (SCons
                        (SCons
                          (SAtom (AIdent "hallo"))
                          (SAtom (AIdent "welt")))
                        (SCons
                          (SCons
                            (SAtom (AIdent "bonjour"))
                            (SAtom (AIdent "monde")))
                          (SCons
                            (SCons
                              (SAtom (AIdent "hola"))
                              (SAtom (AIdent "mundo")))
                            (SCons
                              (SAtom (AIdent "ciao"))
                              (SAtom (AIdent "mundo")))))))
              -- l2 = list of 2
              l2 = (SCons
                     (SAtom (AIdent "hi"))
                     (SCons
                       (SAtom (AIdent "world"))
                       SNil))
              -- l1 = list of 1
              l1 = (SCons (SAtom (AIdent "hi")) SNil)
              pair = (SCons (SAtom (AIdent "hi")) (SAtom (AIdent "world")))
              -- linl = list within a list
              linl = (SCons
                         (SAtom (AIdent "hi"))
                         (SCons
                           (SCons
                            (SAtom (AIdent "world"))
                            (SCons
                              (SAtom (AIdent "and"))
                              (SCons
                                (SAtom (AIdent "people"))
                                SNil)))
                           (SCons
                            (SAtom (AIdent "hallo"))
                            (SCons
                             (SAtom (AIdent "welt"))
                             (SCons
                               (SAtom (AIdent "und"))
                               (SCons
                                 (SAtom (AIdent "leute"))
                                 SNil))))))
            in
            [ TestLabel "basic checks" $ TestList
              [ TestLabel "flat print" $ TestList
                [ TestLabel "flatprint SNil" $ "()" ~=? printSExpr SNil
                , TestLabel "flatprint SAtom" $ "hi" ~=? printSExpr (SAtom (AIdent "hi"))
                , TestLabel "flatprint pair" $ "(hi . world)" ~=?
                  printSExpr pair
                , TestLabel "flatprint list of 1" $ "(hi)" ~=?
                  printSExpr l1
                , TestLabel "flatprint list of 2" $ "(hi world)" ~=?
                  printSExpr l2
                , TestLabel "flatprint list of 2 pairs" $ "((hi . hallo) world . welt)" ~=?
                  printSExpr l2p
                , TestLabel "flatprint list of 3 starting in a pair" $
                  -- pairs count as a single element
                  "((hi . world) hallo welt)" ~=?
                  printSExpr l3sp
                , TestLabel "flatprint list of 3 ending in a pair" $ "(hi world hallo . welt)" ~=?
                  printSExpr l3ep
                , TestLabel "flatprint list of 3" $ "(hi world hallo)" ~=?
                  printSExpr l3
                , TestLabel "flatprint pair of list of 4" $ "(hi (world and people) hallo welt und leute)" ~=?
                  printSExpr linl
                , TestLabel "flatprint list of 5 pairs" $
                  "((hi . world) (hallo . welt) (bonjour . monde) (hola . mundo) ciao . mundo)" ~=?
                  printSExpr l5p
                ]

              , TestLabel "pretty print width 40" $
                let pprintIt = pprintSExpr 40 Swing in TestList
                [ TestLabel "pretty print SNil" $ "()" ~=? pprintIt SNil
                , TestLabel "pretty print SAtom" $ "hi" ~=? pprintIt (SAtom (AIdent "hi"))
                , TestLabel "pretty print pair" $ "(hi . world)" ~=?
                  pprintIt pair
                , TestLabel "pretty print list of 1" $ "(hi)" ~=?
                  pprintIt l1
                , TestLabel "pretty print list of 2" $ "(hi world)" ~=?
                  pprintIt l2
                , TestLabel "pretty print list of 2 pairs" $
                  "((hi . hallo) world . welt)" ~=?
                  pprintIt l2p
                , TestLabel "pretty print list of 3 starting in a pair" $
                  -- pairs count as a single element
                  "((hi . world) hallo welt)" ~=?
                  pprintIt l3sp
                , TestLabel "pretty print list of 3 ending in a pair" $
                  "(hi world hallo . welt)" ~=?
                  pprintIt l3ep
                , TestLabel "pretty print list of 3" $ "(hi world hallo)" ~=?
                  pprintIt l3
                , TestLabel "pretty print pair of list of 4" $ "(hi\n  (world and people)\n  hallo\n  welt\n  und\n  leute)" ~=?
                  pprintIt linl
                , TestLabel "pretty print list of 5 pairs" $
                  "((hi . world)\n  (hallo . welt)\n  (bonjour . monde)\n  (hola . mundo)\n  ciao . mundo)" ~=?
                  pprintIt l5p
                ]

              , TestLabel "pretty print width 10" $
                let pprintIt = pprintSExpr 10 Swing in TestList
                [ TestLabel "pretty print SNil" $ "()" ~=? pprintIt SNil
                , TestLabel "pretty print SAtom" $ "hi" ~=? pprintIt (SAtom (AIdent "hi"))
                , TestLabel "pretty print pair" $ "(hi . world)" ~=?
                  pprintIt pair
                , TestLabel "pretty print list of 1" $ "(hi)" ~=?
                  pprintIt l1
                , TestLabel "pretty print list of 2" $ "(hi world)" ~=?
                  pprintIt l2
                , TestLabel "pretty print list of 2 pairs" $
                  "((hi . hallo)\n  world . welt)" ~=?
                  pprintIt l2p
                , TestLabel "pretty print list of 3 starting in a pair" $
                  -- pairs count as a single element
                  "((hi . world)\n  hallo\n  welt)" ~=?
                  pprintIt l3sp
                , TestLabel "pretty print list of 3 ending in a pair" $
                  "(hi\n  world\n  hallo . welt)" ~=?
                  pprintIt l3ep
                , TestLabel "pretty print list of 3" $ "(hi\n  world\n  hallo)" ~=?
                  pprintIt l3
                , TestLabel "pretty print pair of list of 4" $ "(hi\n  (world\n    and\n    people)\n  hallo\n  welt\n  und\n  leute)" ~=?
                  pprintIt linl
                , TestLabel "pretty print list of 5 pairs" $
                  "((hi . world)\n  (hallo . welt)\n  (bonjour . monde)\n  (hola . mundo)\n  ciao . mundo)" ~=?
                  pprintIt l5p
                ]

              , TestLabel "pretty print width 10 indent 5" $
                let pprintIt = encodeOne (setIndentStrategy (const Swing) $
                                          setIndentAmount 5 $
                                          setMaxWidth 10 $
                                          basicPrint printAtom)
                in TestList
                [ TestLabel "pretty print SNil" $ "()" ~=? pprintIt SNil
                , TestLabel "pretty print SAtom" $ "hi" ~=? pprintIt (SAtom (AIdent "hi"))
                , TestLabel "pretty print pair" $ "(hi . world)" ~=?
                  pprintIt pair
                , TestLabel "pretty print list of 1" $ "(hi)" ~=?
                  pprintIt l1
                , TestLabel "pretty print list of 2" $ "(hi world)" ~=?
                  pprintIt l2
                , TestLabel "pretty print list of 2 pairs" $
                  "((hi . hallo)\n      world . welt)" ~=?
                  pprintIt l2p
                , TestLabel "pretty print list of 3 starting in a pair" $
                  -- pairs count as a single element
                  "((hi . world)\n      hallo\n      welt)" ~=?
                  pprintIt l3sp
                , TestLabel "pretty print list of 3 ending in a pair" $
                  "(hi\n      world\n      hallo . welt)" ~=?
                  pprintIt l3ep
                , TestLabel "pretty print list of 3" $
                  "(hi\n      world\n      hallo)" ~=?
                  pprintIt l3
                , TestLabel "pretty print pair of list of 4" $ "(hi\n      (world\n            and\n            people)\n      hallo\n      welt\n      und\n      leute)" ~=?
                  pprintIt linl
                , TestLabel "pretty print list of 5 pairs" $
                  "((hi . world)\n      (hallo . welt)\n      (bonjour . monde)\n      (hola . mundo)\n      ciao . mundo)" ~=?
                  pprintIt l5p
                ]

              , TestLabel "pretty print width 10 swing-after 3" $
                let pprintIt = pprintSExpr 10 (SwingAfter 3) in TestList
                [ TestLabel "pretty print SNil" $ "()" ~=? pprintIt SNil
                , TestLabel "pretty print SAtom" $ "hi" ~=? pprintIt (SAtom (AIdent "hi"))
                , TestLabel "pretty print pair" $ "(hi . world)" ~=?
                  pprintIt pair
                , TestLabel "pretty print list of 1" $ "(hi)" ~=?
                  pprintIt l1
                , TestLabel "pretty print list of 2" $ "(hi world)" ~=?
                  pprintIt l2
                , TestLabel "pretty print list of 2 pairs" $
                  -- pairs are not split internally
                  "((hi . hallo) world . welt)" ~=?
                  pprintIt l2p
                , TestLabel "pretty print list of 3 starting in a pair" $
                  -- pairs count as a single element
                  "((hi . world) hallo welt)" ~=?
                  pprintIt l3sp
                , TestLabel "pretty print list of 3 ending in a pair" $
                  -- pairs are not split internally
                  "(hi world hallo . welt)" ~=?
                  pprintIt l3ep
                , TestLabel "pretty print list of 3" $ "(hi world hallo)" ~=?
                  pprintIt l3
                , TestLabel "pretty print pair of list of 4" $
                  -- atom list pair\n ...
                  "(hi (world and people) hallo welt\n  und\n  leute)" ~=?
                  pprintIt linl
                , TestLabel "pretty print list of 5 pairs" $
                  "((hi . world) (hallo . welt) (bonjour . monde) (hola . mundo)\n  ciao . mundo)" ~=?
                  pprintIt l5p
                ]

              , TestLabel "pretty print width 10 aligned" $
                let pprintIt = pprintSExpr 10 Align in TestList
                [ TestLabel "pretty print SNil" $ "()" ~=? pprintIt SNil
                , TestLabel "pretty print SAtom" $ "hi" ~=? pprintIt (SAtom (AIdent "hi"))
                , TestLabel "pretty print pair" $ "(hi . world)" ~=?
                  pprintIt pair
                , TestLabel "pretty print list of 1" $ "(hi)" ~=?
                  pprintIt l1
                , TestLabel "pretty print list of 2" $ "(hi world)" ~=?
                  pprintIt l2
                , TestLabel "pretty print list of 2 pairs" $
                  "((hi . hallo) world . welt)" ~=?
                  pprintIt l2p
                , TestLabel "pretty print list of 3 starting in a pair" $
                  -- pairs count as a single element
                  "((hi . world) hallo\n              welt)" ~=?
                  pprintIt l3sp
                , TestLabel "pretty print list of 3 ending in a pair" $
                  "(hi world\n    hallo . welt)" ~=?
                  pprintIt l3ep
                , TestLabel "pretty print list of 3" $ "(hi world\n    hallo)" ~=?
                  pprintIt l3
                , TestLabel "pretty print pair of list of 4" $
                  "(hi (world and\n           people)\n    hallo\n    welt\n    und\n    leute)" ~=?
                  pprintIt linl
                , TestLabel "pretty print list of 5 pairs" $
                  "((hi . world) (hallo . welt)\n              (bonjour . monde)\n              (hola . mundo)\n              ciao . mundo)" ~=?
                  pprintIt l5p
                ]

              , TestLabel "pretty print width 400" $
                let pprintIt = pprintSExpr 400 Swing in TestList
                [ TestLabel "pretty print SNil" $ "()" ~=? pprintIt SNil
                , TestLabel "pretty print SAtom" $ "hi" ~=? pprintIt (SAtom (AIdent "hi"))
                , TestLabel "pretty print pair" $ "(hi . world)" ~=?
                  pprintIt pair
                , TestLabel "pretty print list of 1" $ "(hi)" ~=?
                  pprintIt l1
                , TestLabel "pretty print list of 2" $ "(hi world)" ~=?
                  pprintIt l2
                , TestLabel "pretty print list of 2 pairs" $
                  -- No Swing if it fits on a line
                  "((hi . hallo) world . welt)" ~=?
                  pprintIt l2p
                , TestLabel "pretty print list of 3 starting in a pair" $
                  -- pairs count as a single element
                  "((hi . world) hallo welt)" ~=?
                  pprintIt l3sp
                , TestLabel "pretty print list of 3 ending in a pair" $
                  "(hi world hallo . welt)" ~=?
                  pprintIt l3ep
                , TestLabel "pretty print list of 3" $ "(hi world hallo)" ~=?
                  pprintIt l3
                , TestLabel "pretty print pair of list of 4" $ "(hi (world and people) hallo welt und leute)" ~=?
                  pprintIt linl
                , TestLabel "pretty print list of 5 pairs" $
                  "((hi . world) (hallo . welt) (bonjour . monde) (hola . mundo) ciao . mundo)" ~=?
                  pprintIt l5p
                ]

              , TestLabel "unconstrained print" $
                let pprintIt = ucPrintSExpr Swing in TestList
                [ TestLabel "pretty print SNil" $ "()" ~=? pprintIt SNil
                , TestLabel "pretty print SAtom" $ "hi" ~=? pprintIt (SAtom (AIdent "hi"))
                , TestLabel "pretty print pair" $ "(hi . world)" ~=?
                  pprintIt pair
                , TestLabel "pretty print list of 1" $ "(hi)" ~=?
                  pprintIt l1
                , TestLabel "pretty print list of 2" $ "(hi world)" ~=?
                  pprintIt l2
                , TestLabel "pretty print list of 2 pairs" $
                  "((hi . hallo)\n  world\n  . welt)" ~=?
                  pprintIt l2p
                , TestLabel "pretty print list of 3 starting in a pair" $
                  -- pairs count as a single element
                  "((hi . world)\n  hallo\n  welt)" ~=?
                  pprintIt l3sp
                , TestLabel "pretty print list of 3 ending in a pair" $
                  "(hi world hallo . welt)" ~=?
                  pprintIt l3ep
                , TestLabel "pretty print list of 3" $ "(hi world hallo)" ~=?
                  pprintIt l3
                , TestLabel "pretty print pair of list of 4" $ "(hi\n  (world and people)\n  hallo\n  welt\n  und\n  leute)" ~=?
                  pprintIt linl
                , TestLabel "pretty print list of 5 pairs" $
                  "((hi . world)\n  (hallo . welt)\n  (bonjour . monde)\n  (hola . mundo)\n  ciao\n  . mundo)" ~=?
                  pprintIt l5p
                ]

              ]
            , TestLabel "round-trip" $ TestList $
              concatMap (\t -> map t srcs) $
              [ testParsePrint
              ]
            ]
  if errors counts + failures counts > 0
  then exitFailure
  else exitSuccess


testParsePrint :: (String, T.Text) -> Test
testParsePrint (n,s) = TestList
                       [ testParseFlatPrint n s

                       , testParseUnconstrainedPrint Swing n s
                       , testParseUnconstrainedPrint Align n s

                       , testParsePPrint 80 Swing n s
                       , testParsePPrint 60 Swing n s
                       , testParsePPrint 40 Swing n s
                       , testParsePPrint 20 Swing n s
                       , testParsePPrint 15 Swing n s
                       , testParsePPrint 10 Swing n s

                       , testParsePPrint 80 Align n s
                       , testParsePPrint 40 Align n s
                       , testParsePPrint 10 Align n s
                       ]


testParseFlatPrint testName src =
    testRoundTrip (testName <> " flat print")
                      (fromRight (error "Failed parse") . parseSExpr)
                      printSExpr
                      stripAllText
                      src

testParseUnconstrainedPrint indentStyle testName src =
    testRoundTrip (testName <> " unconstrained print")
                      (fromRight (error "Failed parse") . parseSExpr)
                      (ucPrintSExpr indentStyle)
                      stripAllText
                      src

testParsePPrint width indentStyle testName src =
    testRoundTrip (testName <> " pretty print")
                      (fromRight (error "Failed parse") . parseSExpr)
                      (pprintSExpr width indentStyle)
                      stripAllText
                      src

stripAllText = T.unwords . concatMap T.words . T.lines

testRoundTrip nm there back prep src = TestList
  [ TestLabel (nm <> " round trip") $
    TestCase $ (prep src) @=? (prep $ back $ there src)

  , TestLabel (nm <> " round trip twice") $
    TestCase $ (prep src) @=? (prep $ back $ there $ back $ there src)
  ]


------------------------------------------------------------------------

data FAtom = AIdent String
           | AQuoted String
           | AString String
           | AInt Integer
           | ABV Int Integer
           deriving (Eq, Show)


string :: String -> SExpr FAtom
string = SAtom . AString

-- | Lift an unquoted identifier.
ident :: String -> SExpr FAtom
ident = SAtom . AIdent

-- | Lift a quoted identifier.
quoted :: String -> SExpr FAtom
quoted = SAtom . AQuoted

-- | Lift an integer.
int :: Integer -> SExpr FAtom
int = SAtom . AInt


printAtom :: FAtom -> T.Text
printAtom a =
  case a of
    AIdent s -> T.pack s
    AQuoted s -> T.pack ('\'' : s)
    AString s -> T.pack (show s)
    AInt i -> T.pack (show i)
    ABV w val -> formatBV w val


printSExpr :: SExpr FAtom -> T.Text
printSExpr = encodeOne (flatPrint printAtom)

pprintSExpr :: Int -> Indent -> SExpr FAtom -> T.Text
pprintSExpr w i = encodeOne (setIndentStrategy (const i) $
                             setMaxWidth w $
                             setIndentAmount 1 $
                             basicPrint printAtom)

ucPrintSExpr :: Indent -> SExpr FAtom -> T.Text
ucPrintSExpr i = encodeOne (setIndentStrategy (const i) $
                            setIndentAmount 1 $
                            unconstrainedPrint printAtom)

getIdent :: FAtom -> Maybe String
getIdent (AIdent s) = Just s
getIdent _ = Nothing

formatBV :: Int -> Integer -> T.Text
formatBV w val = T.pack (prefix ++ printf fmt val)
  where
    (prefix, fmt)
      | w `rem` 4 == 0 = ("#x", "%0" ++ show (w `div` 4) ++ "x")
      | otherwise = ("#b", "%0" ++ show w ++ "b")

parseIdent :: Parser String
parseIdent = (:) <$> first <*> P.many rest
  where first = P.letter P.<|> P.oneOf "+-=<>_"
        rest = P.letter P.<|> P.digit P.<|> P.oneOf "+-=<>_"

parseString :: Parser String
parseString = do
  _ <- P.char '"'
  s <- P.many (P.noneOf ['"'])
  _ <- P.char '"'
  return s

parseBV :: Parser (Int, Integer)
parseBV = P.char '#' >> ((P.char 'b' >> parseBin) P.<|> (P.char 'x' >> parseHex))
  where parseBin = P.oneOf "10" >>= \d -> parseBin' (1, if d == '1' then 1 else 0)

        parseBin' :: (Int, Integer) -> Parser (Int, Integer)
        parseBin' (bits, x) = do
          P.optionMaybe (P.oneOf "10") >>= \case
            Just d -> parseBin' (bits + 1, x * 2 + (if d == '1' then 1 else 0))
            Nothing -> return (bits, x)

        parseHex = (\s -> (length s * 4, read ("0x" ++ s))) <$> P.many1 P.hexDigit

parseAtom :: Parser FAtom
parseAtom
  =   AIdent      <$> parseIdent
  P.<|> AQuoted     <$> (P.char '\'' >> parseIdent)
  P.<|> AString     <$> parseString
  P.<|> AInt . read <$> P.many1 P.digit
  P.<|> uncurry ABV <$> parseBV

parserLL :: SExprParser FAtom (SExpr FAtom)
parserLL = withLispComments (mkParser parseAtom)

parseSExpr :: T.Text -> Either String (SExpr FAtom)
parseSExpr = decodeOne parserLL
