-- This file is part of Goatee.
--
-- Copyright 2014-2018 Bryan Gardiner
--
-- Goatee is free software: you can redistribute it and/or modify
-- it under the terms of the GNU Affero General Public License as published by
-- the Free Software Foundation, either version 3 of the License, or
-- (at your option) any later version.
--
-- Goatee is distributed in the hope that it will be useful,
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-- GNU Affero General Public License for more details.
--
-- You should have received a copy of the GNU Affero General Public License
-- along with Goatee.  If not, see <http://www.gnu.org/licenses/>.

-- | A panel that displays a vertical list of controls for editing a game's
-- 'GameInfo'.
module Game.Goatee.Ui.Gtk.GamePropertiesPanel (
  GamePropertiesPanel,
  create,
  destroy,
  myWidget,
  ) where

import Control.Arrow (first)
import Control.Monad (forM_, void, when)
import Control.Monad.Trans (liftIO)
import Data.IORef (newIORef, readIORef, writeIORef)
import Data.Maybe (fromMaybe)
import qualified Game.Goatee.Common.Bigfloat as BF
import Game.Goatee.Lib.Board
import Game.Goatee.Lib.Monad hiding (on)
import Game.Goatee.Lib.Types
import Game.Goatee.Ui.Gtk.Common
import Game.Goatee.Ui.Gtk.Utils
import Game.Goatee.Ui.Gtk.Widget
import Graphics.UI.Gtk (
  AttachOptions (Expand, Fill),
  AttrOp ((:=)),
  CheckButton,
  Label,
  Packing (PackGrow, PackNatural),
  PolicyType (PolicyAutomatic, PolicyNever),
  TextView,
  Widget,
  WidgetClass,
  boxPackStart,
  checkButtonNewWithLabel,
  containerAdd,
  entryWidthChars,
  focusOutEvent,
  get,
  hBoxNew,
  hSeparatorNew,
  labelNew, labelSetMnemonicWidget, labelSetText,
  on,
  scrolledWindowAddWithViewport, scrolledWindowNew, scrolledWindowSetPolicy,
  set,
  spinButtonSetDigits,
  tableAttach, tableAttachDefaults, tableNew, tableSetRowSpacing,
  textViewNew,
  toggleButtonActive, toggled,
  toWidget,
  vBoxNew,
  widgetSetSensitive, widgetSetSizeRequest,
  )

-- A getter for 'Stringlike' 'GameInfo' fields.
data InfoGetter = forall a. Stringlike a => InfoGetter (GameInfo -> Maybe a)

data GamePropertiesPanel ui = GamePropertiesPanel
  { myUi :: ui
  , myState :: ViewState
  , myWidget :: Widget

    -- Black's info.
  , myBlackNameEntry :: GoateeEntry
  , myBlackRankEntry :: GoateeEntry
  , myBlackTeamEntry :: GoateeEntry

    -- White's info.
  , myWhiteNameEntry :: GoateeEntry
  , myWhiteRankEntry :: GoateeEntry
  , myWhiteTeamEntry :: GoateeEntry

    -- Game rules.
  , myRulesetEntry :: GoateeEntry
  , myMainTimeSpin :: GoateeSpinButton
  , myMainTimeLabel :: Label
  , myOvertimeEntry :: GoateeEntry
  , myGameResultDisplayCheck :: CheckButton
  , myGameResultEntry :: GoateeEntry

    -- Game editors.
  , myGameAnnotatorEntry :: GoateeEntry
  , myGameEntererEntry :: GoateeEntry

    -- Game context.
  , myEventNameEntry :: GoateeEntry
  , myGamePlaceEntry :: GoateeEntry
  , myGameRoundEntry :: GoateeEntry
  , myGameDatesEntry :: GoateeEntry
  , myGameNameEntry :: GoateeEntry
  , myGameSourceEntry :: GoateeEntry
  , myGameCopyrightEntry :: GoateeEntry

    -- Further commentary.
  , myGameOpeningEntry :: GoateeEntry
  , myGameCommentTextView :: TextView
  , myGameCommentTextViewSetter :: String -> IO ()
  }

