-- | Implements word movements.
--
-- Copyright (c) Hans-Peter Deifel
module Data.Text.Zipper.Generic.Words
  ( moveWordLeft
  , moveWordRight
  , deletePrevWord
  , deleteWord
  )
where

import Data.Char

import Data.Text.Zipper
import qualified Data.Text.Zipper.Generic as TZ

-- | Move one word to the left.
--
-- A word is defined as a consecutive string not satisfying isSpace.
-- This function always leaves the cursor at the beginning of a word
-- (except at the very start of the text).
moveWordLeft :: TZ.GenericTextZipper a => TextZipper a -> TextZipper a
moveWordLeft = doWordLeft False moveLeft

-- | Delete the previous word.
--
-- Does the same as 'moveWordLeft' but deletes characters instead of
-- simply moving past them.
deletePrevWord :: (Eq a, TZ.GenericTextZipper a) => TextZipper a -> TextZipper a
deletePrevWord = doWordLeft False deletePrevChar

doWordLeft :: TZ.GenericTextZipper a
           => Bool
           -> (TextZipper a -> TextZipper a)
           -> TextZipper a
           -> TextZipper a
doWordLeft inWord transform zipper = case charToTheLeft zipper of
    Nothing -> zipper  -- start of text
    Just c
        | isSpace c && not inWord ->
          doWordLeft False transform (transform zipper)
        | not (isSpace c) && not inWord ->
          doWordLeft True transform zipper -- switch to skipping letters
        | not (isSpace c) && inWord ->
          doWordLeft True transform (transform zipper)
        | otherwise ->
          zipper -- Done

-- | Move one word to the right.
--
-- A word is defined as a consecutive string not satisfying isSpace.
-- This function always leaves the cursor at the end of a word (except
-- at the very end of the text).
moveWordRight :: TZ.GenericTextZipper a => TextZipper a -> TextZipper a
moveWordRight = doWordRight False moveRight

-- | Delete the next word.
--
-- Does the same as 'moveWordRight' but deletes characters instead of
-- simply moving past them.
deleteWord :: TZ.GenericTextZipper a => TextZipper a -> TextZipper a
deleteWord = doWordRight False deleteChar

doWordRight :: TZ.GenericTextZipper a
            => Bool
            -> (TextZipper a -> TextZipper a)
            -> TextZipper a
            -> TextZipper a
doWordRight inWord transform zipper = case charToTheRight zipper of
    Nothing -> zipper -- end of text
    Just c
        | isSpace c && not inWord ->
          doWordRight False transform (transform zipper)
        | not (isSpace c) && not inWord ->
          doWordRight True transform zipper -- switch to skipping letters
        | not (isSpace c) && inWord ->
          doWordRight True transform (transform zipper)
        | otherwise ->
          zipper -- Done

-- Helpers

charToTheLeft :: TZ.GenericTextZipper a => TextZipper a -> Maybe Char
charToTheLeft zipper = case cursorPosition zipper of
  (0, 0) -> Nothing  -- Very start of text, no char left
  (_, 0) -> Just '\n' -- Start of line, simulate newline
  (_, x) -> Just (TZ.toList (currentLine zipper) !! (x-1))

charToTheRight :: TZ.GenericTextZipper a => TextZipper a -> Maybe Char
charToTheRight zipper
  | null (getText zipper) = Nothing
  | otherwise =
    let
      (row, col) = cursorPosition zipper
      content = getText zipper
      curLine = content !! row
      numLines = length content
    in
      if row == numLines - 1 && col == (TZ.length curLine) then
        Nothing -- very end
      else if col == (TZ.length curLine) then
        Just '\n' -- simulate newline
      else
        Just (TZ.toList curLine !! col)