{-# LANGUAGE CPP #-}
{-# OPTIONS_HADDOCK show-extensions #-}

-- |
-- Module      :  Yi.Buffer.Region
-- License     :  GPL-2
-- Maintainer  :  yi-devel@googlegroups.com
-- Stability   :  experimental
-- Portability :  portable
--
-- This module defines buffer operation on regions

module Yi.Buffer.Region
  ( module Yi.Region
  , swapRegionsB
  , deleteRegionB
  , replaceRegionB
  , readRegionB
  , mapRegionB
  , modifyRegionB
  , winRegionB
  , inclusiveRegionB
  , blockifyRegion
  , joinLinesB
  , concatLinesB
  , linesOfRegionB
  ) where

import           Control.Monad       (when)
import           Data.Char           (isSpace)
import           Data.List           (sort)
import           Yi.Buffer.Misc
import           Yi.Region
import           Yi.Rope             (YiString)
import qualified Yi.Rope             as R (YiString, cons, dropWhile, filter
                                          , lines, lines', map, null, length)
import           Yi.String           (overInit)
import           Yi.Utils            (SemiNum ((~-)))
import           Yi.Window           (winRegion)



winRegionB :: BufferM Region
winRegionB = askWindow winRegion

-- | Delete an arbitrary part of the buffer
deleteRegionB :: Region -> BufferM ()
deleteRegionB r = deleteNAt (regionDirection r) (fromIntegral (regionEnd r ~- regionStart r)) (regionStart r)

readRegionB :: Region -> BufferM YiString
readRegionB r = nelemsB (fromIntegral (regionEnd r - i)) i
    where i = regionStart r

-- | Replace a region with a given rope.
replaceRegionB :: Region -> YiString -> BufferM ()
replaceRegionB r s = do
  deleteRegionB r
  insertNAt s $ regionStart r

-- | Map the given function over the characters in the region.
mapRegionB :: Region -> (Char -> Char) -> BufferM ()
mapRegionB r f = do
  text <- readRegionB r
  replaceRegionB r (R.map f text)

-- | Swap the content of two Regions
swapRegionsB :: Region -> Region -> BufferM ()
swapRegionsB r r'
    | regionStart r > regionStart r' = swapRegionsB r' r
    | otherwise = do w0 <- readRegionB r
                     w1 <- readRegionB r'
                     replaceRegionB r' w0
                     replaceRegionB r  w1

-- | Modifies the given region according to the given
-- string transformation function
modifyRegionB :: (R.YiString -> R.YiString)
                 -- ^ The string modification function
              -> Region
                 -- ^ The region to modify
              -> BufferM ()
modifyRegionB f region = f <$> readRegionB region >>= replaceRegionB region

-- | Extend the right bound of a region to include it.
inclusiveRegionB :: Region -> BufferM Region
inclusiveRegionB r =
          if regionStart r <= regionEnd r
              then mkRegion (regionStart r) <$> pointAfterCursorB (regionEnd r)
              else mkRegion <$> pointAfterCursorB (regionStart r) <*> pure (regionEnd r)

-- | See a region as a block/rectangular region,
-- since regions are represented by two point, this returns
-- a list of small regions form this block region.
blockifyRegion :: Region -> BufferM [Region]
blockifyRegion r = savingPointB $ do
  [lowCol, highCol] <- sort <$> mapM colOf [regionStart r, regionEnd r]
  startLine <- lineOf $ regionStart r
  endLine   <- lineOf $ regionEnd r
  when (startLine > endLine) $ fail "blockifyRegion: impossible"
  mapM (\line -> mkRegion <$> pointOfLineColB line lowCol
                          <*> pointOfLineColB line (1 + highCol))
       [startLine..endLine]

-- | Joins lines in the region with a single space, skipping any empty
-- lines.
joinLinesB :: Region -> BufferM ()
joinLinesB = savingPointB . modifyRegionB g'
  where
    g' = overInit $ mconcat . pad . R.lines

    pad :: [R.YiString] -> [R.YiString]
    pad [] = []
    pad (x:xs) = x : fmap (skip (R.cons ' ' . R.dropWhile isSpace)) xs

    skip g x = if R.null x then x else g x

-- | Concatenates lines in the region preserving the trailing newline
-- if any.
concatLinesB :: Region -> BufferM ()
concatLinesB = savingPointB . modifyRegionB (overInit $ R.filter (/= '\n'))

-- | Gets the lines of a region (as a region), preserving newlines. Thus the
-- resulting list of regions is a partition of the original region.
--
-- The direction of the region is preserved and all smaller regions will
-- retain that direction.
--
-- Note that regions should never be empty, so it would be odd for this to
-- return an empty list...
linesOfRegionB :: Region -> BufferM [Region]
linesOfRegionB region = do
    let start = regionStart region
        direction = regionDirection region
    ls <- R.lines' <$> readRegionB region
    return $ case ls of
        [] -> []
        (l:ls') -> let initialRegion = mkRegion' direction start (start + fromIntegral (R.length l))
                   in scanl nextRegion initialRegion ls'

-- | Given some text and the previous region, finds the next region
-- (used for implementing linesOfRegionB, not generally useful)
nextRegion :: Region -> R.YiString -> Region
nextRegion r l = mkRegion' (regionDirection r) (regionEnd r) (regionEnd r + len)
    where len = fromIntegral $ R.length l