instance UiCtrl go ui => UiView go ui (GamePropertiesPanel ui) where
  viewName = const "GamePropertiesPanel"
  viewCtrl = myUi
  viewState = myState
  viewUpdate = update

create :: UiCtrl go ui => ui -> IO (GamePropertiesPanel ui)
create ui = do
  let rows = 27
      cols = 2
  scroll <- scrolledWindowNew Nothing Nothing
  box <- vBoxNew False 0
  table <- tableNew rows cols False
  scrolledWindowSetPolicy scroll PolicyNever PolicyAutomatic
  scrolledWindowAddWithViewport scroll box
  boxPackStart box table PackNatural 0

  nextRowRef <- newIORef 0
  let nextRow = do
        row <- readIORef nextRowRef
        writeIORef nextRowRef $ row + 1
        return row
      addSeparator = do
        row <- nextRow
        sep <- hSeparatorNew
        tableAttachDefaults table sep 0 cols row (row + 1)
        tableSetRowSpacing table (row - 1) 6
        tableSetRowSpacing table row 6
      addWidget :: WidgetClass widget => String -> widget -> IO widget
      addWidget labelText widget = do
        row <- nextRow
        label <- labelNew $ Just labelText
        labelSetMnemonicWidget label widget
        tableAttach table label 0 1 row (row + 1)
                    [Fill] [] 0 0
        widgetSetSizeRequest widget 0 (-1)
        tableAttach table widget 1 2 row (row + 1)
                    [Expand, Fill] [] 0 0
        return widget
      addWideWidget :: WidgetClass widget => widget -> IO widget
      addWideWidget widget = do
        row <- nextRow
        widgetSetSizeRequest widget 0 (-1)
        tableAttachDefaults table widget 0 2 row (row + 1)
        return widget
      addEntry :: String -> IO GoateeEntry
      addEntry labelText = do
        entry <- goateeEntryNew
        let widget = goateeEntryWidget entry
        set widget [entryWidthChars := 0]
        addWidget labelText widget
        return entry

  blackNameEntry <- addEntry "Black"
  blackRankEntry <- addEntry "Rank"
  blackTeamEntry <- addEntry "Team"
  addSeparator
  whiteNameEntry <- addEntry "White"
  whiteRankEntry <- addEntry "Rank"
  whiteTeamEntry <- addEntry "Team"
  addSeparator
  rulesetEntry <- addEntry "Ruleset"

  mainTimeBox <- hBoxNew True 0
  mainTimeSpin <- goateeSpinButtonNewWithRange 0 3155692600 {- 100 years -} 1
  let mainTimeSpinWidget = goateeSpinButtonWidget mainTimeSpin
  spinButtonSetDigits mainTimeSpinWidget 1
  mainTimeLabel <- labelNew (Nothing :: Maybe String)
  boxPackStart mainTimeBox mainTimeSpinWidget PackGrow 0
  boxPackStart mainTimeBox mainTimeLabel PackNatural 0
  addWidget "Time" mainTimeBox

  overtimeEntry <- addEntry "Overtime"
  gameResultDisplayCheck <- addWideWidget =<< checkButtonNewWithLabel "Show game result"
  gameResultEntry <- addEntry "Result"
  addSeparator
  gameAnnotatorEntry <- addEntry "Annotator"
  gameEntererEntry <- addEntry "Enterer"
  addSeparator
  eventNameEntry <- addEntry "Event"
  gamePlaceEntry <- addEntry "Place"
  gameRoundEntry <- addEntry "Round"
  gameDatesEntry <- addEntry "Dates"
  gameNameEntry <- addEntry "Name"
  gameSourceEntry <- addEntry "Source"
  gameCopyrightEntry <- addEntry "Copyright"
  addSeparator
  gameOpeningEntry <- addEntry "Opening"
  addWideWidget =<< labelNew (Just "Game comment:")

  gameCommentScroll <- scrolledWindowNew Nothing Nothing
  scrolledWindowSetPolicy scroll PolicyAutomatic PolicyAutomatic
  gameCommentTextView <- textViewNew
  -- Set a minimum height on the text view, otherwise it's too short.
  widgetSetSizeRequest gameCommentTextView 0 175
  containerAdd gameCommentScroll gameCommentTextView
  boxPackStart box gameCommentScroll PackNatural 0

  gameCommentTextViewSetter <- textViewConfigure gameCommentTextView $ \value ->
    doUiGo ui $ void $ modifyGameInfo $ \info ->
    info { gameInfoGameComment = if null value then Nothing else Just $ stringToSgf value }

  addedRowCount <- readIORef nextRowRef
  when (addedRowCount /= rows) $ fail $
    "GamePropertiesPanel: Table expected " ++ show rows ++ " rows, got " ++
    show addedRowCount ++ "."

  state <- viewStateNew

  let me = GamePropertiesPanel
        { myUi = ui
        , myState = state
        , myWidget = toWidget scroll

        , myBlackNameEntry = blackNameEntry
        , myBlackRankEntry = blackRankEntry
        , myBlackTeamEntry = blackTeamEntry

        , myWhiteNameEntry = whiteNameEntry
        , myWhiteRankEntry = whiteRankEntry
        , myWhiteTeamEntry = whiteTeamEntry

        , myRulesetEntry = rulesetEntry
        , myMainTimeSpin = mainTimeSpin
        , myMainTimeLabel = mainTimeLabel
        , myOvertimeEntry = overtimeEntry
        , myGameResultDisplayCheck = gameResultDisplayCheck
        , myGameResultEntry = gameResultEntry

        , myGameAnnotatorEntry = gameAnnotatorEntry
        , myGameEntererEntry = gameEntererEntry

        , myEventNameEntry = eventNameEntry
        , myGamePlaceEntry = gamePlaceEntry
        , myGameRoundEntry = gameRoundEntry
        , myGameDatesEntry = gameDatesEntry
        , myGameNameEntry = gameNameEntry
        , myGameSourceEntry = gameSourceEntry
        , myGameCopyrightEntry = gameCopyrightEntry

        , myGameOpeningEntry = gameOpeningEntry
        , myGameCommentTextView = gameCommentTextView
        , myGameCommentTextViewSetter = gameCommentTextViewSetter
        }

  initialize me
  return me

