{-# LANGUAGE OverloadedStrings #-}
module Data.Dtb
  (Dtb(..), MemoryReservation(..), Property(..), DeviceTree(..)
  , parseDtb
  , lookupNode
  , lookupProperty
  , rootNode
  , reservedMemory
  , addressSizeCells
  , asText
  , asWord32
  , asWord64
  , asRegList
  )
where

import qualified Data.ByteString   as B
import           Data.Dtb.LowLevel
import           Data.Dtb.Parser
import           Data.Dtb.Property
import           Data.Foldable     (find)
import           Data.List         (isPrefixOf)
import           Data.Maybe        (catMaybes, isJust, listToMaybe)
import qualified Data.Text         as T
import           Data.Word         (Word32)

-- |The representation of a device tree.
--
-- Memory reservations are not part of the tree structure itself, but
-- are represented using a list. Everything else exists as a
-- `DeviceTree` tree.
data Dtb = Dtb [MemoryReservation] DeviceTree
  deriving (Show)

-- |Parses a device tree from a `ByteString`.
--
-- Returns an error message on failure.
parseDtb :: B.ByteString -> Either T.Text Dtb
parseDtb dta = do
  header <- parseHeader dta
  memRsv <- memoryReservations header dta
  sb <- stringsBlock header dta
  stb <- structBlock header dta
  tokens <- deviceTreeTokens sb stb
  tree <- parse tokens
  return $ Dtb memRsv tree

-- |Returns the root node of the device tree.
rootNode :: Dtb -> DeviceTree
rootNode (Dtb _ n) = n

reservedMemory :: Dtb -> [MemoryReservation]
reservedMemory (Dtb m _) = m

-- |A list of node names is a path through the tree.
type Path = [T.Text]

-- |Split a path into its components.
toPath :: T.Text -> Path
toPath p = if split == ["", ""] then [""] else split
  -- "/" is split into two empty strings. We want [""] instead.
  where split = T.splitOn "/" $ if T.isPrefixOf "/" p then p else "/" <> p


-- |Split a path component into name and unit address.
--
-- As the unit address is optional, it can be Nothing.
splitName :: T.Text -> (T.Text, Maybe T.Text)
splitName name = if isJust (T.find (== '@') name) then
                   let (n, u) = T.breakOn "@" name in
                     (n, Just u)
                 else
                   (name, Nothing)

-- |Check whether two names match.
matchesNames :: (T.Text, Maybe T.Text) -> (T.Text, Maybe T.Text) -> Bool
matchesNames (a, Nothing) (b, _) = a == b
matchesNames (a, _) (b, Nothing) = a == b
matchesNames a b                 = a == b

-- |Check whether a name matches a
matchesPath :: T.Text -> DeviceTree -> Bool
matchesPath p (Node name _ _) = matchesNames splitP splitN
  where
    splitP = splitName p
    splitN = splitName name


-- |Look up a device tree node by its path.
--
-- This function does not handle aliases.
lookupNodeByPath :: Path -> DeviceTree -> Maybe DeviceTree
lookupNodeByPath [] n = Nothing
lookupNodeByPath (p:ps) n@(Node _ _ children)
  | matchesPath p n = case ps of
      [] -> Just n
      _  -> listToMaybe $ catMaybes $ lookupNodeByPath ps <$> children
  | otherwise = Nothing

-- |An alias is is a pair of alias name and path it points to.
type Alias = (T.Text, Path)

-- |The list of aliases defined in the device tree.
--
-- This list is empty if there is no alias node or the alias node is
-- empty.
aliases :: Dtb -> [Alias]
aliases dtb = map (\p -> (propName p, toPath $ asText p))
  $ case lookupNodeByPath ["", "aliases"] (rootNode dtb) of
      Just (Node _ props _) -> props
      _                     -> []

-- |Try to replace an alias in a path.
--
-- Only a single alias is resolved and only of it occurs at the start
-- of the path.
resolveAliases :: [Alias] -> Path -> Path
resolveAliases _ []           = []
resolveAliases aliases path = case find (\(a, _) -> isPrefixOf ["", a] path) aliases of
  Just (_, replacement) -> replacement ++ drop 2 path
  Nothing               -> path

-- |Convert a textual path to a path structure.
--
-- This function also resolves aliases.
resolvePath :: T.Text -> Dtb -> Path
resolvePath name dtb = resolveAliases (aliases dtb) $ toPath name

-- |Look up a property in the device tree using its path.
--
-- The path is a string like "/cpus/cpu@0". This function currently
-- does NOT support aliases.
lookupNode :: T.Text -> Dtb -> Maybe DeviceTree
lookupNode name dtb@(Dtb _ n) = lookupNodeByPath (resolvePath name dtb) n

-- |Find a property with the given name in the property list of a
-- node.
lookupProperty :: T.Text -> DeviceTree -> Maybe Property
lookupProperty name (Node _ props _) = find ((== name) . propName) props

dropLast :: [a] -> [a]
dropLast []     = error "Cannot drop last element from empty list"
dropLast (_:[]) = []
dropLast (x:xs) = x:(dropLast xs)

-- |Returns the #address-cells and #size-cells properties of a
-- node.
--
-- If the node doesn't have the required properties the tree is
-- traversed upwards to the first node that has them.
--
-- This function can be used together with `asRegList` to parse
-- resource lists.
addressSizeCells :: T.Text -> Dtb -> Maybe (Word32, Word32)
addressSizeCells name dtb@(Dtb _ n) = descend path
  where
    descend :: Path -> Maybe (Word32, Word32)
    descend [] = Nothing
    descend path = case lookupNodeByPath path n >>= addressSizeCellsExact of
      Just addrSize -> Just addrSize
      Nothing       -> descend $ dropLast path
    path = resolvePath name dtb
    addressSizeCellsExact dtb' =  (,) <$> propAsWord32 dtb' "#address-cells" <*> propAsWord32 dtb' "#size-cells"
    propAsWord32 t name = lookupProperty name t >>= asWord32
