packages feed

moffy-samples-0.1.0.2: src/Control/Moffy/Samples/FollowboxAnother.hs

{-# LANGUAGE ImportQualifiedPost #-}
{-# LANGUAGE BlockArguments, LambdaCase, TupleSections, OverloadedStrings #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE ViewPatterns #-}
{-# OPTIONS_GHC -Wall -fno-warn-tabs #-}

module Control.Moffy.Samples.FollowboxAnother (
	-- * followbox
	followbox ) where

import Prelude hiding (break, until, repeat, scanl)

import Control.Arrow ((>>>), second, (***), (&&&))
import Control.Monad (void, forever, (<=<))
import Control.Moffy (React, adjust, adjustSig, emit, waitFor, first, break, until, indexBy, find, repeat, scanl)
import Control.Moffy.Event.Lock (LockId, newLockId, withLock)
import Control.Moffy.Samples.Event.Random (getRandomR)
import Control.Moffy.Samples.Event.Delete (deleteEvent)
import Control.Moffy.Samples.Viewable.Basic (Position, Color(..))
import Control.Moffy.Samples.Followbox.Event (
	SigF, ReactF,
	clearJsons, storeJsons, loadJsons, httpGet, getTimeZone,
	browse, beginSleep, checkBeginSleep, endSleep,
	Error(..), raiseError, checkTerminate )
import Control.Moffy.Samples.Followbox.Clickable (
	Clickable, view, click, clickable, clickableText,
	WithTextExtents, withTextExtents, nextToText, translate,
	FontName, FontSize )
import Control.Moffy.Samples.Followbox.ViewType (
	View(..), View1, white, Png(..), VText(..), Line(..), Image(..) )
import Control.Moffy.Samples.Followbox.TypeSynonym (ErrorMessage, Uri)
import Data.Type.Set
import Data.Type.Flip ((<$%>), (<*%>), ftraverse)
import Data.OneOfThem
import Data.Or (Or(..))
import Data.HashMap.Strict qualified as HM
import Data.Bool
import Data.ByteString qualified as BS
import Data.ByteString.Char8 qualified as BSC
import Data.ByteString.Lazy qualified as LBS
import Data.Text qualified as T
import Data.Time (utcToLocalTime)
import Data.Time.Clock.POSIX (posixSecondsToUTCTime)
import Data.Aeson (Object, Value(..), eitherDecode)
import Data.Aeson.KeyMap (toHashMap)
import Text.Read (readMaybe)
import Codec.Picture qualified as P

import Data.Hashable
import Data.String

import Control.Moffy.Samples.Event.Mouse qualified as Mouse
import Control.Moffy.Samples.Event.Area

import Debug.Trace

---------------------------------------------------------------------------

-- * PARAMETER LIST
-- 	+ NUMBER OF USER TO DISPLAY
-- 	+ MAX NUMBER OF GITHUB USER
-- 	+ BACKGROUND
-- 	+ FONT
-- 	+ AVATAR, NAME AND CROSS
-- * SIG AND REACT
--	+ FOLLOWBOX
--	+ USERS
--	+ GET USER
--	+ GET OBJECT

---------------------------------------------------------------------------
-- PARAMETER LIST
---------------------------------------------------------------------------

-- NUMBER OF USER TO DISPLAY

numOfUsers :: Integer
numOfUsers = 3

-- MAX NUMBER OF GITHUB USER

userPageMax :: Int
userPageMax = 2 ^ (27 :: Int)

-- BACKGROUND

titlePos, nextPos, refreshPos, resetTimePos :: Position
titlePos = (50, 44); nextPos = (500, 44)
refreshPos = (600, 44); resetTimePos = (100, 470)

-- FONT

defaultFont :: FontName
defaultFont = "sans"

middleSize, largeSize :: FontSize
middleSize = 30; largeSize = 36

-- AVATAR, NAME AND CROSS

avatarSizeX, avatarSizeY :: Double
(avatarSizeX, avatarSizeY) = (80, 80)

avatarPos, namePos :: Double -> Position
avatarPos n = (100, 120 + 120 * n)
namePos n = (210, 150 + 120 * n)

crossSize :: Double
crossSize = largeSize / 2

crossPos :: Position -> WithTextExtents -> Position
crossPos p wte = translate (nextToText p wte) wte (1 / 2, 3 / 8)

crossMergin :: Double
crossMergin = 4

---------------------------------------------------------------------------
-- SIG AND REACT
---------------------------------------------------------------------------

-- FOLLOWBOX

followbox :: SigF s AreaView ()
followbox = () <$
	fieldWithResetTime numOfUsers `break` deleteEvent `break` checkTerminate

type AreaView = ([(Int, Maybe (Point, Point))], View)

fieldWithResetTime :: Integer -> SigF s AreaView ()
fieldWithResetTime n = (<>) <$%> field' n <*%> (([] ,) <$%> resetTime)

field' :: Integer -> SigF s AreaView ()
field' n = do
	(nxt, rfs) <- waitFor
		$ (,) <$> link nextPos "Next" <*> link refreshPos "Refresh"
	let	frame = View [title] <> view nxt <> view rfs
--	let	frame = View [title] <> view nxt <> view rfs <> View [expand $ Singleton $ Line' (Color 255 0 0) 30 (30, 30) (100, 100)]
--	let	frame = View [title] <> view nxt <> view rfs <> bar 25
--		clear = emit ([(0, Nothing), (0, Nothing), (0, Nothing)], frame)
		clear = emit ([], frame)
		refresh = forever do
			emit Nothing
			us <- Just . Left <$%> getUsers'
			emit . Just $ Right us
			waitFor . adjust $ click rfs
		clickCrosses lck i = forever do
--			emit =<< waitFor (adjust $ getRandomR (0, 99))
			emit =<< waitFor (withLock lck (adjust $ getRandomR (0, 29) :: ReactF s Int))
--			waitFor $ clickArea ((0, 0 + i * 150), (100, 100 + i * 150))
			waitFor $ (clickArea =<< adjust (getArea i)) `first` click rfs
	lck <- waitFor $ adjust newLockId
	(clear >>) $ second (frame <>) <$%> ((\a b c -> a <> b <> c)
		<$%> (chooseUser 0 <$%> refresh <*%> clickCrosses lck 0)
		<*%> (chooseUser 1 <$%> refresh <*%> clickCrosses lck 1)
		<*%> (chooseUser 2 <$%> refresh <*%> clickCrosses lck 2))
	where
	title = twhite largeSize titlePos "Who to follow"
	link p t = clickableText p
		<$> adjust (withTextExtents defaultFont middleSize t)

bar :: Double -> View
bar p = View [expand . Singleton $ Line' (Color 30 215 10) 50 (80, 300) (80 + p * 25, 300)]

resetTime :: SigF s View ()
resetTime = forever $ emit (View []) >> do
	emit =<< waitFor do
		(t, tz) <- (,) <$> adjust checkBeginSleep <*> adjust getTimeZone
		pure $ View [twhite middleSize resetTimePos . T.pack
			$ "Wait until " <> show (utcToLocalTime tz t)]
	waitFor $ adjust endSleep

twhite :: FontSize -> Position -> T.Text -> View1
twhite fs p = expand . Singleton . Text' white defaultFont fs p

chooseUser :: Integer -> Maybe (Either Int [(Png, T.Text, WithTextExtents)]) -> Int -> AreaView
chooseUser _ (Just (Left i)) _ = ([], bar $ fromIntegral i)
chooseUser (fromIntegral &&& fromIntegral -> (n, n')) (Just (Right us)) i =
	mkUser' n n' (us !! i)
chooseUser (fromIntegral &&& fromIntegral -> (n, n')) Nothing i = -- ([], View [])
	([(n, Nothing)], View [])

-- USERS

mkUser' :: Int -> Double -> (Png, Uri, WithTextExtents) -> AreaView
mkUser' n n' (avt, _uri, wte) =
	let	ap = avatarPos n'; np = namePos n'
		lnk = clickableText np wte; cr = cross $ crossPos np wte
		ara = crossArea $ crossPos np wte
		aview = ([(n, Just ara)], View [expand . Singleton $ Image' ap avt] <> view lnk <> view cr) in
	aview

mkUser :: Int -> Double -> Png -> Uri -> WithTextExtents -> (AreaView, SigF s AreaView())
mkUser n n' avt uri wte =
	let	ap = avatarPos n'; np = namePos n'
		lnk = clickableText np wte; cr = cross $ crossPos np wte
		ara = crossArea $ crossPos np wte
		lsn = waitFor $ listenForUserPage lnk uri
		aview = ([(n, Just ara)], View [expand . Singleton $ Image' ap avt] <> view lnk <> view cr) in
	(aview, lsn)

clickCross :: Int -> ReactF s Int
clickCross i = do
	clickArea =<< adjust (getArea i)
	adjust $ getRandomR (0, 99)

clickArea :: (Point, Point) -> ReactF s ()
clickArea ((l, u), (r, d)) = (() <$)
	. adjust $ find isd $ fst <$%> repeat Mouse.move `indexBy` repeat leftClick
	where isd (x, y) = l <= x && x <= r && u <= y && y <= d

clickOn :: Mouse.Button -> React s (Singleton Mouse.Down) ()
clickOn b0 = do
	b <- Mouse.down
	bool (clickOn b0) (pure ()) (b == b0)

leftClick :: React s (Singleton Mouse.Down) ()
leftClick = clickOn Mouse.ButtonPrimary

listenForUserPage :: Clickable s -> Uri -> ReactF s ()
listenForUserPage nm u = forever $ adjust (click nm) >> adjust (browse u)

crossArea :: Position -> (Position, Position)
crossArea (l, t) = ((l', t'), (r', b'))
	where
	(lt, lb, rt, rb) = ((l, t), (l, b), (r, t), (r, b))
	(r, b) = (l + crossSize, t + crossSize)
	(l', t') = (l - crossMergin, t - crossMergin)
	(r', b') = (r + crossMergin, b + crossMergin)

cross :: Position -> Clickable s
cross (l, t) = clickable (View [lwhite lt rb, lwhite lb rt]) (l', t') (r', b')
	where
	(lt, lb, rt, rb) = ((l, t), (l, b), (r, t), (r, b))
	(r, b) = (l + crossSize, t + crossSize)
	(l', t') = (l - crossMergin, t - crossMergin)
	(r', b') = (r + crossMergin, b + crossMergin)
	lwhite p q = expand . Singleton $ Line' white 4 p q

-- GET USER

getUsers' :: SigF s Int [(Png, T.Text, WithTextExtents)]
getUsers' = mapM (waitFor . userTextExtents) =<< getUsers

userTextExtents ::
	(Png, T.Text, T.Text) -> ReactF s (Png, T.Text, WithTextExtents)
userTextExtents (avt, nm, uri) = do
	wte <- adjust $ withTextExtents defaultFont largeSize nm
	pure (avt, uri, wte)

getUsers :: SigF s Int [(Png, T.Text, T.Text)]
getUsers = waitFor (sequence . map (ex3 . toHashMap) <$> getObjs') >>= err `either` \aunmurls -> do
	let	(aus, nms, urls) = unzip3 aunmurls
	sequence <$> (scanl (+) 0 $ mapM getAvatarPng aus) >>= either err (pure . uncurry3 zip3 . (, nms, urls))
	where
	err e = waitFor (adjust (uncurry raiseError e)) >> getUsers

uncurry3 :: (a -> b -> c -> d) -> (a, b, c) -> d
uncurry3 f (x, y, z) = f x y z

ex3 :: (Hashable k, IsString k, IsString b) =>
	HM.HashMap k Value -> Either (Error, b) (T.Text, T.Text, T.Text)
ex3 o = (,,)
	<$> ex "avatar_url" (NoAvatarAddress, "No Avatar Address")
	<*> ex "login" (NoLoginName, "No Login Name")
	<*> ex "html_url" (NoHtmlUrl, "No HTML URL")
	where
	ex k e = case HM.lookup k o of Just (String v) -> Right v; _ -> Left e

getAvatarPng :: T.Text -> SigF s Int (Either (Error, ErrorMessage) Png)
getAvatarPng url = do
	emit 1
	r <- waitFor $ (<$> adjust (httpGet url))
		$ snd >>> LBS.toStrict >>> convert >>> either
			(Left . (NoAvatar ,))
			(Right . Png avatarSizeX avatarSizeY)
--	pure $!! r
	pure r

convert :: BS.ByteString -> Either String BS.ByteString
convert img = LBS.toStrict . P.encodePng . P.convertRGB8 <$> P.decodeImage img

-- GET OBJECT

getObj1 :: LockId -> ReactF s Object
getObj1 lck = withLock lck $ adjust loadJsons >>= \case
	[] -> getObj1FromWeb
	o : os -> o <$ adjust (storeJsons os)

getObj1FromWeb :: ReactF s Object
getObj1FromWeb = getObjs >>= \case
	Right (o : os) -> o <$ adjust (storeJsons os)
	Right [] -> adjust (raiseError EmptyJson "Empty JSON") >> getObj1FromWeb
	Left em -> adjust (raiseError NotJson em) >> getObj1FromWeb

getObjs' :: ReactF s [Object]
getObjs' = getObjs >>= \case
	Right [] -> adjust (raiseError EmptyJson "Empty JSON") >> getObjs'
	Right os -> pure os
	Left em -> adjust (raiseError NotJson em) >> getObjs'

getObjs :: ReactF s (Either String [Object])
getObjs = do
	n <- adjust $ getRandomR (0, userPageMax)
	(hdr, bdy) <- trace (show n) . adjust . httpGet $ api n
	case (rmng hdr, rst hdr) of
		(Just rmn, _) | rmn > (0 :: Int) -> pure $ eitherDecode bdy
		(Just _, Just t) ->
			adjust (beginSleep t) >> adjust endSleep >> getObjs
		(Just _, Nothing) -> adjust (uncurry raiseError rstE) >> getObjs
		(Nothing, _) -> adjust (uncurry raiseError rmngE) >> getObjs
	where
	api = ("https://api.github.com/users?since=" <>) . T.pack . show @Int
	rmng = (read . BSC.unpack <$>) . lookup "X-RateLimit-Remaining"
	rst = posixSeconds <=< lookup "X-RateLimit-Reset"
	rmngE = (NoRateLimitRemaining, "No X-RateLimit-Remaining header")
	rstE = (NoRateLimitReset, "No X-RateLimit-Reset header")
	posixSeconds = (posixSecondsToUTCTime . fromInteger <$>)
		. readMaybe . BSC.unpack