{-# LANGUAGE CPP #-}
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE OverloadedStrings #-}
-----------------------------------------------------------------------------
--
-- Module      :  IDE.TextEditor.Yi
-- Copyright   :  2007-2013 Juergen Nicklisch-Franken, Hamish Mackenzie
-- License     :  GPL Nothing
--
-- Maintainer  :  maintainer@leksah.org
-- Stability   :  provisional
-- Portability :
--
-- |
--
-----------------------------------------------------------------------------

module IDE.TextEditor.Yi (
    Yi(..)

#ifdef LEKSAH_WITH_YI
  , TextEditor(..)
  , EditorBuffer(..)
  , EditorView(..)
  , EditorIter(..)
  , EditorMark(..)
  , EditorTag(..)
  , EditorTagTable(..)

  , newYiBuffer
#endif
) where

import Data.Typeable (Typeable)
import Graphics.UI.Gtk (castToWidget)
import Data.Text (Text)
import qualified Data.Text as T (pack, unpack)

#ifdef LEKSAH_WITH_YI
import IDE.TextEditor.Class (TextEditor(..))
import IDE.Core.Types (IDEM)
import IDE.Core.State (liftYi, onIDE, reflectIDE, liftYiControl)
import qualified Yi.UI.Pango.Control as Yi
       (getBuffer, setText, newView, getText, newBuffer)
import Yi.UI.Pango.Control
       (Control(..), View(..), iterFBufRef, Iter(..), fBufRef, Buffer(..),
        setBufferMode)
import Yi
       (moveToColB, gotoLn, atSol, atEof, atSof, curCol, curLn, readLnB,
        readCharB, nextWordB, moveToEol, rightB, rightN, nextPointB,
        unitWord, atBoundaryB, moveToSol, prevWordB, leftB, readB,
        doUntilB_, prevPointB, Mode, modifyMode, insertingA, undoB,
        markSavedB, setSelectRegionB, redoB, markPointA, insertNAt,
        regionIsEmpty, regionEnd, regionStart, selMark, isUnchangedBuffer,
        Point(..), MarkValue(..), lineOf, pointOfLineColB,
        askMarks, insMark, sizeB, getRawestSelectRegionB, mkRegion,
        deleteRegionB, newMarkB, Mark, pointB, moveTo, savingPointB, Point,
        withGivenBuffer, withEditor, BufferM, BufferRef, Mode(..),
        IndentSettings(..), BufferId(..))
import qualified Yi.Rope as Yi (length, fromText)
import Control.Applicative ((<$>))
import Yi.Keymap.Cua (paste, cut, copy)
import Yi.Buffer.Basic (Direction(..))
import Control.Monad.State.Class (gets)
import Control.Monad (unless)
import Control.Monad.IO.Class (MonadIO(..))
import Data.Time (getCurrentTime)
import qualified Graphics.UI.Gtk as Gtk (Modifier(..))
import IDE.Utils.GUIUtils (fontDescription)
import Graphics.UI.Gtk
       (popupMenuSignal, focusInEvent, menuPopup, menuAttachToWidget,
        menuNew, eventModifier, widgetAddEvents, keyReleaseEvent,
        leaveNotifyEvent, motionNotifyEvent, keyPressEvent,
        buttonReleaseEvent, buttonPressEvent, widgetGrabFocus,
        Rectangle(..), layoutSetFontDescription, EventMask(..),
#ifdef MIN_VERSION_gtk3
        widgetGetWindow
#else
        widgetGetDrawWindow
#endif
        )
import Control.Monad.Reader.Class (MonadReader(..))
import Graphics.UI.Editor.Basics (Connection(..))
import Control.Monad.Trans.Class (MonadTrans(..))
import System.Glib.Signals (on, after)
import Control.Lens (use)
#endif

data Yi = Yi deriving( Typeable, Show )

#ifdef LEKSAH_WITH_YI
newYiBuffer :: Maybe FilePath -> Text -> IDEM (EditorBuffer Yi)
newYiBuffer mbFilename contents = do
    liftYiControl $ do
        let (filename, id) = case mbFilename of
                                Just fn -> (fn, FileBuffer fn)
                                Nothing -> ("Unknown.hs", MemBuffer "*leksah*")
        buffer <- Yi.newBuffer id $ Yi.fromText contents
        setBufferMode filename buffer
        return $ YiBuffer buffer

withYiBuffer' :: BufferRef -> BufferM a -> IDEM a
withYiBuffer' b f = liftYi $ withEditor $ withGivenBuffer b f

withYiBuffer :: Buffer -> BufferM a -> IDEM a
withYiBuffer b f = withYiBuffer' (fBufRef b) f

mkYiIter' :: BufferRef -> Point -> EditorIter Yi
mkYiIter' b p = YiIter $ Iter b p

mkYiIter :: Buffer -> Point -> EditorIter Yi
mkYiIter b p = mkYiIter' (fBufRef b) p

withYiIter :: Iter -> BufferM a -> IDEM a
withYiIter (Iter b p) f = withYiBuffer' b $ do
    savingPointB $ do
        moveTo p
        f

transformYiIter' :: Iter -> BufferM Point -> IDEM (EditorIter Yi)
transformYiIter' i f = mkYiIter' (iterFBufRef i) <$> withYiIter i f

transformYiIter :: Iter -> BufferM a -> IDEM (EditorIter Yi)
transformYiIter i f = transformYiIter' i (f >> pointB)

tryTransformYiIter' :: Iter -> BufferM Point -> IDEM (Maybe (EditorIter Yi))
tryTransformYiIter' i@(Iter b p) f = withYiIter i $ do
    newPoint <- f
    if p == newPoint
        then return Nothing
        else return . Just $ mkYiIter' b newPoint

tryTransformYiIter :: Iter -> BufferM a -> IDEM (Maybe (EditorIter Yi))
tryTransformYiIter i f = tryTransformYiIter' i (f >> pointB)

iterFromYiBuffer' :: BufferRef -> BufferM Point -> IDEM (EditorIter Yi)
iterFromYiBuffer' b f = mkYiIter' b <$> withYiBuffer' b f

iterFromYiBuffer :: Buffer -> BufferM Point -> IDEM (EditorIter Yi)
iterFromYiBuffer b f = iterFromYiBuffer' (fBufRef b) f

instance TextEditor Yi where
    data EditorBuffer Yi = YiBuffer Buffer
    data EditorView Yi = YiView View
    data EditorMark Yi = YiMark Mark
    data EditorIter Yi = YiIter Iter
    data EditorTagTable Yi = YiTagTable
    data EditorTag Yi = YiTag

    newBuffer = newYiBuffer
    applyTagByName (YiBuffer fb) name (YiIter first) (YiIter last) = return () -- TODO
    beginNotUndoableAction (YiBuffer fb) = return () -- TODO
    beginUserAction (YiBuffer fb) = return () -- TODO
    canRedo (YiBuffer fb) = return True -- TODO
    canUndo (YiBuffer fb) = return True -- TODO
    copyClipboard (YiBuffer fb) _ = liftYi $ withEditor $ copy
    createMark (YiBuffer b) (YiIter (Iter _ p)) leftGravity = withYiBuffer b $
        YiMark <$> newMarkB (MarkValue p (if leftGravity then Backward else Forward))
    cutClipboard (YiBuffer fb) clipboard defaultEditable = liftYi $ withEditor $ cut
    delete (YiBuffer b) (YiIter (Iter _ first)) (YiIter (Iter _ last)) =
        withYiBuffer b $ deleteRegionB $ mkRegion first last
    deleteSelection (YiBuffer b) = withYiBuffer b $ do
        region <- getRawestSelectRegionB
        deleteRegionB region -- TODO support flags
    endNotUndoableAction (YiBuffer fb) = return () -- TODO
    endUserAction (YiBuffer fb) = return () -- TODO
    getEndIter (YiBuffer b) = iterFromYiBuffer b sizeB
    getInsertMark (YiBuffer b) = YiMark <$> (withYiBuffer b $ insMark <$> askMarks)
    getIterAtLine (YiBuffer b) line = iterFromYiBuffer b $ pointOfLineColB line 1
    getIterAtMark (YiBuffer b) (YiMark m) = iterFromYiBuffer b $ (use . markPointA) m
    getIterAtOffset (YiBuffer b) offset = return $ mkYiIter b $ Point offset
    getLineCount (YiBuffer b) = withYiBuffer b $ sizeB >>= lineOf
    getModified (YiBuffer b) = not <$> (withYiBuffer b $ gets isUnchangedBuffer)
    getSelectionBoundMark (YiBuffer b) = YiMark . selMark <$> (withYiBuffer b $ askMarks)
    getSelectionBounds (YiBuffer b) = withYiBuffer b $ do
        region <- getRawestSelectRegionB
        return (mkYiIter b (regionStart region),
                mkYiIter b (regionEnd region))
    getInsertIter (YiBuffer b) = withYiBuffer b $ do
        insertMark <- insMark <$> askMarks
        mkYiIter b <$> (use . markPointA) insertMark
    getSlice (YiBuffer b) (YiIter first) (YiIter last) includeHidenChars = liftYiControl $
        Yi.getText b first last
    getStartIter (YiBuffer b) = return $ mkYiIter b $ Point 0
    getTagTable (YiBuffer b) = return YiTagTable -- TODO
    getText (YiBuffer b) (YiIter first) (YiIter last) includeHidenChars = liftYiControl $
        Yi.getText b first last
    hasSelection (YiBuffer b) = withYiBuffer b $ do
        region <- getRawestSelectRegionB
        return $ not $ regionIsEmpty region
    insert (YiBuffer b) (YiIter (Iter _ p)) text = withYiBuffer b $ insertNAt (Yi.fromText text) p
    newView (YiBuffer b) mbFontString = do
        fd <- fontDescription mbFontString
        liftYiControl $ fmap YiView $ Yi.newView b fd
    pasteClipboard (YiBuffer b) clipboard (YiIter (Iter _ p)) defaultEditable = liftYi $ withEditor $ paste
    placeCursor (YiBuffer b) (YiIter (Iter _ p)) = withYiBuffer b $ moveTo p
    redo (YiBuffer b) = withYiBuffer b redoB
    removeTagByName (YiBuffer b) name = return () -- TODO
    selectRange (YiBuffer b) (YiIter (Iter _ first)) (YiIter (Iter _ last)) = withYiBuffer b $
        setSelectRegionB $ mkRegion first last
    setModified (YiBuffer b) modified = unless modified $ do
        now <- liftIO $ getCurrentTime
        withYiBuffer b $ markSavedB now
    setStyle preferDark (YiBuffer b) mbStyle = return () -- TODO
    setText (YiBuffer b) text = liftYiControl $ Yi.setText b (Yi.fromText text)
    undo (YiBuffer b) =  withYiBuffer b undoB
    bufferToWindowCoords (YiView v) point = return point -- TODO
    drawTabs (YiView _) = return () -- TODO
    getBuffer (YiView v) = return $ YiBuffer $ Yi.getBuffer v
#ifdef MIN_VERSION_gtk3
    getWindow (YiView v) = liftIO $ widgetGetWindow (drawArea v)
#else
    getWindow (YiView v) = liftIO $ Just <$> widgetGetDrawWindow (drawArea v)
#endif
    getIterAtLocation (YiView View{viewFBufRef = b}) x y = return $ mkYiIter' b $ Point 0 -- TODO
    getIterLocation (YiView v) (YiIter i) = return $ Rectangle 0 0 0 0 -- TODO
    getOverwrite (YiView View{viewFBufRef = b}) = withYiBuffer' b $ not <$> use insertingA
    getScrolledWindow (YiView v) = return $ scrollWin v
    getEditorWidget (YiView v) = return $ castToWidget $ drawArea v
    grabFocus (YiView View{drawArea = da}) = liftIO $ widgetGrabFocus da
    scrollToMark (YiView v) (YiMark m) withMargin mbAlign = return () -- TODO
    scrollToIter (YiView v) (YiIter i) withMargin mbAlign = return () -- TODO
    setFont (YiView v) mbFontString = do
        fd <- fontDescription mbFontString
        liftIO $ layoutSetFontDescription (layout v) (Just fd)
    setIndentWidth (YiView View{viewFBufRef = b}) width =
        withYiBuffer' b $ modifyMode $
            \ (mode@Mode{modeIndentSettings = mis}) ->
                mode{modeIndentSettings = mis{shiftWidth = width}}
    setWrapMode (YiView View{viewFBufRef = b}) width = return ()
    setRightMargin (YiView v) mbRightMargin = return () -- TODO
    setShowLineNumbers (YiView v) show = return () -- TODO
    setTabWidth (YiView View{viewFBufRef = b}) width =
        withYiBuffer' b $ modifyMode $
            \ (mode@Mode{modeIndentSettings = mis}) ->
                mode{modeIndentSettings = mis{tabSize = width}}

    backwardCharC (YiIter i) = transformYiIter' i prevPointB
    backwardFindCharC (YiIter i) pred mbLimit = tryTransformYiIter i $
        doUntilB_ (pred <$> readB) leftB
    backwardWordStartC (YiIter i@(Iter b p)) = withYiIter i $ do
        prevWordB
        newPoint <- pointB
        if p == newPoint
            then return Nothing
            else return . Just $ mkYiIter' b newPoint
    backwardToLineStartC (YiIter i) = transformYiIter i moveToSol
    endsWord (YiIter i) = withYiIter i $ do
        atBoundaryB unitWord Forward
    forwardCharC (YiIter i) = transformYiIter' i nextPointB
    forwardCharsC (YiIter i) n = transformYiIter i $ rightN n
    forwardFindCharC (YiIter i) pred mbLimit = tryTransformYiIter i $
        doUntilB_ (pred <$> readB) rightB
    forwardSearch (YiIter i) str pred mbLimit = return Nothing -- TODO
    forwardToLineEndC (YiIter i) = transformYiIter i moveToEol
    forwardWordEndC (YiIter i@(Iter b p)) = withYiIter i $ do
        nextWordB
        newPoint <- pointB
        if p == newPoint
            then return Nothing
            else return . Just $ mkYiIter' b newPoint
    getChar (YiIter i) = withYiIter i readCharB
    getCharsInLine (YiIter i) = withYiIter i $ Yi.length <$> readLnB
    getLine (YiIter i) = withYiIter i curLn
    getLineOffset (YiIter i) = withYiIter i curCol
    getOffset (YiIter (Iter _ (Point o))) = return o
    isStart (YiIter i) = withYiIter i atSof
    isEnd (YiIter i) = withYiIter i atEof
    iterEqual (YiIter (Iter _ p1)) (YiIter (Iter _ p2)) = return $ p1 == p2
    startsLine (YiIter i) = withYiIter i atSol
    startsWord (YiIter i) = withYiIter i atSol -- TODO
    atEnd (YiIter (Iter b _)) = iterFromYiBuffer' b sizeB
    atLine (YiIter i) line = transformYiIter i $ gotoLn line
    atLineOffset (YiIter i) column = transformYiIter i $ moveToColB column
    atOffset (YiIter (Iter b _)) offset = return $ YiIter $ Iter b (Point offset)
    atStart (YiIter (Iter b _)) = return $ mkYiIter' b $ Point 0
    newTag (YiTagTable) name = return YiTag -- TODO
    lookupTag (YiTagTable) name = return Nothing -- TODO
    background (YiTag) color = return () -- TODO
    underline (YiTag) value = return () -- TODO
    afterFocusIn (YiView v) f = do
        ideR <- ask
        liftIO $ do
            id1 <- (drawArea v) `after` focusInEvent $ lift $ reflectIDE f ideR >> return False
            return [ConnectC id1]
    afterModifiedChanged (YiBuffer b) f = return [] -- TODO
    afterMoveCursor (YiView v) f = return [] -- TODO
    afterToggleOverwrite (YiView v) f = return [] -- TODO
    onButtonPress (YiView v) f = do
        id1 <- (drawArea v) `onIDE` buttonPressEvent $ f
        return [ConnectC id1]
    onButtonRelease (YiView v) f = do
        id1 <- (drawArea v) `onIDE` buttonReleaseEvent $ f
        return [ConnectC id1]
    onCompletion (YiView v) start cancel = return [] -- TODO
    onKeyPress (YiView v) f = do
        id1 <- (drawArea v) `onIDE` keyPressEvent $ f
        return [ConnectC id1]
    onMotionNotify (YiView v) f = do
        id1 <- (drawArea v) `onIDE` motionNotifyEvent $ f
        return [ConnectC id1]
    onLeaveNotify (YiView v) f = do
        id1 <- (drawArea v) `onIDE` leaveNotifyEvent $ f
        return [ConnectC id1]
    onKeyRelease (YiView v) f = do
        id1 <- (drawArea v) `onIDE` keyReleaseEvent $ f
        return [ConnectC id1]
    onLookupInfo (YiView v) f = do
        liftIO $ (drawArea v) `widgetAddEvents` [ButtonReleaseMask]
        id1 <- (drawArea v) `onIDE` buttonReleaseEvent $ do
            mod <- lift $ eventModifier
            case mod of
                [Gtk.Control] -> f >> return True
                _             -> return False
        return [ConnectC id1]
    onMotionNotifyEvent (YiView v) f = return [] -- TODO
    onPopulatePopup (YiView v) f = do
        ideR <- ask
        liftIO $ do
            id1 <- (drawArea v) `on` popupMenuSignal $ do
                 menu <- menuNew
                 menuAttachToWidget menu (drawArea v)
                 reflectIDE (f menu) ideR
                 menuPopup menu Nothing
                 return True
            return [ConnectC id1]

#endif