{-# LANGUAGE DeriveAnyClass            #-}
{-# LANGUAGE DeriveGeneric             #-}
{-# LANGUAGE FlexibleInstances         #-}
{-# LANGUAGE MultiParamTypeClasses     #-}
{-# LANGUAGE NoMonomorphismRestriction #-}
{-# LANGUAGE ScopedTypeVariables       #-}
{-# LANGUAGE TupleSections             #-}

-- |Permanently register and retrieve absolute type definitions
module Network.Top.Repo
  ( RepoProtocol(..)
  , recordType
  , recordADTs
  , solveType
  , solveRefs
  , knownTypes
  ) where

import           Control.Monad
import           Data.Either.Extra (lefts)
import           Data.List         (nub)
import qualified Data.Map          as M
import           Network.Top.Run
import           Network.Top.Types
import           Network.Top.Util
import           Repo.Types
import           ZM

{-|
A (simplistic) protocol to permanently store and retrieve ADT definitions.
-}
data RepoProtocol = Record AbsADT                      -- ^Permanently record an absolute type
                  | Solve AbsRef                       -- ^Retrieve the absolute type
                  | Solved AbsRef AbsADT               -- ^Return the absolute type identified by an absolute reference
                  | AskDataTypes                       -- ^Request the list of all known data types
                  | KnownDataTypes [(AbsRef, AbsADT)]  -- ^Return the list of all known data types
  deriving (Eq, Ord, Show, Generic, Flat, Model)

--instance Flat [(AbsRef,AbsADT)]

type RefSolver = AbsRef -> IO (Either RepoError AbsADT)
-- type TypeSolver = AbsType -> IO (Either RepoError AbsTypeModel)
type RepoError = String -- SomeException

-- |Permanently record all the ADT definitions referred by a type, with all their dependencies
recordType :: Model a => Config -> Proxy a -> IO ()
recordType cfg proxy = recordADTs cfg $ absADTs $ proxy

-- |Permanently record a set of ADT definitions with all their dependencies
recordADTs :: Foldable t => Config -> t AbsADT -> IO ()
recordADTs cfg adts = runApp cfg ByType $ \conn -> mapM_ (output conn . Record) adts

-- |Retrieve all known data types
knownTypes :: Config -> IO (Either String [(AbsRef, AbsADT)])
knownTypes cfg = runApp cfg ByType $ \conn -> do
  output conn AskDataTypes

  let loop = do
        msg <- input conn
        case msg of
          KnownDataTypes ts -> return ts
          _                 -> loop

  withTimeout 30 loop

-- |Retrieve the full type model for the given absolute type
-- from Top's RepoProtocol channel, using the given Repo as a cache
solveType :: Repo -> Config -> AbsType -> IO (Either RepoError AbsTypeModel)
solveType repo cfg t = ((TypeModel t) <$>) <$> solveRefs repo cfg (references t)

-- |Solve ADT references recursively, returning all dependencies.
solveRefs :: Repo -> Config -> [AbsRef] -> IO (Either RepoError (M.Map AbsRef AbsADT))
solveRefs repo cfg refs = runApp cfg ByType $ \conn -> (solveRefsRec repo (resolveRef__ conn)) refs
  where
    solveRefsRec :: Repo -> RefSolver -> [AbsRef] -> IO (Either RepoError (M.Map AbsRef AbsADT))
    solveRefsRec _     _     [] = return $ Right M.empty
    solveRefsRec repo solver refs = do
      er <- allErrs <$> mapM (solveRef repo solver) refs
      case er of
        Left err -> return $ Left err
        Right ros -> (M.union (M.fromList ros) <$>) <$> solveRefsRec repo solver (concatMap (innerReferences . snd) ros)

    allErrs :: [Either String r] -> Either String [r]
    allErrs rs =
      let errs = lefts rs
      in if null errs
         then sequence rs
         else Left (unlines errs)

    solveRef :: Repo -> RefSolver -> AbsRef -> IO (Either RepoError (AbsRef,AbsADT))
    solveRef repo solver ref = ((ref,) <$> )<$> do
      rr <- get repo ref
      case rr of
        Nothing -> solver ref >>= mapM (\o -> put repo o >> return o)
        Just o  -> return $ Right o

resolveRef :: Config -> AbsRef -> IO (Either String AbsADT)
resolveRef cfg ref = checked $ resolveRef_ cfg ref

resolveRef_ :: Config -> AbsRef -> IO (Either String AbsADT)
resolveRef_ cfg ref = runApp cfg ByType (flip resolveRef__ ref)

resolveRef__ :: Connection RepoProtocol -> AbsRef -> IO (Either String AbsADT)
resolveRef__ conn ref = checked $ resolveRef___ conn ref

resolveRef___ :: Connection RepoProtocol -> AbsRef -> IO (Either String AbsADT)
resolveRef___ conn ref = do
    output conn (Solve ref)

    let loop = do
          msg <- input conn
          case msg of
            -- Solved t r | t == typ -> return $ (\e -> AbsoluteType (M.fromList e) typ) <$> r
            Solved sref sadt | ref == sref && absRef sadt == sref -> return $ Right sadt
            _ -> loop

    join <$> withTimeout 25 loop

absADTs :: Model a => Proxy a -> [AbsADT]
absADTs = typeADTs . absTypeModel

checked :: NFData b => IO (Either String b) -> IO (Either String b)
checked f = either (Left . show) id <$> strictTry f