import Data.Bifunctor (first)
import Data.Bool (bool)
import Data.Char (isSpace)
import qualified Data.Text as T
import qualified Data.Text.IO as T
import qualified Data.Text.Read as T
import Data.Tree (Forest, Tree (..), foldTree)
import Numeric (showFFloat)
import System.Directory (doesFileExist)



data Coverage = Coverage
  { name :: T.Text,
    weight :: Float,
    coverage :: Float,
    share :: Float
  }
  deriving (Show)


fp = "./coverageOutline.txt"

main :: IO ()
main =
  doesFileExist fp
    >>= bool
      (print $ "File not found: " <> fp)
      (T.readFile fp >>= T.putStrLn . updatedCoverageOutline)


updatedCoverageOutline :: T.Text -> T.Text
updatedCoverageOutline s =
  let delimiter = "|"
      indentedLines = T.lines s
      columnNames =
        init $
          tokenizeWith
            delimiter
            ( head indentedLines
            )
   in T.unlines
        [ tabulation
            delimiter
            (columnNames <> ["SHARE OF RESIDUE"]),
          indentedLinesFromTree
            "    "
            (showCoverage delimiter)
            $ withResidueShares 1.0 $
              foldTree
                weightedCoverage
                (parseTreeFromOutline delimiter indentedLines)
        ]


weightedCoverage ::
  Coverage ->
  Forest Coverage ->
  Tree Coverage
weightedCoverage x xs =
  let cws = ((,) . coverage <*> weight) . rootLabel <$> xs
      totalWeight = foldr ((+) . snd) 0 cws
   in Node
        ( x
            { coverage =
                foldr
                  (\(c, w) a -> (c * w) + a)
                  (coverage x)
                  cws
                  / bool 1 totalWeight (0 < totalWeight)
            }
        )
        xs

withResidueShares :: Float -> Tree Coverage -> Tree Coverage
withResidueShares shareOfTotal tree =
  let go fraction node =
        let forest = subForest node
            weights = weight . rootLabel <$> forest
            weightTotal = sum weights
            nodeRoot = rootLabel node
         in Node
              ( nodeRoot
                  { share =
                      fraction
                        * (1 - coverage nodeRoot)
                  }
              )
              ( zipWith
                  go
                  ((fraction *) . (/ weightTotal) <$> weights)
                  forest
              )
   in go shareOfTotal tree


parseTreeFromOutline :: T.Text -> [T.Text] -> Tree Coverage
parseTreeFromOutline delimiter indentedLines =
  partialRecord . tokenizeWith delimiter
    <$> head
      ( forestFromLineIndents $
          indentLevelsFromLines $ tail indentedLines
      )

forestFromLineIndents :: [(Int, T.Text)] -> [Tree T.Text]
forestFromLineIndents pairs =
  let go [] = []
      go ((n, s): xs) =
        let (firstTreeLines, rest) = span ((n <) . fst) xs
         in Node s (go firstTreeLines): go rest
   in go pairs

indentLevelsFromLines :: [T.Text] -> [(Int, T.Text)]
indentLevelsFromLines xs =
  let pairs = T.span isSpace <$> xs
      indentUnit =
        foldr
          ( \x a ->
              let w = (T.length . fst) x
               in bool a w (w < a && 0 < w)
          )
          (maxBound :: Int)
          pairs
   in first (flip div indentUnit . T.length) <$> pairs

partialRecord :: [T.Text] -> Coverage
partialRecord xs =
  let [name, weightText, coverageText] =
        take
          3
          (xs <> repeat "")
   in Coverage
        { name = name,
          weight = defaultOrRead 1.0 weightText,
          coverage = defaultOrRead 0.0 coverageText,
          share = 0.0
        }

defaultOrRead :: Float -> T.Text -> Float
defaultOrRead n txt = either (const n) fst $ T.rational txt

tokenizeWith :: T.Text -> T.Text -> [T.Text]
tokenizeWith delimiter = fmap T.strip . T.splitOn delimiter


indentedLinesFromTree ::
  T.Text ->
  (T.Text -> a -> T.Text) ->
  Tree a ->
  T.Text
indentedLinesFromTree tab showRoot tree =
  let go indent node =
        showRoot indent (rootLabel node):
        (subForest node >>= go (T.append tab indent))
   in T.unlines $ go "" tree

showCoverage :: T.Text -> T.Text -> Coverage -> T.Text
showCoverage delimiter indent x =
  tabulation
    delimiter
    ( [T.append indent (name x), T.pack (showN 0 (weight x))]
        <> (T.pack . showN 4 <$> ([coverage, share] <*> [x]))
    )

tabulation :: T.Text -> [T.Text] -> T.Text
tabulation delimiter =
  T.intercalate (T.append delimiter " ")
    . zipWith (`T.justifyLeft` ' ') [31, 9, 9, 9]

justifyRight :: Int -> a -> [a] -> [a]
justifyRight n c = (drop . length) <*> (replicate n c <>)

showN :: Int -> Float -> String
showN p n = justifyRight 7 ' ' (showFFloat (Just p) n "")