{-# LANGUAGE RankNTypes, TupleSections, FlexibleContexts, MultiParamTypeClasses, FlexibleInstances #-}

module Data.Text.Region (
	pt, start, lineStart, regionLength, till, linesSize, regionLines, emptyRegion, line,
	regionSize, expandLines, atRegion, overlaps, applyMap, cutMap, insertMap,
	cutRegion, insertRegion,
	EditAction(..), replace, cut, paste, overwrite, apply, update, undo,

	module Data.Text.Region.Types
	) where

import Prelude hiding (id, (.))
import Prelude.Unicode

import Control.Category
import Control.Lens

import Data.Text.Region.Types

-- | Make 'Point' from line and column
pt  Int  Int  Point
pt = Point

-- | 'Point' at the beginning
start  Point
start = pt 0 0

-- | 'Point' at the beginning of line
lineStart  Int  Point
lineStart l = pt l 0

-- | Regions length
regionLength  Lens' Region Size
regionLength = lens fromr tor where
	fromr (Region f t) = t .-. f
	tor (Region f _) sz = Region f (f .+. sz)

-- | Region from one 'Point' to another
till  Point  Point  Region
l `till` r = Region (min l r) (max l r)

-- | Distance of @n@ lines
linesSize  Int  Size
linesSize = pt 0

-- 'Region' height in lines, any 'Region' at least of line height 1
regionLines  Lens' Region Int
regionLines = lens fromr tor where
	fromr (Region f t) = succ $ (t ^. pointLine) - (f ^. pointLine)
	tor (Region f t) l = Region f (set pointLine (f ^. pointLine + l) t)

-- | Is 'Region' empty
emptyRegion  Region  Bool
emptyRegion r = r ^. regionFrom  r ^. regionTo

-- | n'th line region, starts at the beginning of line and ends on the next line
line  Int  Region
line l = lineStart l `till` lineStart (succ l)

-- | Make 'Region' by start position and 'Size'
regionSize  Point  Size  Region
regionSize pt' sz = pt' `till` (pt' .+. sz)

-- | Expand 'Region' to contain full lines
expandLines  Region  Region
expandLines (Region f t) = lineStart (f ^. pointLine) `till` lineStart (succ $ t ^. pointLine)

-- | Get contents at 'Region'
atRegion  Editable s  Region  Lens' (Contents s) (Contents s)
atRegion r = lens fromc toc where
	fromc cts = cts ^. splitted (r ^. regionTo) . _1 . splitted (r ^. regionFrom) . _2
	toc cts cts' = (cts ^. splitted (r ^. regionFrom) . _1) `concatCts` cts' `concatCts` (cts ^. splitted (r ^. regionTo) . _2)

-- | Does regions overlaps
overlaps  Region  Region  Bool
overlaps l r
	| r ^. regionFrom  l ^. regionTo = False
	| r ^. regionTo  l ^. regionFrom = False
	| otherwise = True

applyMap  Map  Region  Region
applyMap = view  mapIso

-- | Cut 'Region' mapping
cutMap  Region  Map
cutMap rgn = Map $ iso (cutRegion rgn) (insertRegion rgn)

-- | Opposite to 'cutMap'
insertMap  Region  Map
insertMap = invert  cutMap

-- | Update second 'Region' position as if it was data cutted at first 'Region'
cutRegion  Region  Region  Region
cutRegion (Region is ie) (Region s e) = Region
	(if is < s then (s .-. ie) .+. is else s)
	(if is < e then (e .-. ie) .+. is else e)

-- | Update second region position as if it was data inserted at first region (region sets insertion point and data size)
-- Region tries not to extend if data inserted at region bound except when region is empty
-- This allows define replace as cut and insert in special case when we replace region itself
insertRegion  Region  Region  Region
insertRegion (Region is ie) (Region s e)
	| (s  e)  (is  s) = Region is ie
	| otherwise = Region
		(if is  s then (s .-. is) .+. ie else s)
		(if is < e then (e .-. is) .+. ie else e)

class Editable s  EditAction e s where
	-- | Make replace action over 'Region' and 'Contents'
	replaceAction  Region  Contents s  e s
	-- | Make 'Map' from action
	actionMap  e s  Map
	-- | Perform action, modifying 'Contents'
	perform  e s  Contents s  Contents s
	-- | Get action undo
	inversed  e s  Contents s  e s

-- | Replace region with data
replace  EditAction e s  Region  s  e s
replace r = replaceAction r  view contents

-- | Cuts region
cut  EditAction e s  Region  e s
cut r = replaceAction r emptyContents

-- | Pastes 'Contents' at some 'Point'
paste  EditAction e s  Point  s  e s
paste p = replaceAction (p `till` p)  view contents

-- | Overwrites 'Contents' at some 'Point'
overwrite  EditAction e s  Point  s  e s
overwrite p c = replaceAction (p `regionSize` measure cts) cts where
	cts = view contents c

-- | 'perform' for 'Edit'
apply  Editable s  Edit s  s  s
apply = over contents  perform

-- | Get undo
undo  Editable s  Edit s  s  Edit s
undo e = inversed e  view contents

-- | Update regions
update  (Editable s, Regioned r)  Edit s  r  r
update e = over regions (applyMap  actionMap $ e)

instance Editable s  EditAction Replace s where
	replaceAction = Replace
	actionMap (Replace r w) = insertMap (r & regionLength .~ measure w) `mappend` cutMap r
	perform (Replace r w) cts = cts & atRegion r .~ w
	inversed (Replace r w) cts = Replace (r & regionLength .~ measure w) (cts ^. atRegion r)

instance Editable s  EditAction Edit s where
	replaceAction rgn txt = Edit [replaceAction rgn txt]
	actionMap = foldr go mempty  view replaces where
		go r m = actionMap (over replaceRegion (applyMap m) r) `mappend` m
	perform = snd  foldr go (mempty, id)  view replaces where
		go r (m, fn) = (actionMap r' `mappend` m, perform r'  fn) where
			r' = over replaceRegion (applyMap m) r
	inversed e@(Edit rs) cts = Edit [Replace (applyMap m r) (cts ^. atRegion r) | Replace r _  rs] where
		m = actionMap e