{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}

module Output.Formatter
  ( OutputFormat(..)
  , formatOutput
  ) where

import qualified Data.Aeson as A
import qualified Data.ByteString.Lazy as BL
import qualified Data.Csv as Csv
import qualified Data.Map.Strict as Map
import Data.Text (Text)
import qualified Data.Text as T
import qualified Data.Text.Encoding as TE
import qualified Data.Vector as V
import Data.Char (toLower)
import Language.Language (Language(..))
import Stats.FileStats (FileStats(..), combineStats)
import Text.Printf (printf)

data OutputFormat
  = Console
  | JsonFormat
  | CsvFormat
  | MarkdownFormat
  deriving (Show, Eq)

instance Read OutputFormat where
  readsPrec _ input = case map toLower input of
    "json"     -> [(JsonFormat, "")]
    "csv"      -> [(CsvFormat, "")]
    "markdown" -> [(MarkdownFormat, "")]
    _          -> [(Console, "")]

data OutputRow = OutputRow
  { orLanguage :: String
  , orFiles :: Int
  , orBlank :: Int
  , orComment :: Int
  , orCode :: Int
  } deriving (Show, Eq)

instance A.ToJSON OutputRow where
  toJSON OutputRow{..} = A.object
    [ "language" A..= orLanguage
    , "files" A..= orFiles
    , "blank" A..= orBlank
    , "comment" A..= orComment
    , "code" A..= orCode
    ]

instance Csv.ToNamedRecord OutputRow where
  toNamedRecord OutputRow{..} = Csv.namedRecord
    [ "language" Csv..= orLanguage
    , "files" Csv..= orFiles
    , "blank" Csv..= orBlank
    , "comment" Csv..= orComment
    , "code" Csv..= orCode
    ]

instance Csv.DefaultOrdered OutputRow where
  headerOrder _ = V.fromList ["language", "files", "blank", "comment", "code"]

formatOutput :: Map.Map Language FileStats -> OutputFormat -> Text
formatOutput stats fmt = case fmt of
    JsonFormat -> formatJSON stats
    CsvFormat -> formatCSV stats
    MarkdownFormat -> formatMarkdown stats
    Console -> error "Console format should use prettyPrint directly"

formatJSON :: Map.Map Language FileStats -> Text
formatJSON stats =
  let rows = statsToRows stats
   in TE.decodeUtf8 . BL.toStrict $ A.encode rows

formatCSV :: Map.Map Language FileStats -> Text
formatCSV stats =
  let rows = statsToRows stats
   in TE.decodeUtf8 . BL.toStrict $ Csv.encodeDefaultOrderedByName rows

formatMarkdown :: Map.Map Language FileStats -> Text
formatMarkdown stats =
  let header = "| Language | Files | Blank | Comment | Code |\n\
               \|----------|-------|-------|---------|------|\n"
      rows = map formatMarkdownRow (statsToRows stats)
   in T.pack header <> T.unlines rows

formatMarkdownRow :: OutputRow -> Text
formatMarkdownRow OutputRow{..} =
  T.pack $ printf "| %s | %d | %d | %d | %d |"
    orLanguage
    orFiles
    orBlank
    orComment
    orCode

statsToRows :: Map.Map Language FileStats -> [OutputRow]
statsToRows stats =
  let (others, knowns) = Map.partitionWithKey (\k _ -> isOther k) stats
      knownRows = map toOutputRow (Map.toList knowns)
      otherRow = case Map.null others of
        True -> []
        False -> [combineToRow "Other" (Map.elems others)]
      total = [combineToRow "Total" (Map.elems stats)]
   in knownRows ++ otherRow ++ total
  where
    isOther (Other _) = True
    isOther _ = False

toOutputRow :: (Language, FileStats) -> OutputRow
toOutputRow (lang, FileStats{..}) = OutputRow
  { orLanguage = show lang
  , orFiles = fsFileCount
  , orBlank = fsBlankLines
  , orComment = fsSingleComments + fsMultiComments
  , orCode = fsCodeLines
  }

combineToRow :: String -> [FileStats] -> OutputRow
combineToRow name stats =
  let combined = foldl combineStats (head stats) (tail stats)
   in OutputRow
      { orLanguage = name
      , orFiles = fsFileCount combined
      , orBlank = fsBlankLines combined
      , orComment = fsSingleComments combined + fsMultiComments combined
      , orCode = fsCodeLines combined
      }

