{- -----------------------------------------------------------------------------
Copyright 2020 Kevin P. Barry

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
----------------------------------------------------------------------------- -}

-- Author: Kevin P. Barry [ta0kira@gmail.com]

-- | Generic document-editing components.

{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE Safe #-}

module WEditor.Document (
  EditingDocument,
  editDocument,
  -- From Base >>>
  EditAction(..),
  EditDirection(..),
  EditorAction,
  FixedFontEditor(..),
  FixedFontParser,
  FixedFontViewer(..),
  MoveDirection(..),
  UnparsedPara(..),
  ViewerAction,
  editorAppendAction,
  editorBackspaceAction,
  editorDeleteAction,
  editorDownAction,
  editorEndAction,
  editorEnterAction,
  editorHomeAction,
  editorInsertAction,
  editorLeftAction,
  editorPageDownAction,
  editorPageUpAction,
  editorRightAction,
  editorSetPositionAction,
  editorUpAction,
  viewerFillAction,
  viewerResizeAction,
  viewerShiftUpAction,
  viewerShiftDownAction,
  -- <<< From Base
) where

import WEditor.Base
import WEditor.Internal.Line
import WEditor.Internal.Para


-- | Generic document editor with a dynamic viewport.
--
--   This editor bounds vertical view scrolling (see 'ViewAction') to keep the
--   cursor within view.
data EditingDocument c =
  forall a. FixedFontParser a c => EditingDocument {
    edBefore :: [VisibleParaBefore c (BreakType a)],  -- Reversed.
    edEditing :: EditingPara c (BreakType a),
    edAfter :: [VisibleParaAfter c (BreakType a)],
    edWidth :: Int,
    edHeight :: Int,
    edOffset :: Int,
    edLastCursor :: Int,
    edParser :: a
  }

-- | Create an editor for a document.
editDocument :: FixedFontParser a c => a                 -- ^ Parser used to break paragraphs into lines.
                                    -> [UnparsedPara c]  -- ^ List of unparsed paragraphs to be edited.
                                    -> EditingDocument c -- ^ Document editor.
editDocument parser ps = document where
  document = EditingDocument {
      edBefore = [],
      edEditing = editPara parser first,
      edAfter = map (parseParaAfter parser) rest,
      edWidth = 0,
      edHeight = 0,
      edOffset = 0,
      edLastCursor = 0,
      edParser = parser
    }
  (first:rest) = nonempty ps
  nonempty [] = [emptyPara]
  nonempty ps = ps


-- Private below here.

instance Show (EditingDocument c) where
  show d =
    "EditingDocument { size: " ++ show (getViewSize d) ++
                    ", cursor: " ++ show (getCursor d) ++
                    ", point: " ++ show (getEditPoint d) ++ " }"

instance FixedFontViewer (EditingDocument c) c where
  setViewSize d s@(w,h)
    | w /= edWidth d  = storeCursor $ resizeHeight h $ resizeWidth w d
    | h /= edHeight d = storeCursor $ resizeHeight h d
    | otherwise = d
  getViewSize d = (edWidth d,edHeight d)
  getVisible = getVisibleLines
  updateView = flip updateDocView

instance FixedFontEditor (EditingDocument c) c where
  editText da m d = storeCursor $ modifyDoc m d da
  breakPara da d = storeCursor $ insertParaSplit d da
  moveCursor da d = updateCursor $ moveDocCursor d da where
    updateCursor
      | d == MovePrev || d == MoveNext || d == MoveHome || d == MoveEnd = storeCursor
      | otherwise = applyCursor
  getCursor (EditingDocument _ e _ _ h k _ p) =
    (tweakCursor p (getCurrentLine e) $ getParaCursorChar e,boundOffset h k)
  getEditPoint = getDocEditPoint
  setEditPoint da (p,c) = storeCursor $ setDocEditPoint (p,c) da
  getParaSize (EditingDocument _ e _ _ _ _ _ _) = getParaCharCount e
  getParaCount da@(EditingDocument bs _ as _ _ _ _ _) = 1 + length bs + length as
  exportData = exportDocument

exportDocument :: EditingDocument c -> [UnparsedPara c]
exportDocument (EditingDocument bs e as _ _ _ _ _) =
  reverse (map unparseParaBefore bs) ++ [unparsePara e] ++ (map unparseParaAfter as)

boundOffset :: Int -> Int -> Int
boundOffset h k
  | h < 1 = max 0 k
  | otherwise = max 0 (min (h-1) k)

storeCursor :: EditingDocument c -> EditingDocument c
storeCursor (EditingDocument bs e as w h k _ p) =
  EditingDocument bs e as w h k (getParaCursorChar e) p

applyCursor :: EditingDocument c -> EditingDocument c
applyCursor (EditingDocument bs e as w h k c p) =
  EditingDocument bs (setParaCursorChar c e) as w h k c p

joinParaNext :: EditingDocument c -> EditingDocument c
joinParaNext da@(EditingDocument _ _ [] _ _ _ _ _) = da
joinParaNext (EditingDocument bs e (a:as) w h k c p) =
  EditingDocument bs (appendToPara p e a) as w h k c p

joinParaPrev :: EditingDocument c -> EditingDocument c
joinParaPrev da@(EditingDocument [] _ _ _ _ _ _ _) = da
joinParaPrev (EditingDocument (b:bs) e as w h k c p) =
  EditingDocument bs (prependToPara p b e) as w h (boundOffset h (k-1)) c p

resizeWidth :: Int -> EditingDocument c -> EditingDocument c
resizeWidth w (EditingDocument bs e as _ h k c p) = (EditingDocument bs2 e2 as2 w h k c p2) where
  p2 = setLineWidth p w
  bs2 = map (parseParaBefore p2 . unparseParaBefore) bs
  as2 = map (parseParaAfter  p2 . unparseParaAfter)  as
  e2 = reparsePara p2 e

resizeHeight :: Int -> EditingDocument c -> EditingDocument c
resizeHeight h da@(EditingDocument bs e as w _ k c p) =
  (EditingDocument bs e as w h offset c p) where
    offset
      | h < 1     = length $ catLinesBefore $ getBeforeLines e:bs
      | otherwise = boundOffset h $ min (paraLinesBefore da) k

paraLinesBefore :: EditingDocument c -> Int
paraLinesBefore (EditingDocument bs e _ _ h _ _ _) = total where
  total = countLinesBefore h bs'
  bs' = getBeforeLines e:bs

paraLinesAfter :: EditingDocument c -> Int
paraLinesAfter (EditingDocument _ e as _ h _ _ _) = total where
  total = countLinesAfter h as'
  as' = getAfterLines e:as

getVisibleLines :: EditingDocument c -> [[c]]
getVisibleLines (EditingDocument bs e as _ h k _ p) = visible where
  visible = map (renderLine p) $ bs2 ++ [e2] ++ as2
  bs2
    | h < 1     = reverse $ catLinesBefore before
    | otherwise = reverse $ take (boundOffset h k) $ catLinesBefore before
  e2 = getCurrentLine e
  as2
    | h < 1     = catLinesAfter after
    | otherwise = take (h-length bs2-1) $ catLinesAfter after
  before = getBeforeLines e:bs
  after = getAfterLines e:as

moveDocCursor :: MoveDirection -> EditingDocument c -> EditingDocument c
moveDocCursor d da@(EditingDocument bs e as w h k c p) = revised where
  revised
    | paraCursorMovable d e =
      let e2 = moveParaCursor d e in (EditingDocument bs e2 as w h (fixOffset e2) c p)
    | d == MoveDown && null as && atParaBack e =
      EditingDocument bs e as w h (boundOffset h (k-1)) c p
    | (d == MoveUp && null bs) || (d == MoveDown && null as) =
      -- NOTE: The cursor is explicitly stored here so that the position at the
      -- front or back of the paragraph is preserved.
      let e2 = moveParaCursor d e in storeCursor $ (EditingDocument bs e2 as w h (fixOffset e2) c p)
    | d == MoveUp =
        let
          bs2 = tail bs
          e2 = seekParaBack $ editPara p $ unparseParaBefore $ head bs
          as2 = viewParaAfter e:as
          k2 = boundOffset h (k-1) in
        -- NOTE: This doesn't preserve cursor position.
        EditingDocument bs2 e2 as2 w h k2 c p
    | d == MoveDown =
        let
          bs2 = viewParaBefore e:bs
          e2 = editPara p $ unparseParaAfter $ head as
          as2 = tail as
          k2 = boundOffset h (k+1) in
        -- NOTE: This doesn't preserve cursor position.
        EditingDocument bs2 e2 as2 w h k2 c p
    | d == MovePrev && not (null bs) = seekBack  $ moveDocCursor MoveUp   da
    | d == MoveNext && not (null as) = seekFront $ moveDocCursor MoveDown da
    | d == MovePageUp   = editAtTop $ repeatTimes h (moveDocCursor MoveUp)   da
    | d == MovePageDown = editAtTop $ repeatTimes h (moveDocCursor MoveDown) da
    | otherwise = da
  fixOffset e2 = boundOffset h $ k + (getParaCursorLine e2 - getParaCursorLine e)
  seekBack  (EditingDocument bs e as w h k c p) = EditingDocument bs (seekParaBack e)  as w h k c p
  seekFront (EditingDocument bs e as w h k c p) = EditingDocument bs (seekParaFront e) as w h k c p
  repeatTimes n = foldr (flip (.)) id . replicate n
  editAtTop (EditingDocument bs e as w h _ c p) = EditingDocument bs e as w h 0 c p

modifyDoc :: EditAction c -> EditDirection -> EditingDocument c -> EditingDocument c
modifyDoc m d da@(EditingDocument bs e as w h k c p) = revised m d where
  revised DeleteText EditBefore
    | atParaFront e && not (null bs) =
      EditingDocument (tail bs) (prependToPara p (head bs) e) as w h (boundOffset h (k-1)) c p
  revised DeleteText EditAfter
    | atParaBack e && not (null as) =
      EditingDocument bs (appendToPara p e (head as)) (tail as) w h k c p
  revised _ _ = let e2 = (modifyPara p m d e) in EditingDocument bs e2 as w h (fixOffset e2) c p
  fixOffset e2 = boundOffset h $ k + (getParaCursorLine e2 - getParaCursorLine e)

insertParaSplit :: EditDirection -> EditingDocument c -> EditingDocument c
insertParaSplit d (EditingDocument bs e as w h k c p) = revised where
  (b,a) = splitPara p e
  revised
    | d == EditBefore = EditingDocument (parseParaBefore p b:bs) (editPara p a) as w h (boundOffset h (k+1)) c p
    | d == EditAfter  = EditingDocument bs (seekParaBack $ editPara p b) (parseParaAfter p a:as) w h k c p

getDocEditPoint :: EditingDocument c -> (Int,Int)
getDocEditPoint (EditingDocument bs e _ _ _ _ _ _) = (length bs,getParaEditChar e)

setEditPara :: Int -> EditingDocument c -> EditingDocument c
setEditPara n (EditingDocument bs e as w h k c p) = revised where
  revised = resizeHeight h $ EditingDocument bs2 e2 as2 w h k c p
  (bs2,e2,as2) = shift bs e as
  shift bs e as
    | length bs < n && not (null as) =
      shift (viewParaBefore e:bs) (editPara p $ unparseParaAfter $ head as)  (tail as)
    | length bs > n && not (null bs) =
      shift (tail bs)             (editPara p $ unparseParaBefore $ head bs) (viewParaAfter e:as)
    | otherwise = (bs,e,as)

setEditChar :: Int -> EditingDocument c -> EditingDocument c
setEditChar n (EditingDocument bs e as w h k c p) = EditingDocument bs (setParaEditChar n e) as w h k c p

setDocEditPoint :: (Int,Int) -> EditingDocument c -> EditingDocument c
setDocEditPoint (p,c) = setEditChar c . setEditPara p

updateDocView :: ViewAction -> EditingDocument c -> EditingDocument c
updateDocView _ da@(EditingDocument _ _ _ _ h _ _ _) | h <= 0 = da
updateDocView FillView da@(EditingDocument bs e as w h k c p) = revised where
  revised = EditingDocument bs e as w h (boundOffset h (min before k2)) c p
  before = paraLinesBefore da
  after  = paraLinesAfter  da
  k2 = if after >= h-k-1  -- The view is already full.
          then k
          else h-after-1  -- k2+after=h-1, i.e., a full viewport.
updateDocView (ShiftVertical n) da@(EditingDocument bs e as w h k c p) = revised where
  revised = EditingDocument bs e as w h (boundOffset h k2) c p
  before = paraLinesBefore da
  k2 = min before (k-n)