{-# LANGUAGE CPP                #-}
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE DeriveGeneric      #-}
{-# LANGUAGE TemplateHaskell    #-}
{-# OPTIONS_HADDOCK show-extensions #-}

-- |
-- Module      :  Yi.Keymap.Vim.Common
-- License     :  GPL-2
-- Maintainer  :  yi-devel@googlegroups.com
-- Stability   :  experimental
-- Portability :  portable
--
-- Common types used by the vim keymap.

module Yi.Keymap.Vim.Common
    ( VimMode(..)
    , VimBinding(..)
    , GotoCharCommand(..)
    , VimState(..)
    , Register(..)
    , RepeatToken(..)
    , RepeatableAction(..)
    , MatchResult(..)
    , EventString(..), unEv
    , OperatorName(..), unOp
    , RegisterName
    , module Yi.Keymap.Vim.MatchResult
    , lookupBestMatch, matchesString
    ) where

import           GHC.Generics              (Generic)

import           Control.Applicative       (Alternative ((<|>)), (<$>))
import           Control.Lens              (makeLenses)
import           Data.Binary               (Binary (..))
import           Data.Default              (Default (..))
import qualified Data.HashMap.Strict       as HM (HashMap)
import           Data.Monoid               (Monoid (mappend, mempty), (<>))
import           Data.String               (IsString (..))
import qualified Data.Text                 as T (Text, isPrefixOf, pack)
import qualified Data.Text.Encoding        as E (decodeUtf8, encodeUtf8)
import           Data.Typeable             (Typeable)
import           Yi.Buffer.Adjusted        (Direction, Point, RegionStyle)
import           Yi.Editor                 (EditorM)
import           Yi.Keymap                 (YiM)
import           Yi.Keymap.Vim.MatchResult (MatchResult (..))
import           Yi.Rope                   (YiString)
import           Yi.Types                  (YiVariable)


newtype EventString = Ev { _unEv :: T.Text } deriving (Show, Eq, Ord)

instance IsString EventString where
  fromString = Ev . T.pack

newtype OperatorName = Op { _unOp :: T.Text } deriving (Show, Eq)

instance IsString OperatorName where
  fromString = Op . T.pack

instance Monoid EventString where
  mempty = Ev mempty
  Ev t `mappend` Ev t' = Ev $ t <> t'

instance Monoid OperatorName where
  mempty = Op mempty
  Op t `mappend` Op t' = Op $ t <> t'

instance Binary EventString where
  get = Ev . E.decodeUtf8 <$> get
  put (Ev t) = put $ E.encodeUtf8 t

instance Binary OperatorName where
  get = Op . E.decodeUtf8 <$> get
  put (Op t) = put $ E.encodeUtf8 t

makeLenses ''EventString
makeLenses ''OperatorName

-- 'lookupBestMatch' and 'matchesString' pulled out of MatchResult
-- module to prevent cyclic dependencies. Screw more bootfiles.
lookupBestMatch :: EventString -> [(EventString, a)] -> MatchResult a
lookupBestMatch key = foldl go NoMatch
    where go m (k, x) = m <|> fmap (const x) (key `matchesString` k)

matchesString :: EventString -> EventString -> MatchResult ()
matchesString (Ev got) (Ev expected)
  | expected == got = WholeMatch ()
  | got `T.isPrefixOf` expected = PartialMatch
  | otherwise = NoMatch

type RegisterName = Char
type MacroName = Char

data RepeatableAction = RepeatableAction
    { raPreviousCount :: !Int
    , raActionString  :: !EventString
    } deriving (Typeable, Eq, Show, Generic)

data Register = Register
    { regRegionStyle :: RegionStyle
    , regContent     :: YiString
    } deriving (Generic)

data VimMode
    = Normal
    | NormalOperatorPending OperatorName
    | Insert Char -- ^ char denotes how state got into insert mode ('i', 'a', etc.)
    | Replace
    | ReplaceSingleChar
    | InsertNormal -- ^ after C-o
    | InsertVisual -- ^ after C-o and one of v, V, C-v
    | Visual RegionStyle
    | Ex
    | Search { previousMode :: VimMode, direction :: Direction }
    deriving (Typeable, Eq, Show, Generic)

data GotoCharCommand = GotoCharCommand !Char !Direction !RegionStyle
    deriving (Generic)

data VimState = VimState
    { vsMode                  :: !VimMode
    , vsCount                 :: !(Maybe Int)
    , vsAccumulator           :: !EventString -- ^ for repeat and potentially macros
    , vsTextObjectAccumulator :: !EventString
    , vsRegisterMap           :: !(HM.HashMap RegisterName Register)
    , vsActiveRegister        :: !RegisterName
    , vsRepeatableAction      :: !(Maybe RepeatableAction)
    , vsStringToEval          :: !EventString -- ^ see Yi.Keymap.Vim.vimEval comment
    , vsStickyEol             :: !Bool -- ^ is set on $, allows j and k walk the right edge of lines
    , vsOngoingInsertEvents   :: !EventString
    , vsLastGotoCharCommand   :: !(Maybe GotoCharCommand)
    , vsBindingAccumulator    :: !EventString
    , vsSecondaryCursors      :: ![Point]
    , vsPaste                 :: !Bool -- ^ like vim's :help paste
    , vsCurrentMacroRecording :: !(Maybe (MacroName, EventString))
    } deriving (Typeable, Generic)

instance Binary RepeatableAction
instance Binary Register
instance Binary GotoCharCommand

instance Default VimMode where
    def = Normal

instance Binary VimMode

instance Default VimState where
    def = VimState
            Normal -- mode
            Nothing -- count
            mempty -- accumulator
            mempty -- textobject accumulator
            mempty -- register map
            '\0' -- active register
            Nothing -- repeatable action
            mempty -- string to eval
            False -- sticky eol
            mempty -- ongoing insert events
            Nothing -- last goto char command
            mempty -- binding accumulator
            mempty -- secondary cursors
            False -- :set paste
            Nothing -- current macro recording

instance Binary VimState

instance YiVariable VimState

-- Whether an action can be repeated through the use of the '.' key.
--
-- Actions with a RepeatToken of:
--
--  - Finish are repeatable.
--  - Drop are not repeatable.
--  - Continue are currently in progress. They will become repeatable when
--    completed. It is possible to cancel a in progress action, in which case
--    it will not be repeatable.
data RepeatToken = Finish
                 | Drop
                 | Continue
    deriving Show

-- Distinction between YiM and EditorM variants is for testing.
data VimBinding
    = VimBindingY (EventString -> VimState -> MatchResult (YiM RepeatToken))
    | VimBindingE (EventString -> VimState -> MatchResult (EditorM RepeatToken))
