{-|
Module      : Toml.Semantics.Ordered
Description : Tool for extracting an ordering from an existing TOML file
Copyright   : (c) Eric Mertens, 2023
License     : ISC
Maintainer  : emertens@gmail.com

This module can help build a key ordering projection given an existing
TOML file. This could be useful for applying a transformation to a TOML
file before pretty-printing it back in something very close to the
original order.

When using the computed order, table keys will be remembered in the order
they appeared in the source file. Any key additional keys added to the
tables will be ordered alphabetically after all the known keys.

@
demo =
 do txt <- 'readFile' \"demo.toml\"
    let Right exprs = 'Toml.Parser.parseRawToml' txt
        to          = 'extractTableOrder' exprs
        Right toml  = 'Toml.Semantics.semantics' exprs
        projection  = 'projectKey' to
    'print' ('Toml.Pretty.prettyTomlOrdered' projection toml)
@

-}
module Toml.Semantics.Ordered (
    TableOrder,
    extractTableOrder,
    projectKey,
    ProjectedKey,
    debugTableOrder,
    ) where

import Data.Foldable (foldl', toList)
import Data.List (sortOn)
import Data.Map (Map)
import Data.Map qualified as Map
import Data.Text (Text)
import Data.Text qualified as Text
import Toml.Syntax.Types (Expr(..), Key, Val(ValTable, ValArray))

-- | Summary of the order of the keys in a TOML document.
newtype TableOrder = TO (Map Text KeyOrder)

-- | Internal type used by 'TableOrder'
--
-- The 'Int' field determines the order of the current key and the
-- 'TableOrder' determines the order of the children of this key.
data KeyOrder = KeyOrder !Int TableOrder

-- | Opaque type used by 'projectKey'
newtype ProjectedKey = PK (Either Int Text)
    deriving (Eq, Ord)

-- | Generate a projection function for use with 'Toml.Pretty.prettyTomlOrdered'
projectKey ::
    TableOrder   {- ^ table order                           -} ->
    [Text]       {- ^ table path                            -} ->
    Text         {- ^ key                                   -} ->
    ProjectedKey {- ^ type suitable for ordering table keys -}
projectKey (TO to) [] = \k ->
    case Map.lookup k to of
        Just (KeyOrder i _)     -> PK (Left i)
        Nothing                 -> PK (Right k)
projectKey (TO to) (p:ps) =
    case Map.lookup p to of
        Just (KeyOrder _ to')   -> projectKey to' ps
        Nothing                 -> PK . Right

emptyOrder :: TableOrder
emptyOrder = TO Map.empty

-- | Extract a 'TableOrder' from the output of 'Toml.Parser.parseRawToml'
-- to be later used with 'projectKey'.
extractTableOrder :: [Expr a] -> TableOrder
extractTableOrder = snd . foldl' addExpr ([], emptyOrder)

addExpr :: ([Text], TableOrder) -> Expr a -> ([Text], TableOrder)
addExpr (prefix, to) = \case
    TableExpr k      -> let k' = keyPath k in (k', addKey to k')
    ArrayTableExpr k -> let k' = keyPath k in (k', addKey to k')
    KeyValExpr k v   -> (prefix, addVal prefix (addKey to (prefix ++ keyPath k)) v)

addVal :: [Text] -> TableOrder -> Val a -> TableOrder
addVal prefix to lval =
    case lval of
        ValArray _ xs -> foldl' (addVal prefix) to xs
        ValTable _ kvs ->
            foldl' (\acc (k,v) ->
                let k' = prefix ++ keyPath k in
                addVal k' (addKey acc k') v) to kvs
        _ -> to

addKey :: TableOrder -> [Text] -> TableOrder
addKey to [] = to
addKey (TO to) (x:xs) = TO (Map.alter f x to)
    where
        f Nothing = Just (KeyOrder (Map.size to) (addKey emptyOrder xs))
        f (Just (KeyOrder i m)) = Just (KeyOrder i (addKey m xs))

keyPath :: Key a -> [Text]
keyPath = map snd . toList

-- | Render a white-space nested representation of the key ordering extracted
-- by 'extractTableOrder'. This is provided for debugging and understandability.
debugTableOrder :: TableOrder -> String
debugTableOrder to = unlines (go 0 to [])
    where
        go i (TO m) z =
            foldr (go1 i) z
                (sortOn p (Map.assocs m))

        go1 i (k, KeyOrder _ v) z =
            (replicate (4*i) ' ' ++ Text.unpack k) :
            go (i+1) v z

        p (_, KeyOrder i _) = i