initialize :: UiCtrl go ui => GamePropertiesPanel ui -> IO ()
initialize me = do
  register me [AnyEvent gameInfoChangedEvent]

  viewUpdate me

  connectEntry me (myBlackNameEntry me) gameInfoBlackName $ \x info ->
    info { gameInfoBlackName = x }
  connectEntry me (myBlackRankEntry me) gameInfoBlackRank $ \x info ->
    info { gameInfoBlackRank = x }
  connectEntry me (myBlackTeamEntry me) gameInfoBlackTeamName $ \x info ->
    info { gameInfoBlackTeamName = x }

  connectEntry me (myWhiteNameEntry me) gameInfoWhiteName $ \x info ->
    info { gameInfoWhiteName = x }
  connectEntry me (myWhiteRankEntry me) gameInfoWhiteRank $ \x info ->
    info { gameInfoWhiteRank = x }
  connectEntry me (myWhiteTeamEntry me) gameInfoWhiteTeamName $ \x info ->
    info { gameInfoWhiteTeamName = x }

  connectEntry me (myRulesetEntry me) gameInfoRuleset $ \x info -> info { gameInfoRuleset = x }
  connect me (goateeSpinButtonOnSpinned $ myMainTimeSpin me) $ \x info ->
    info { gameInfoBasicTimeSeconds = if x == 0 then Nothing else Just x }
  connectEntry me (myOvertimeEntry me) gameInfoOvertime $ \x info -> info { gameInfoOvertime = x }
  let gameResultCheck = myGameResultDisplayCheck me
  on gameResultCheck toggled $ viewUpdate me
  connectEntry' me (myGameResultEntry me) (get gameResultCheck toggleButtonActive) gameInfoResult $
    \x info -> info { gameInfoResult = x }

  connectEntry me (myGameAnnotatorEntry me) gameInfoAnnotatorName $ \x info ->
    info { gameInfoAnnotatorName = x }
  connectEntry me (myGameEntererEntry me) gameInfoEntererName $ \x info ->
    info { gameInfoEntererName = x }

  connectEntry me (myEventNameEntry me) gameInfoEvent $ \x info -> info { gameInfoEvent = x }
  connectEntry me (myGamePlaceEntry me) gameInfoPlace $ \x info -> info { gameInfoPlace = x }
  connectEntry me (myGameRoundEntry me) gameInfoRound $ \x info -> info { gameInfoRound = x }
  connectEntry me (myGameDatesEntry me) gameInfoDatesPlayed $ \x info ->
    info { gameInfoDatesPlayed = x }
  connectEntry me (myGameNameEntry me) gameInfoGameName $ \x info -> info { gameInfoGameName = x }
  connectEntry me (myGameSourceEntry me) gameInfoSource $ \x info -> info { gameInfoSource = x }
  connectEntry me (myGameCopyrightEntry me) gameInfoCopyright $ \x info ->
    info { gameInfoCopyright = x }

  connectEntry me (myGameOpeningEntry me) gameInfoOpeningComment $ \x info ->
    info { gameInfoOpeningComment = x }

