{-# LANGUAGE DeriveDataTypeable         #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE LambdaCase                 #-}
{-# LANGUAGE OverloadedStrings          #-}
{-# OPTIONS_HADDOCK show-extensions #-}

-- |
-- Module      :  Yi.Search
-- License     :  GPL-2
-- Maintainer  :  yi-devel@googlegroups.com
-- Stability   :  experimental
-- Portability :  portable
--
-- Search/Replace functions

module Yi.Search (
        setRegexE,      -- :: SearchExp -> EditorM ()
        resetRegexE,    -- :: EditorM ()
        getRegexE,      -- :: EditorM (Maybe SearchExp)
        SearchMatch,
        SearchResult(..),
        SearchOption(..),
        doSearch,            -- :: (Maybe String) -> [SearchOption]
                            -- -> Direction -> YiM ()
        searchInit,        -- :: String
                            -- -> [SearchOption]
                            -- -> IO SearchExp
        continueSearch,          -- :: SearchExp
                            -- -> IO SearchResult
        makeSimpleSearch,

        -- * Batch search-replace
        searchReplaceRegionB,
        searchReplaceSelectionB,
        replaceString,
        searchAndRepRegion,
        searchAndRepRegion0,
        searchAndRepUnit, -- :: String -> String -> Bool -> TextUnit -> EditorM Bool

        -- * Incremental Search
        isearchInitE,
        isearchIsEmpty,
        isearchAddE,
        isearchPrevE,
        isearchNextE,
        isearchWordE,
        isearchHistory,
        isearchDelE,
        isearchCancelE,
        isearchFinishE,
        isearchCancelWithE,
        isearchFinishWithE,

        -- * Replace
        qrNext,
        qrReplaceAll,
        qrReplaceOne,
        qrFinish
                 ) where

import           Lens.Micro.Platform          ((.=))
import           Control.Monad       (void, when)
import           Data.Binary         (Binary, get, put)
import           Data.Char           (isAlpha, isUpper)
import           Data.Default        (Default, def)
import           Data.Maybe          (listToMaybe)
import           Data.Monoid         ((<>))
import qualified Data.Text           as T (Text, any, break, empty, length, null, takeWhile, unpack)
import qualified Data.Text.Encoding  as E (decodeUtf8, encodeUtf8)
import           Data.Typeable       (Typeable)
import           Yi.Buffer
import           Yi.Editor
import           Yi.History          (historyFinishGen, historyMoveGen, historyStartGen)
import           Yi.Regex
import qualified Yi.Rope             as R (YiString, null, toString, toText)
import           Yi.Search.Internal  (getRegexE, resetRegexE, setRegexE)
import           Yi.String           (showT)
import           Yi.Types            (YiVariable)
import           Yi.Utils            (fst3)
import           Yi.Window           (Window)

-- ---------------------------------------------------------------------
--
-- | Global searching. Search for regex and move point to that position.
-- @Nothing@ means reuse the last regular expression. @Just s@ means use
-- @s@ as the new regular expression. Direction of search can be
-- specified as either @Backward@ or @Forward@ (forwards in the buffer).
-- Arguments to modify the compiled regular expression can be supplied
-- as well.
--

type SearchMatch = Region
data SearchResult = PatternFound
                  | PatternNotFound
                  | SearchWrapped
  deriving Eq

doSearch :: Maybe String        -- ^ @Nothing@ means used previous
                                -- pattern, if any. Complain otherwise.
                                -- Use getRegexE to check for previous patterns
        -> [SearchOption]            -- ^ Flags to modify the compiled regex
        -> Direction            -- ^ @Backward@ or @Forward@
        -> EditorM SearchResult

doSearch (Just re) fs d = searchInit re d fs >>= withCurrentBuffer . continueSearch
doSearch Nothing   _  d = do
  mre <- getRegexE
  case mre of
    Nothing -> fail "No previous search pattern" -- NB
    Just r  -> withCurrentBuffer (continueSearch (r,d))

-- | Set up a search.
searchInit :: String -> Direction -> [SearchOption] -> EditorM (SearchExp, Direction)
searchInit re d fs = do
    let Right c_re = makeSearchOptsM fs re
    setRegexE c_re
    searchDirectionA .= d
    return (c_re,d)

-- | Do a search, placing cursor at first char of pattern, if found.
-- Keymaps may implement their own regex language. How do we provide for this?
-- Also, what's happening with ^ not matching sol?
continueSearch :: (SearchExp, Direction) -> BufferM SearchResult
continueSearch (c_re, dir) = do
  mp <- savingPointB $ do
    moveB Character dir  -- start immed. after cursor
    rs <- regexB dir c_re
    moveB Document (reverseDir dir) -- wrap around
    ls <- regexB dir c_re
    return $ listToMaybe $ fmap Right rs ++ fmap Left ls
  maybe (return ()) (moveTo . regionStart . either id id) mp
  return $ f mp
    where
        f (Just (Right _)) = PatternFound
        f (Just (Left  _)) = SearchWrapped
        f Nothing          = PatternNotFound

------------------------------------------------------------------------
-- Batch search and replace
--

-- | Search and Replace all within the current region.
-- Note the region is the final argument since we might perform
-- the same search and replace over multiple regions however we are
-- unlikely to perform several search and replaces over the same region
-- since the first such may change the bounds of the region.
searchReplaceRegionB :: R.YiString -- ^ The string to search for
                     -> R.YiString -- ^ The string to replace it with
                     -> Region -- ^ The region to perform this over
                     -> BufferM Int
searchReplaceRegionB from to =
  searchAndRepRegion0 (makeSimpleSearch from) to True


-- | Peform a search and replace on the selection
searchReplaceSelectionB :: R.YiString  -- ^ text to search for
                        -> R.YiString  -- ^ text to replace it with
                        -> BufferM Int
searchReplaceSelectionB from to =
  getSelectRegionB >>= searchReplaceRegionB from to

-- | Replace a string by another everywhere in the document
replaceString :: R.YiString -> R.YiString -> BufferM Int
replaceString a b = regionOfB Document >>= searchReplaceRegionB a b

------------------------------------------------------------------------
-- | Search and replace in the given region.
--
-- If the input boolean is True, then the replace is done globally,
-- otherwise only the first match is replaced. Returns the number of
-- replacements done.
searchAndRepRegion0 :: SearchExp -> R.YiString -> Bool -> Region -> BufferM Int
searchAndRepRegion0 c_re str globally region = do
    mp <- (if globally then id else take 1) <$> regexRegionB c_re region  -- find the regex
    -- mp' is a maybe not reversed version of mp, the goal
    -- is to avoid replaceRegionB to mess up the next regions.
    -- So we start from the end.
    let mp' = mayReverse (reverseDir $ regionDirection region) mp
    mapM_ (`replaceRegionB` str) mp'
    return (length mp)

searchAndRepRegion :: R.YiString -> R.YiString -> Bool -> Region -> EditorM Bool
searchAndRepRegion s str globally region = case R.null s of
  False -> return False
  True -> do
    let c_re = makeSimpleSearch s
    setRegexE c_re     -- store away for later use
    searchDirectionA .= Forward
    withCurrentBuffer $ (/= 0) <$> searchAndRepRegion0 c_re str globally region

------------------------------------------------------------------------
-- | Search and replace in the region defined by the given unit.
-- The rest is as in 'searchAndRepRegion'.
searchAndRepUnit :: R.YiString -> R.YiString -> Bool -> TextUnit -> EditorM Bool
searchAndRepUnit re str g unit =
  withCurrentBuffer (regionOfB unit) >>= searchAndRepRegion re str g

--------------------------
-- Incremental search


newtype Isearch = Isearch [(T.Text, Region, Direction)]
  deriving (Typeable, Show)

instance Binary Isearch where
  put (Isearch ts) = put (map3 E.encodeUtf8 ts)
  get = Isearch . map3 E.decodeUtf8 <$> get

map3 :: (a -> d) -> [(a, b, c)] -> [(d, b, c)]
map3 _ [] = []
map3 f ((a, b, c):xs) = (f a, b, c) : map3 f xs


-- This contains: (string currently searched, position where we
-- searched it, direction, overlay for highlighting searched text)

-- Note that this info cannot be embedded in the Keymap state: the state
-- modification can depend on the state of the editor.

instance Default Isearch where
    def = Isearch []

instance YiVariable Isearch

isearchInitE :: Direction -> EditorM ()
isearchInitE dir = do
  historyStartGen iSearch
  p <- withCurrentBuffer pointB
  putEditorDyn (Isearch [(T.empty ,mkRegion p p, dir)])
  printMsg "I-search: "

isearchIsEmpty :: EditorM Bool
isearchIsEmpty = do
  Isearch s <- getEditorDyn
  return . not . T.null . fst3 $ head s

isearchAddE :: T.Text -> EditorM ()
isearchAddE inc = isearchFunE (<> inc)

-- | Create a SearchExp that matches exactly its argument
makeSimpleSearch :: R.YiString -> SearchExp
makeSimpleSearch s = se
    where Right se = makeSearchOptsM [QuoteRegex] (R.toString s)

makeISearch :: T.Text -> SearchExp
makeISearch s = case makeSearchOptsM opts (T.unpack s) of
                  Left _ -> SearchExp (T.unpack s) emptyRegex emptyRegex []
                  Right search -> search
   where opts = QuoteRegex : if T.any isUpper s then [] else [IgnoreCase]

isearchFunE :: (T.Text -> T.Text) -> EditorM ()
isearchFunE fun = do
  Isearch s <- getEditorDyn
  case s of
    [_] -> resetRegexE
    _ -> return ()
  let (previous,p0,direction) = head s
      current = fun previous
      srch = makeISearch current
  printMsg $ "I-search: " <> current
  setRegexE srch
  prevPoint <- withCurrentBuffer pointB
  matches <- withCurrentBuffer $ do
      moveTo $ regionStart p0
      when (direction == Backward) $
         moveN $ T.length current
      regexB direction srch

  let onSuccess p = do withCurrentBuffer $ moveTo (regionEnd p)
                       putEditorDyn $ Isearch ((current, p, direction) : s)

  case matches of
    (p:_) -> onSuccess p
    [] -> do matchesAfterWrap <- withCurrentBuffer $ do
               case direction of
                 Forward -> moveTo 0
                 Backward -> do
                   bufferLength <- sizeB
                   moveTo bufferLength
               regexB direction srch

             case matchesAfterWrap of
               (p:_) -> onSuccess p
               [] -> do withCurrentBuffer $ moveTo prevPoint -- go back to where we were
                        putEditorDyn $ Isearch ((current, p0, direction) : s)
                        printMsg $ "Failing I-search: " <> current

isearchDelE :: EditorM ()
isearchDelE = do
  Isearch s <- getEditorDyn
  case s of
    (_:(text,p,dir):rest) -> do
      withCurrentBuffer $
        moveTo $ regionEnd p
      putEditorDyn $ Isearch ((text,p,dir):rest)
      setRegexE $ makeISearch text
      printMsg $ "I-search: " <> text
    _ -> return () -- if the searched string is empty, don't try to remove chars from it.

isearchHistory :: Int -> EditorM ()
isearchHistory delta = do
  Isearch ((current,_p0,_dir):_) <- getEditorDyn
  h <- historyMoveGen iSearch delta (return current)
  isearchFunE (const h)

isearchPrevE :: EditorM ()
isearchPrevE = isearchNext0 Backward

isearchNextE :: EditorM ()
isearchNextE = isearchNext0 Forward

isearchNext0 :: Direction -> EditorM ()
isearchNext0 newDir = do
  Isearch ((current,_p0,_dir):_rest) <- getEditorDyn
  if T.null current
    then isearchHistory 1
    else isearchNext newDir


isearchNext :: Direction -> EditorM ()
isearchNext direction = do
  Isearch ((current, p0, _dir) : rest) <- getEditorDyn
  withCurrentBuffer $ moveTo (regionStart p0 + startOfs)
  mp <- withCurrentBuffer $
    regexB direction (makeISearch current)
  case mp of
    [] -> do
      endPoint <- withCurrentBuffer $ do
              moveTo (regionEnd p0) -- revert to offset we were before.
              sizeB
      printMsg "isearch: end of document reached"
      let wrappedOfs = case direction of
                         Forward -> mkRegion 0 0
                         Backward -> mkRegion endPoint endPoint
      putEditorDyn $ Isearch ((current,wrappedOfs,direction):rest) -- prepare to wrap around.
    (p:_) -> do
      withCurrentBuffer $
        moveTo (regionEnd p)
      printMsg $ "I-search: " <> current
      putEditorDyn $ Isearch ((current,p,direction):rest)
 where startOfs = case direction of
                      Forward  ->  1
                      Backward -> -1

isearchWordE :: EditorM ()
isearchWordE = do
   -- add maximum 32 chars at a time.
  text <- R.toText <$> withCurrentBuffer (pointB >>= nelemsB 32)

  let (prefix, rest) = T.break isAlpha text
      word = T.takeWhile isAlpha rest
  isearchAddE $ prefix <> word

-- | Succesfully finish a search. Also see 'isearchFinishWithE'.
isearchFinishE :: EditorM ()
isearchFinishE = isearchEnd True

-- | Cancel a search. Also see 'isearchCancelWithE'.
isearchCancelE :: EditorM ()
isearchCancelE = isearchEnd False

-- | Wrapper over 'isearchEndWith' that passes through the action and
-- accepts the search as successful (i.e. when the user wants to stay
-- at the result).
isearchFinishWithE :: EditorM a -> EditorM ()
isearchFinishWithE act = isearchEndWith act True

-- | Wrapper over 'isearchEndWith' that passes through the action and
-- marks the search as unsuccessful (i.e. when the user wants to
-- jump back to where the search started).
isearchCancelWithE :: EditorM a -> EditorM ()
isearchCancelWithE act = isearchEndWith act False

iSearch :: T.Text
iSearch = "isearch"

-- | Editor action describing how to end finish incremental search.
-- The @act@ parameter allows us to specify an extra action to run
-- before finishing up the search. For Vim, we don't want to do
-- anything so we use 'isearchEnd' which just does nothing. For emacs,
-- we want to cancel highlighting and stay where we are.
isearchEndWith :: EditorM a -> Bool -> EditorM ()
isearchEndWith act accept = getEditorDyn >>= \case
  Isearch [] -> return ()
  Isearch s@((lastSearched, _, dir):_) -> do
    let (_,p0,_) = last s
    historyFinishGen iSearch (return lastSearched)
    searchDirectionA .= dir
    if accept
       then do void act
               printMsg "Quit"
       else do resetRegexE
               withCurrentBuffer $ moveTo $ regionStart p0

-- | Specialised 'isearchEndWith' to do nothing as the action.
isearchEnd :: Bool -> EditorM ()
isearchEnd = isearchEndWith (return ())

-----------------
-- Query-Replace

-- | Find the next match and select it.
-- Point is end, mark is beginning.
qrNext :: Window -> BufferRef -> SearchExp -> EditorM ()
qrNext win b what = do
  mp <- withGivenBufferAndWindow win b $ regexB Forward what
  case mp of
    [] -> do
      printMsg "String to search not found"
      qrFinish
    (r:_) -> withGivenBufferAndWindow win b $ setSelectRegionB r

-- | Replace all the remaining occurrences.
qrReplaceAll :: Window -> BufferRef -> SearchExp -> R.YiString -> EditorM ()
qrReplaceAll win b what replacement = do
  n <- withGivenBufferAndWindow win b $ do
    exchangePointAndMarkB -- so we replace the current occurence too
    searchAndRepRegion0 what replacement True =<< regionOfPartB Document Forward
  printMsg $ "Replaced " <> showT n <> " occurrences"
  qrFinish

-- | Exit from query/replace.
qrFinish :: EditorM ()
qrFinish = do
  currentRegexA .= Nothing
  closeBufferAndWindowE  -- the minibuffer.

-- | We replace the currently selected match and then move to the next
-- match.
qrReplaceOne :: Window -> BufferRef -> SearchExp -> R.YiString -> EditorM ()
qrReplaceOne win b reg replacement = do
  qrReplaceCurrent win b replacement
  qrNext win b reg

-- | This may actually be a bit more general it replaces the current
-- selection with the given replacement string in the given window and
-- buffer.
qrReplaceCurrent :: Window -> BufferRef -> R.YiString -> EditorM ()
qrReplaceCurrent win b replacement =
  withGivenBufferAndWindow win b $
   flip replaceRegionB replacement =<< getRawestSelectRegionB
