{-# LANGUAGE ConstraintKinds, TupleSections #-}

module Database.Stash (
    Stashable(..),
    Meta,
    put,
    get,
    find,
    find',
    getType,
    findType,
    findType',    
) where

{-# LANGUAGE ConstraintKinds #-}
import           Control.Monad        (forM, join)
import           Data.Aeson           (FromJSON, ToJSON)
import           Data.Maybe           (listToMaybe)
import           Data.Maybe           (catMaybes)
import           Data.Typeable        (Typeable)
import qualified Data.Aeson
import qualified Data.Aeson.Parser
import qualified Data.ByteString.Lazy as ByteString
import qualified Data.ByteString as SByteString
import qualified Data.Hashable
import qualified Data.String
import qualified Data.Typeable
import qualified Data.Vector
import qualified System.Directory
import qualified Data.Attoparsec.ByteString
import qualified Data.Text 
type Stashable a = (ToJSON a, FromJSON a, Typeable a, Show a)
-- class (ToJSON a, FromJSON a, Typeable a            , Show a) => Stashable a where

-- instance Stashable () -- TODO
-- instance Stashable Int -- TODO
-- instance Stashable Double -- TODO
-- instance Stashable Char -- TODO
-- instance Stashable a => Stashable [a] -- TODO
-- instance (Stashable a, Stashable b) => Stashable (a, b) -- TODO

{-
TODO Only works if key and value are arrays and objects (in their JSON representation!)
-}


type Meta = Data.Aeson.Value
emptyMeta = Data.Aeson.Null

put     :: Stashable a => Meta -> a -> IO ()
put k v = do
  let meta_ = Data.Aeson.encode $ k -- need to wrap to match JSON spec!
  let type_ = Data.Aeson.encode $ Data.Aeson.String $ Data.String.fromString $ show $ Data.Typeable.typeOf v -- Just for sanity at the moment
  let data_ = Data.Aeson.encode v
  let hash  = Data.Hashable.hash k
  let dirPath = stashPath ++ "/" ++ (tail $ show hash) -- get rid of - sign
  System.Directory.createDirectoryIfMissing True stashPath -- i.e: ~/.stash
  System.Directory.createDirectoryIfMissing True dirPath   -- i.e: ~/.stash/2271273
  ByteString.writeFile (dirPath++"/"++"meta.json") meta_
  ByteString.writeFile (dirPath++"/"++"type.json") type_
  ByteString.writeFile (dirPath++"/"++"data.json") data_
  return ()

get     :: Stashable a => Meta -> IO (Maybe a)
get k = fmap (listToMaybe) $ find (== k)


find    :: Stashable a => (Meta -> Bool) -> IO [a]
find = fmap (fmap (\(_,_,x)->x)) . find'

find'    :: Stashable a => (Meta -> Bool) -> IO [(Meta,String,a)]
find' p = do
  stashSubDirectories <- fmap (filter ((> 11) . length)) $ -- get rid of . .. .DS_Store etc
    System.Directory.getDirectoryContents stashPath

  -- TODO warn on type error
  fmap catMaybes $ forM stashSubDirectories $ \subDirectoryName -> do
    let dirPath = stashPath ++ "/" ++ subDirectoryName
    _meta <- SByteString.readFile (dirPath++"/"++"meta.json")
    _type <- SByteString.readFile (dirPath++"/"++"type.json")
    _data <- ByteString.readFile (dirPath++"/"++"data.json")

    let k = (dec _meta :: Data.Aeson.Value)
    let ok  = p k
    let da  = (Data.Aeson.decode $ _data)
    let ty  = getS (dec _type)
    let res = (join $ if ok then Just (fmap (k,ty,) da) else Nothing)
    return res

getType :: Meta -> IO (Maybe String)
getType k = fmap (listToMaybe) $ findType (== k)

findType :: (Meta -> Bool) -> IO [String]
findType = fmap (fmap snd) . findType'

findType' :: (Meta -> Bool) -> IO [(Meta,String)]
findType' p = do
  stashSubDirectories <- fmap (filter ((> 11) . length)) $ -- get rid of . .. .DS_Store etc
    System.Directory.getDirectoryContents stashPath

  -- TODO warn on type error
  fmap catMaybes $ forM stashSubDirectories $ \subDirectoryName -> do
    let dirPath = stashPath ++ "/" ++ subDirectoryName
    _meta <- SByteString.readFile (dirPath++"/"++"meta.json")
    _type <- SByteString.readFile (dirPath++"/"++"type.json")
    _data <- ByteString.readFile (dirPath++"/"++"data.json")

    let k = (dec _meta :: Data.Aeson.Value)
    let ok  = p $ (dec _meta :: Data.Aeson.Value)
    -- TODO
    let ty  = getS (dec _type)
    let res = (if ok then Just (k, ty) else Nothing)
    return res

-- TODO hide
getS (Data.Aeson.String x) = Data.Text.unpack x
    -- print $ _meta
    -- print $ _data
    -- print $ ok
    -- print $ da
    -- print $ res
    -- putStrLn ""
    -- putStrLn ""


dec :: SByteString.ByteString -> Data.Aeson.Value
-- dec = fromJust . Data.Aeson.decode
dec = fromEither . Data.Attoparsec.ByteString.parseOnly Data.Aeson.Parser.value

fromEither (Right x) = x

-- wrapKey :: Meta -> Meta
-- wrapKey = Data.Aeson.Array . return
--
-- unwrapKey = Data.Vector.head . getArray
--   where
--     getArray (Data.Aeson.Array x) = x

-- /.stash/
--   /items/
--     1a238a9aa289a898/ -- hash of meta
--       data.json -- JSON value
--       type.json -- JSON string which is the TypeRep
--       meta.json -- JSON string which is the TypeRep
--

-- js :: String -> Data.Aeson.Value
-- js = fromJust . Data.Aeson.decode . Data.String.fromString
-- 
-- put' k = put (js k)
-- get' k = get (js k)
fromJust (Just x) = x
-- fromRes (Data.Aeson.Success x) = x

stashPath = "/Users/hans/.stash"

-- ---------
-- -- in stash dir, add all, git commit, git pull, git push
-- sync :: IO ()
--
--
--
-- ///////
--
--
-- type Conf = () -- TODO
-- type Stash a = ReaderT Conf STA a
--
-- class (ToJSON a, FromJSON a, Typeable a) => Stashable a where
--   makePreview :: a -> FilePath -> IO Bool
--
-- -- A 128-bit sum or something
-- type Index -- Eq, Ord
--
-- -- Put something in stash, generating a unique index
-- stash :: Stashable a => a -> Stash Index
-- -- Get and remove something from stash
-- unstash :: Stashable a => Index -> Stash a
-- -- Get and put back something to stash
-- peekStash :: Stashable a => Index -> Stash a
--
-- -- View all currently stashed items
-- items :: Stash [(Index, TypeRep)]
-- -- Write a png file containing a thumbnail of the given stashed item to the given path.
-- -- Return whether successful
-- preview :: Index -> FilePath -> Stash Bool
--
--
-- type Name = String
-- collectIndices :: Name -> [Index] -> Stash ()
-- uncollectIndices :: Name -> Stash [Index]
-- peekCollectIndices :: Name -> Stash [Index]
-- -- Combination with stash
-- collect :: Stashable a => Name -> [a] -> Stash ()
-- uncollect :: Stashable a => Name -> Stash [a]
-- peekCollect :: Stashable a => Name -> Stash [a]
--
-- -- Same thing, using single-value collections
-- name :: Stashable a => Name -> a -> Stash a
-- unname :: Stashable a => Name -> Stash [Index]
-- peekName :: Stashable a => Name -> Stash a
--
-- -- TODO various configurations: zip files, gitetc
-- backupStash :: Stash ()
--
--