-- | @connectEntry me entry getter setter@ binds an 'Entry' to a field
-- in the current 'GameInfo' so that changes in one affect the other.
-- Empty strings are coerced to 'Nothing' and vice versa.
--
-- When an 'Entry' is changed, we immediately write the value back to
-- the model: we can't wait until focus out because e.g. opening menus
-- doesn't fire focus-out events.  We also inhibit this widget's model
-- change handler assigning back to the entry, because e.g. we don't
-- want to canonicalize "B+1.0" to "B+1" as soon as it's typed.
-- Instead, we do the model-to-view canonicalizing update on
-- focus-out.
connectEntry :: (UiCtrl go ui, Stringlike a)
             => GamePropertiesPanel ui
             -> GoateeEntry
             -> (GameInfo -> Maybe a)
             -> (Maybe a -> GameInfo -> GameInfo)
             -> IO ()
connectEntry me entry = connectEntry' me entry (return True)

-- | This is like 'connectEntry'.  The additional @IO Bool@ can return false to
-- indicate that a change in the entry should not be written to the model;
-- returning true writes the change.
connectEntry' :: (UiCtrl go ui, Stringlike a)
              => GamePropertiesPanel ui
              -> GoateeEntry
              -> IO Bool
              -> (GameInfo -> Maybe a)
              -> (Maybe a -> GameInfo -> GameInfo)
              -> IO ()
connectEntry' me entry propagateChangesToModel modelGetter modelSetter = do
  let ui = myUi me
  goateeEntryOnChange entry $ \value -> do
    propagate <- propagateChangesToModel
    when propagate $ doUiGo ui $ void $ modifyGameInfo $ modelSetter $
      if null value then Nothing else Just $ stringToSgf value
  on (goateeEntryWidget entry) focusOutEvent $ liftIO $ do
    cursor <- readCursor ui
    goateeEntrySetText entry $ maybe "" sgfToString $ modelGetter $
      boardGameInfo $ cursorBoard cursor
    return False
  return ()

-- | @connect me connectFn setter@ binds a widget to a field in the current
-- 'GameInfo' so that changes in one affect the other.  @connect@ constructs an
-- @a -> IO ()@ handler that takes a value and puts it into the model using
-- @setter@, and immediately gives it to @connectFn@, which is in charge of
-- registering the handler.
connect :: UiCtrl go ui
        => GamePropertiesPanel ui
        -> ((a -> IO ()) -> IO ())
        -> (a -> GameInfo -> GameInfo)
        -> IO ()
connect me connectFn modelSetter =
  let ui = myUi me
  in connectFn $ \newValue -> doUiGo ui $ void $ modifyGameInfo $ modelSetter newValue

