{-# LANGUAGE DeriveFunctor     #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE TupleSections     #-}
{-# LANGUAGE TypeFamilies      #-}
{-# OPTIONS_GHC  #-}
module Graphics.UI.Threepenny.Editors.Base
  ( -- * Editors
    Editor(..)
  , edited
  , contents
  , Editable(..)
    -- ** Editor compoosition
  , (|*|), (|*), (*|)
  , (-*-), (-*), (*-)
    -- ** Editor constructors
  , editorReadShow
  , editorEnumBounded
  , editorSum
  , editorJust
  -- * Reexports
  , Compose(..)
  )where

import           Data.Functor.Compose
import           Graphics.UI.Threepenny.Attributes
import           Graphics.UI.Threepenny.Core
import           Graphics.UI.Threepenny.Elements
import           Graphics.UI.Threepenny.Events
import           Graphics.UI.Threepenny.Widgets
import           Text.Read

-- | A widget for editing values of type @a@.
data Editor a = Editor
  { editorTidings :: Tidings a
  , editorElement :: Element
  }
  deriving Functor

instance Widget (Editor a) where
  getElement = editorElement

-- | The class of Editable datatypes.
class Editable a where
  -- | The editor factory
  editor :: Behavior a -> Compose UI Editor a

edited :: Editor a -> Event a
edited = rumors . editorTidings

contents :: Editor a -> Behavior a
contents = facts . editorTidings

infixl 4 |*|, -*-
infixl 5 |*, *|, -*, *-

-- | Left-right editor composition
(|*|) :: Compose UI Editor (b -> a) -> Compose UI Editor b -> Compose UI Editor a
a |*| b = Compose $ do
  a <- getCompose a
  b <- getCompose b
  ab <- row [return $ getElement a, return $ getElement b]
  return $ Editor (editorTidings a <*> editorTidings b) ab

-- | Left-right composition of an element with a editor
(*|) :: UI Element -> Compose UI Editor a -> Compose UI Editor a
e *| a = Compose $ do
  e <- e
  a <- getCompose a
  ea <- row [return e, return $ getElement a]
  return $ Editor (editorTidings a) ea

-- | Left-right composition of an element with a editor
(|*) :: Compose UI Editor a -> UI Element -> Compose UI Editor a
a |* e = Compose $ do
  e <- e
  a <- getCompose a
  ea <- row [
    return $ getElement a, return e]
  return $ Editor (editorTidings a) ea

-- | Top-down editor composition
(-*-) :: Compose UI Editor (b -> a) -> Compose UI Editor b -> Compose UI Editor a
a -*- b = Compose $ do
  a <- getCompose a
  b <- getCompose b
  ab <- column [return $ getElement a, return $ getElement b]
  return $ Editor (editorTidings a <*> editorTidings b) ab

-- | Top-down composition of an element with a editor
(*-) :: UI Element -> Compose UI Editor a -> Compose UI Editor a
e *- a = Compose $ do
  e <- e
  a <- getCompose a
  ea <- column [return e, return $ getElement a]
  return $ Editor (editorTidings a) ea

-- | Top-down composition of an element with a editor
(-*) :: Compose UI Editor a -> UI Element -> Compose UI Editor a
a -* e = Compose $ do
  e <- e
  a <- getCompose a
  ea <- column [return $ getElement a, return e]
  return $ Editor (editorTidings a) ea

editorReadShow :: (Read a, Show a) => Behavior (Maybe a) -> Compose UI Editor (Maybe a)
editorReadShow b = Compose $ do
    e <- getCompose $ editor (maybe "" show <$> b)
    let readIt "" = Nothing
        readIt x  = readMaybe x
    let t = tidings b (readIt <$> edited e)
    return $ Editor t (getElement e)

editorEnumBounded
  :: (Bounded a, Enum a, Ord a, Show a)
  => Behavior(a -> UI Element) -> Behavior (Maybe a) -> Compose UI Editor (Maybe a)
editorEnumBounded display b = Compose $ do
  l <- listBox (pure $ enumFrom minBound) b display
  return $ Editor (tidings b (rumors $ userSelection l)) (getElement l)


editorJust :: (Behavior (Maybe b) -> Compose UI Editor (Maybe b))
  -> Behavior b
  -> Compose UI Editor b
editorJust editor b = Compose $ do
  e <- getCompose $ editor (Just <$> b)
  let ev = filterJust (edited e)
  return $ Editor (tidings b ev) (editorElement e)

data SumWrapper tag a = A {display :: tag, theEditor :: Editor a}

instance Eq  tag  => Eq   (SumWrapper tag a) where A a _ == A b _ = a == b
instance Ord tag  => Ord  (SumWrapper tag a) where compare (A a _) (A b _) = compare a b
instance Show tag => Show (SumWrapper tag a) where show = show . display

-- | An editor for union types, built from editors for its constructors.
editorSum
  :: (Ord tag, Show tag)
  => [(tag, Compose UI Editor a)] -> (a -> tag) -> Behavior a -> Compose UI Editor a
editorSum options selector ba = Compose $ do
  w <- askWindow
  options <- traverse (\(tag, Compose mk) -> (tag,) <$> mk) options
  -- extract the tag from the current value
  let bSelected =
        let build a =
              let tag = selector a
              in A tag <$> lookup tag options
        in build <$> ba
  -- build a tag selector following the current tag
  l <-
    listBox (pure $ fmap (uncurry A) options) bSelected (pure (string . show))
  -- a placeholder for the constructor editor
  nestedEditor <- new
  -- when the user selects a tag, refresh the nested editor
  _ <-
    liftIO $
    register (filterJust $ rumors (userSelection l)) $ \x ->
      runUI w $ set' children [getElement $ theEditor x] nestedEditor
  --
  composed <- column [element l, widget nestedEditor]
  -- the result event fires when any of the nested editors or the tag selector fire.
  let editedEvents = fmap (edited . snd) options
      eTag = filterJust $ fmap display <$> rumors (userSelection l)
      taggedOptions = sequenceA [(tag, ) <$> contents e | (tag, e) <- options]
      editedTag = filterJust $ flip lookup <$> taggedOptions <@> eTag
      editedE = head <$> unions (editedTag : editedEvents)
  return $ Editor (tidings ba editedE) composed

instance Editable () where
  editor b = Compose $ do
    t <- new
    return $ Editor (tidings b never) (getElement t)

instance a ~ Char => Editable [a] where
  editor b = Compose $ do
    w <- askWindow
    t <- entry b
    liftIOLater $ do
      initialValue <- currentValue b
      _ <- runUI w $ set value initialValue (element t)
      return ()
    return $ Editor (userText t) (getElement t)

instance Editable Bool where
  editor b = Compose $ do
    t <- sink checked b $ input # set type_ "checkbox"
    return $ Editor (tidings b $ checkedChange t) t

instance Editable (Maybe Int) where editor = editorReadShow
instance Editable (Maybe Double) where editor = editorReadShow
instance Editable Int where editor = editorJust editor
instance Editable Double where editor = editorJust editor


instance (Editable a, Editable b) => Editable (a,b) where
  editor b = (,) <$> editor (fst <$> b) |*| editor (snd <$> b)