{-# language DerivingStrategies         #-}
{-# language GeneralizedNewtypeDeriving #-}
{-# language LambdaCase                 #-}
{-# language ScopedTypeVariables        #-}
{-# language TypeFamilies               #-}

module Termbox.Banana
  ( -- $intro
    TermboxEvent
  , run
  , run_
  , set
  , Scene(..)
  , Cells
  , Cursor(..)
    -- * Re-exports
  , Termbox.black
  , Termbox.red
  , Termbox.green
  , Termbox.yellow
  , Termbox.blue
  , Termbox.magenta
  , Termbox.cyan
  , Termbox.white
  , Termbox.bold
  , Termbox.underline
  , Termbox.reverse
  , Termbox.Attr
  , Termbox.Cell(..)
  , Termbox.Event(..)
  , Termbox.InitError(..)
  , Termbox.InputMode(..)
  , Termbox.Key(..)
  , Termbox.Mouse(..)
  , Termbox.MouseMode(..)
  , Termbox.OutputMode(..)
    -- * Example
    -- $example
  ) where

import Control.Concurrent.MVar
import Control.Exception (throwIO)
import Data.Function (fix)
import Reactive.Banana
import Reactive.Banana.Frameworks

import qualified Termbox


-- $intro
-- See the bottom of this module for a simple, runnable example to get started.
--
-- Here's how to run the examples with @cabal@:
--
-- @
-- cabal v2-run --constraint "termbox-banana +build-examples" termbox-banana-example-echo
-- cabal v2-run --constraint "termbox-banana +build-examples" termbox-banana-example-hoogle
-- @
--
-- This module is intended to be imported qualified.
--
-- @
-- import qualified Termbox.Banana as Termbox
-- @

-- | A @termbox@ event. This type alias exists only for Haddock readability;
-- in code, you are encouraged to use
--
-- * @Event@ for @reactive-banana@ events
-- * @Termbox.Event@ for @termbox@ events
--
-- @since 0.1.0
type TermboxEvent
  = Termbox.Event

-- | A scene to render; a grid of cells and a cursor.
--
-- @since 0.1.0
data Scene
  = Scene !Cells !Cursor

-- | A grid of cells.
--
-- Create a 'Cells' with 'set' or 'mempty' and combine them with ('<>').
--
-- @since 0.1.0
newtype Cells
  = Cells (IO ())
  deriving newtype (Monoid, Semigroup)

-- | A cursor.
--
-- @since 0.1.0
data Cursor
  = Cursor !Int !Int -- ^ Column, then row
  | NoCursor

-- | Set a single cell's value (column, then row).
--
-- @since 0.1.0
set :: Int -> Int -> Termbox.Cell -> Cells
set x y z =
  Cells (Termbox.set x y z)

type EventSource a
  = (AddHandler a, a -> IO ())

-- | Run a @termbox@ program with the specified input and output modes.
--
-- Given
--
-- * the terminal event stream
-- * the time-varying terminal size (width, then height)
--
-- return
--
-- * a time-varying scene to render
-- * an event stream of arbitrary values, only the first of which is relevant,
--   which ends the @termbox@ program and returns from the @main@ action.
--
-- @since 0.2.0
run
  :: Termbox.InputMode -- ^
  -> Termbox.OutputMode -- ^
  -> (  Event TermboxEvent
     -> Behavior (Int, Int)
     -> MomentIO (Behavior Scene, Event a))
  -> IO (Either Termbox.InitError a)
run imode omode program =
  Termbox.run $ do
    Termbox.setInputMode imode
    Termbox.setOutputMode omode

    doneVar :: MVar a <-
      newEmptyMVar

    (eventAddHandler, fireEvent) :: EventSource TermboxEvent <-
      newAddHandler

    network :: EventNetwork <-
      compile $ do
        eEvent :: Event TermboxEvent <-
          fromAddHandler eventAddHandler

        let
          eResize :: Event (Int, Int)
          eResize =
            filterJust
              ((\case
                Termbox.EventResize w h -> Just (w, h)
                _ -> Nothing)
              <$> eEvent)

        bSize :: Behavior (Int, Int) <-
          flip stepper eResize =<<
            liftIO Termbox.getSize

        moment program eEvent bSize (putMVar doneVar)

    actuate network

    fix $ \loop -> do
      Termbox.poll >>= fireEvent
      tryReadMVar doneVar >>=
        maybe loop pure

-- | Like 'run', but throws 'Termbox.InitError's as @IO@ exceptions.
--
-- @since 0.2.0
run_
  :: Termbox.InputMode -- ^
  -> Termbox.OutputMode -- ^
  -> (  Event TermboxEvent
     -> Behavior (Int, Int)
     -> MomentIO (Behavior Scene, Event a))
  -> IO a
run_ imode omode program =
  run imode omode program >>= either throwIO pure

moment
  :: (  Event TermboxEvent
     -> Behavior (Int, Int)
     -> MomentIO (Behavior Scene, Event a))
  -> Event TermboxEvent
  -> Behavior (Int, Int)
  -> (a -> IO ())
  -> MomentIO ()
moment program eEvent bSize abort = do
  (bScene, eDone) :: (Behavior Scene, Event a) <-
    program eEvent bSize

  eScene :: Event (Future Scene) <-
    changes bScene

  let
    render :: Scene -> IO ()
    render (Scene (Cells cells) cursor) = do
      Termbox.clear mempty mempty
      cells
      case cursor of
        Cursor c r -> Termbox.setCursor c r
        NoCursor -> Termbox.hideCursor
      Termbox.flush

  liftIO . render =<< valueB bScene
  reactimate (abort <$> eDone)
  reactimate' ((fmap.fmap) render eScene)

-- $example
--
-- Below is a sample program that simply displays the last key pressed, and
-- quits on @Esc@:
--
-- @
-- {-\# LANGUAGE LambdaCase          \#-}
-- {-\# LANGUAGE ScopedTypeVariables \#-}
--
-- module Main where
--
-- import Reactive.Banana
-- import Reactive.Banana.Frameworks
--
-- import qualified Termbox.Banana as Termbox
--
-- main :: IO ()
-- main =
--   Termbox.'run_'
--     (Termbox.'Termbox.InputModeEsc' Termbox.'Termbox.MouseModeNo')
--     Termbox.'Termbox.OutputModeNormal'
--     moment
--
-- moment
--   :: Event Termbox.'Termbox.Event'
--   -> Behavior (Int, Int)
--   -> MomentIO (Behavior Termbox.'Termbox.Scene', Event ())
-- moment eEvent _bSize = do
--   let
--     eQuit :: Event ()
--     eQuit =
--       () <$ filterE isKeyEsc eEvent
--
--   bLatestEvent :: Behavior (Maybe Termbox.'Termbox.Event') <-
--     stepper
--       Nothing
--       (Just \<$\> eEvent)
--
--   let
--     bCells :: Behavior Termbox.'Termbox.Cells'
--     bCells =
--       maybe mempty renderEvent \<$\> bLatestEvent
--
--   let
--     bScene :: Behavior Termbox.'Termbox.Scene'
--     bScene =
--       Termbox.'Termbox.Scene'
--         \<$\> bCells
--         \<*\> pure Termbox.'Termbox.NoCursor'
--
--   pure (bScene, eQuit)
--
-- renderEvent :: Termbox.'Termbox.Event' -> Termbox.'Termbox.Cells'
-- renderEvent =
--   foldMap (\(i, c) -> Termbox.set i 0 (Termbox.'Termbox.Cell' c mempty mempty))
--     . zip [0..]
--     . show
--
-- isKeyEsc :: Termbox.'Termbox.Event' -> Bool
-- isKeyEsc = \case
--   Termbox.'Termbox.EventKey' Termbox.'Termbox.KeyEsc' _ -> True
--   _ -> False
-- @