module Gargantext.Hooks.Loader where

import Gargantext.Prelude

import Data.Array as A
import Data.Either (Either(..))
import Data.Generic.Rep (class Generic)
import Data.Maybe (Maybe(..), isJust, maybe)
import Data.Newtype (class Newtype)
import Effect (Effect)
import Effect.Aff (Aff, launchAff_, throwError)
import Effect.Class (liftEffect)
import Effect.Exception (error)
import Gargantext.Components.App.Store (Boxes)
import Gargantext.Components.LoadingSpinner (loadingSpinner)
import Gargantext.Config.REST (RESTError, AffRESTError)
import Gargantext.Config.Utils (handleRESTError)
import Gargantext.Types (FrontendError(..))
import Gargantext.Utils.CacheAPI as GUC
import Gargantext.Utils.Crypto (Hash)
import Gargantext.Utils.Reactix as R2
import Reactix as R
import Simple.JSON as JSON
import Toestand as T

here :: R2.Here
here = R2.here "Gargantext.Hooks.Loader"

cacheName :: String
cacheName = "cache-api-loader"

clearCache :: Unit -> Aff Unit
clearCache _ = GUC.delete $ GUC.CacheName cacheName

type UseLoader path state =
  ( errorHandler :: RESTError -> Effect Unit
  , loader       :: path -> AffRESTError state
  , path         :: path
  , render       :: state -> R.Element
  )

useLoader :: forall path st. Eq path => Eq st
          => Record (UseLoader path st)
          -> R.Hooks R.Element
useLoader { errorHandler, loader: loader', path, render } = do
  state <- T.useBox Nothing

  useLoaderEffect { errorHandler, loader: loader', path, state: state }

  pure $ loader { render, state } []


type LoaderProps st =
  ( render :: st -> R.Element
  , state  :: T.Box (Maybe st) )

loader :: forall st. Eq st => R2.Component (LoaderProps st)
loader = R.createElement loaderCpt
loaderCpt :: forall st. Eq st => R.Component (LoaderProps st)
loaderCpt = here.component "loader" cpt
  where
    cpt { render, state } _ = do
      state' <- T.useLive T.unequal state

      pure $ maybe (loadingSpinner { additionalClass: Nothing }) render state'

type UseLoaderEffect path state =
  ( errorHandler :: RESTError -> Effect Unit
  , loader       :: path -> AffRESTError state
  , path         :: path
  , state        :: T.Box (Maybe state)
  )

useLoaderEffect :: forall st path. Eq path => Eq st
                   => Record (UseLoaderEffect path st)
                   -> R.Hooks Unit
useLoaderEffect { errorHandler, loader: loader', path, state } = do
  state' <- T.useLive T.unequal state
  oPath <- R.useRef path

  R.useEffect2' path state' $ do
    path' <- R.readRefM oPath
    if (path' == path) && (isJust state')
    then pure $ R.nothing
    else do
      R.setRef oPath path
      liftEffect $ T.write_ Nothing state
      R2.affEffect "G.H.Loader.useLoaderEffect" $ do
        l <- loader' path
        case l of
          Left err -> liftEffect $ errorHandler err
          Right l' -> liftEffect $ T.write_ (Just l') state


type UseLoaderBox path state =
  ( errorHandler :: RESTError -> Effect Unit
  , loader       :: path -> AffRESTError state
  , path         :: T.Box path
  , render       :: state -> R.Element
  )

useLoaderBox :: forall path st. Eq path => Eq st
          => Record (UseLoaderBox path st)
          -> R.Hooks R.Element
useLoaderBox { errorHandler, loader: loader', path, render } = do
  path' <- T.useLive T.unequal path

  useLoader { errorHandler, loader: loader', path: path', render }


newtype HashedResponse a = HashedResponse { hash  :: Hash, value :: a }
derive instance Generic (HashedResponse a) _
derive instance Newtype (HashedResponse a) _
derive newtype instance JSON.ReadForeign a => JSON.ReadForeign (HashedResponse a)
derive newtype instance JSON.WriteForeign a => JSON.WriteForeign (HashedResponse a)

type LoaderWithCacheAPIProps path res ret =
  ( boxes          :: Boxes
  , cacheEndpoint  :: path -> AffRESTError Hash
  , handleResponse :: HashedResponse res -> ret
  , mkRequest      :: path -> GUC.Request
  , path           :: path
  , renderer       :: ret -> R.Element
  , spinnerClass   :: Maybe String
  )

useLoaderWithCacheAPI :: forall path res ret.
                         Eq ret => Eq path => JSON.ReadForeign res =>
                         Record (LoaderWithCacheAPIProps path res ret)
                      -> R.Hooks R.Element
useLoaderWithCacheAPI { boxes
                      , cacheEndpoint
                      , handleResponse
                      , mkRequest
                      , path
                      , renderer
                      , spinnerClass } = do
  state <- T.useBox Nothing
  state' <- T.useLive T.unequal state

  useCachedAPILoaderEffect { boxes
                           , cacheEndpoint
                           , handleResponse
                           , mkRequest
                           , path
                           , state }
  pure $ maybe (loadingSpinner { additionalClass: spinnerClass }) renderer state'

type LoaderWithCacheAPIEffectProps path res ret = (
    boxes          :: Boxes
  , cacheEndpoint  :: path -> AffRESTError Hash
  , handleResponse :: HashedResponse res -> ret
  , mkRequest      :: path -> GUC.Request
  , path           :: path
  , state          :: T.Box (Maybe ret)
  )

useCachedAPILoaderEffect :: forall path res ret.
                            Eq ret => Eq path => JSON.ReadForeign res =>
                            Record (LoaderWithCacheAPIEffectProps path res ret)
                         -> R.Hooks Unit
useCachedAPILoaderEffect { boxes: { errors }
                         , cacheEndpoint
                         , handleResponse
                         , mkRequest
                         , path
                         , state } = do
  state' <- T.useLive T.unequal state
  oPath <- R.useRef path

  R.useEffect' $ do
    if (R.readRef oPath == path) && (isJust state') then
      pure unit
    else do
      R.setRef oPath path

      let req = mkRequest path
      -- log2 "[useCachedLoader] mState" mState
      launchAff_ $ do
        cache <- GUC.openCache $ GUC.CacheName cacheName
        -- TODO Parallelize?
        hr@(HashedResponse { hash }) <- GUC.cachedJson cache req
        eCacheReal <- cacheEndpoint path
        handleRESTError errors eCacheReal $ \cacheReal -> do
          val <- if hash == cacheReal then
            pure hr
          else do
            _ <- GUC.deleteReq cache req
            hr'@(HashedResponse { hash: h }) <- GUC.cachedJson cache req
            if h == cacheReal then
              pure hr'
            else do
              let err = "[Hooks.Loader] Fetched clean cache but hashes don't match: " <> h <> " != " <> cacheReal
              liftEffect $ T.modify_ (A.cons $ FStringError { error: err }) errors
              throwError $ error err
          liftEffect $ do
            T.write_ (Just $ handleResponse val) state