{-# LANGUAGE StandaloneDeriving #-}

module TreeDB(
    DirList,
    dlEmpty, dlByExt, dlByExts, dlAdd, dlAddByExt,

    TreeDB,
    tdbEmpty, tdbByDir, tdbAdd, tdbAddDir,
    tdbBuild, tdbMerge,

    tdbByDirExt, tdbByDirExts
    )
where

import qualified Data.ByteString.Char8 as C
import Data.List
import Data.Trie(Trie)
import qualified Data.Trie as T
import Data.Typeable

import System.FilePath

--
-- The files in a directory, partitioned by extension.
--
type DirList = [(String, [String])]

dlEmpty :: DirList
dlEmpty = []

-- Linear search for files by extension, in a single directory.
dlByExt :: String -> DirList -> [String]
dlByExt _ [] = []
dlByExt ext ((ext', names) : dirlist)
    | ext' == ext = [n <.> ext' | n <- names]
    | otherwise = dlByExt ext dirlist

-- Search for multiple extensions at once.  'exts' must be sorted, with no
-- duplicates.
dlByExts :: [String] -> DirList -> [String]
dlByExts _ [] = []
dlByExts [] _ = []
dlByExts (ext:exts) ((ext', names):dirlist) =
    case compare ext ext' of
        -- 'ext' isn't in the list.
        LT -> dlByExts exts ((ext', names):dirlist)
        -- 'ext' is right here.
        EQ -> [n <.> ext' | n <- names] ++ dlByExts exts dirlist
        -- 'ext' may be in the remainder.  Nothing else can match here.
        GT -> dlByExts (ext:exts) dirlist

-- Insert a file, given its extension.  Again linear.
dlAdd :: FilePath -> DirList -> DirList
dlAdd file dirList =
    dlAddByExt (takeExtension file) (dropExtension file) dirList

-- Keeps the list sorted by extension
dlAddByExt ::  String -> String -> DirList -> DirList
dlAddByExt ext name [] = [(ext, [name])]
dlAddByExt ext name ((ext', names):dirlist) =
    case compare ext ext' of
        LT -> (ext, [name]):(ext', names):dirlist
        EQ -> (ext', name:names):dirlist
        GT -> (ext', names):(dlAddByExt ext name dirlist)

--
-- A map from directory to contents, excluding subdirectories.
--
type TreeDB = Trie DirList

deriving instance Typeable Trie

tdbEmpty :: TreeDB
tdbEmpty  = T.empty

-- Get directory contents by directory path
tdbByDir :: FilePath -> TreeDB -> Maybe DirList
tdbByDir path treeDB = T.lookup (C.pack path) treeDB

-- Add a file
tdbAdd :: FilePath -> TreeDB -> TreeDB
tdbAdd path treeDB
    | T.member dirS treeDB =
        T.adjust (\dirList -> dlAdd file dirList) dirS treeDB
    | otherwise =
        T.insert dirS (dlAdd file dlEmpty) treeDB
    where
        dir = takeDirectory path
        file = takeFileName path
        dirS = C.pack dir

-- Add a directory, complete with (relative) contents
tdbAddDir :: FilePath -> [FilePath] -> TreeDB -> TreeDB
tdbAddDir dir files treeDB
    | T.member dirS treeDB =
        T.adjust (\dirList -> foldr dlAdd dirList files) dirS treeDB
    | otherwise =
        T.insert dirS (foldr dlAdd dlEmpty files) treeDB
    where
        dirS = C.pack dir

tdbBuild :: [FilePath] -> TreeDB
tdbBuild files = foldr tdbAdd tdbEmpty files

tdbMerge :: TreeDB -> TreeDB -> TreeDB
tdbMerge = T.unionL

--
-- Combined queries
--

-- Find files by directory and extension
tdbByDirExt :: FilePath -> String -> TreeDB -> Maybe [FilePath]
tdbByDirExt path ext treeDB = do
    dirList <- tdbByDir path treeDB
    let filenames = dlByExt ext dirList
    return [ path </> file | file <- filenames ]

-- Look for multiple extensions.  'exts' need not be sorted.
tdbByDirExts :: FilePath -> [String] -> TreeDB -> Maybe [FilePath]
tdbByDirExts path exts treeDB = do
    dirList <- tdbByDir path treeDB
    let filenames = dlByExts (sort exts) dirList
    return [ path </> file | file <- filenames ]