destroy :: UiCtrl go ui => GamePropertiesPanel ui -> IO ()
destroy = viewDestroy

update :: UiCtrl go ui => GamePropertiesPanel ui -> IO ()
update me = do
  cursor <- readCursor $ myUi me
  let info = boardGameInfo $ cursorBoard cursor
  forM_ [ (InfoGetter gameInfoBlackName, myBlackNameEntry)
        , (InfoGetter gameInfoBlackRank, myBlackRankEntry)
        , (InfoGetter gameInfoBlackTeamName, myBlackTeamEntry)

        , (InfoGetter gameInfoWhiteName, myWhiteNameEntry)
        , (InfoGetter gameInfoWhiteRank, myWhiteRankEntry)
        , (InfoGetter gameInfoWhiteTeamName, myWhiteTeamEntry)

        , (InfoGetter gameInfoRuleset, myRulesetEntry)
        , (InfoGetter gameInfoOvertime, myOvertimeEntry)

        , (InfoGetter gameInfoAnnotatorName, myGameAnnotatorEntry)
        , (InfoGetter gameInfoEntererName, myGameEntererEntry)

        , (InfoGetter gameInfoEvent, myEventNameEntry)
        , (InfoGetter gameInfoPlace, myGamePlaceEntry)
        , (InfoGetter gameInfoRound, myGameRoundEntry)
        , (InfoGetter gameInfoDatesPlayed, myGameDatesEntry)
        , (InfoGetter gameInfoGameName, myGameNameEntry)
        , (InfoGetter gameInfoSource, myGameSourceEntry)
        , (InfoGetter gameInfoCopyright, myGameCopyrightEntry)

        , (InfoGetter gameInfoOpeningComment, myGameOpeningEntry)
        ] $ \(InfoGetter getter, entry) ->
    goateeEntrySetText (entry me) $ extractStringlike $ getter info

  -- Update the "main time" spinner.
  goateeSpinButtonSetValue (myMainTimeSpin me) $ fromMaybe 0 $
    gameInfoBasicTimeSeconds info
  labelSetText (myMainTimeLabel me) $ maybe "" renderSeconds $
    gameInfoBasicTimeSeconds info

  -- Update the game result entry.
  let gameResultEntry = myGameResultEntry me
  displayGameResult <- get (myGameResultDisplayCheck me) toggleButtonActive
  widgetSetSensitive (goateeEntryWidget gameResultEntry) displayGameResult
  goateeEntrySetText gameResultEntry $
    if displayGameResult
    then extractStringlike $ gameInfoResult info
    else "(hidden)"

  -- Update the game comment TextView.
  myGameCommentTextViewSetter me $ maybe "" fromText $ gameInfoGameComment info

extractStringlike :: Stringlike a => Maybe a -> String
extractStringlike = maybe "" sgfToString

renderSeconds :: BF.Bigfloat -> String
renderSeconds totalSecondsFloat =
  let isNegative = totalSecondsFloat < 0
      (wholeSeconds, fractionalSecondsStr) = first abs $ splitFloat totalSecondsFloat
      (totalMinutes, seconds) = wholeSeconds `divMod` 60
      (hours, minutes) = totalMinutes `divMod` 60
  in (if isNegative then ('-':) else id) $
     (if hours > 0
      then show hours ++ ':' : show2 minutes ++ ':' : show2 seconds
      else show minutes ++ ':' : show2 seconds) ++
     fractionalSecondsStr
  where show2 n = if n < 10 then '0' : show n else show n

-- | Returns a pair containing the signed whole part of a 'BF.Bigfloat', plus a
-- string containing a decimal place and everything after it, if the float has a
-- fractional part, otherwise an empty string.
splitFloat :: BF.Bigfloat -> (Integer, String)
splitFloat x =
  let xs = show x
      (addNeg, xs') = case xs of
        '-':xs' -> (('-':), xs')
        _ -> (id, xs)
      (hd, tl) = break (== '.') xs'
  in (read $ addNeg hd, tl)