-- |
-- Module      :  Data.Edit
-- Copyright   :  (c) Varun Gandhi 2018
-- License     :  BSD-style (see the file LICENSE)
--
-- Maintainer  :  theindigamer15@gmail.com
-- Stability   :  experimental
-- Portability :  portable
--
-- The 'Edit' type for working with rewriting systems, with associated
-- operations.
--
-- To see a high-level overview of some use cases and a detailed example,
-- check the "Data.Edit.Tutorial" module.
--
-- __Usage notes:__
--
--   1. You probably want to import this module qualified to avoid a name
--      collision with "Data.Maybe"'s 'Data.Maybe.fromMaybe'.
--   2. We re-export the composition operators from "Control.Monad" for
--      convenience.

{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE CPP #-}
{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE DeriveTraversable #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE DeriveAnyClass #-}

module Data.Edit
  (
  -- * Edit type and basic operations
    Edit (..)
  , fromEdit
  , isClean
  , isDirty
  , extract
  , duplicate
  , extend
  -- * Conversions to and from base types
  , toMaybe
  , fromMaybe
  , edits
  , toEither
  , fromEither
  -- * Finding a fixed point
  , polish
  , iterations
  -- * Operations with lists
  , partitionEdits
  -- * Forceful conversions
  , clean
  , dirty
  -- * Re-exports from "Control.Monad"
  , (>=>)
  , (<=<)
  )
  where

#define MONOID_SUPERCLASS_OF_SEMIGROUP    MIN_VERSION_base(4,11,0)
#define SEMIGROUP_EXPORTED_FROM_PRELUDE   MIN_VERSION_base(4,11,0)
#define LIFTREADPREC_IN_READ1             MIN_VERSION_base(4,10,0)

import Control.Applicative
import Control.DeepSeq (NFData)
import Control.Monad ((>=>), (<=<), ap)
import Control.Monad.Zip (MonadZip (..))
import Data.Data (Typeable, Data)
import Data.Either (partitionEithers)
import Data.List (unfoldr)
import Data.Functor.Classes
import GHC.Generics (Generic)

#ifdef WITH_COMONAD_INSTANCE
import Control.Comonad
#endif
#if !SEMIGROUP_EXPORTED_FROM_PRELUDE
import Data.Semigroup (Semigroup (..))
#endif
#ifdef WITH_ARBITRARY_INSTANCE
import Test.QuickCheck (Arbitrary (..), Arbitrary1 (..)
                       , frequency, arbitrary1, shrink1)
#endif

-- | The 'Edit' type encapsulates rewriting.
--
-- Since 'Edit' is also a monad, it allows you to easily "bubble up" information
-- on whether changes were made when working with nested data structures. This
-- is helpful when you want to save the fact that you've reaching a fixed point
-- while rewriting, instead of, say re-computing it after the fact using an 'Eq'
-- instance on the underlying data-type.
--
-- For example,
--
-- >>> halveEvens x = if x `mod` 2 == 0 then (Dirty $ x `div` 2) else (Clean x)
-- >>> traverse halveEvens [1, 2, 3]
-- Dirty [1,1,3]
-- >>> traverse halveEvens [1, 3, 5]
-- Clean [1,3,5]
--
-- To support this behaviour, the 'Applicative' and 'Monad' instances have
-- "polluting" semantics:
--
-- 1. 'pure' = 'Clean' = 'return'.
-- 2. The result of '<*>' is 'Clean' if and only if both the arguments are
--    'Clean'.
-- 3. If you bind a 'Clean' value, you may get anything depending on the
--    function involved. However, if you bind a 'Dirty' value, you will
--    definitely get a 'Dirty' value back.
--
-- If you're familiar with the Writer monad, 'Edit' is isomorphic to
-- @Writer Any@ ('Data.Monoid.Any' is 'Bool' with @(<>) = (||)@).

data Edit a
  = Dirty a -- ^ A value that has been modified.
  | Clean a -- ^ A value that has not been modified.
  deriving
    ( Eq, Show, Read
    , Functor, Foldable, Traversable
    , Generic, NFData, Typeable, Data
    )

instance Applicative Edit where
  pure = Clean
  (<*>) = ap

instance Monad Edit where
  return = pure
  Clean x >>= f = f x
  Dirty x >>= f = dirty (f x)

instance Semigroup a => Semigroup (Edit a) where
  (<>) = liftA2 (<>)

#if MONOID_SUPERCLASS_OF_SEMIGROUP
instance Monoid a => Monoid (Edit a) where
#else
instance (Semigroup a, Monoid a) => Monoid (Edit a) where
#endif
  mempty = Clean mempty
  mappend = (<>)

instance MonadZip Edit where
  mzip = liftA2 (,)

-- These instances have been adapted from Maybe's instances.
instance Eq1 Edit where
  liftEq eq ex ey = eq (extract ex) (extract ey)

instance Show1 Edit where
  liftShowsPrec sp _ d (Clean x) = showsUnaryWith sp "Clean" d x
  liftShowsPrec sp _ d (Dirty x) = showsUnaryWith sp "Dirty" d x

-- Mimicking Maybe's Read1 instance.
#if LIFTREADPREC_IN_READ1
instance Read1 Edit where
  liftReadPrec rp _ =
    readData (readUnaryWith rp "Clean" Clean)
    <|> readData (readUnaryWith rp "Dirty" Dirty)
#else
instance Read1 Edit where
  liftReadsPrec rp _ d =
    readsData (readsUnaryWith rp "Clean" Clean) d
    `mappend` readsData (readsUnaryWith rp "Dirty" Dirty) d
#endif

#if defined(WITH_ARBITRARY_INSTANCE)
instance Arbitrary1 Edit where
  liftArbitrary arb = frequency [(1, Clean <$> arb), (4, Dirty <$> arb)]

  liftShrink shr (Dirty x) = Clean x : liftShrink shr (Clean x) ++ [Dirty x' | x' <- shr x]
  liftShrink shr (Clean x) = [Clean x' | x' <- shr x]

-- | 'arbitrary' is biased towards producing more 'Dirty' values. 'shrink'
-- shrinks the generator towards 'Clean' values.
instance Arbitrary a => Arbitrary (Edit a) where
  arbitrary = arbitrary1
  shrink = shrink1
#endif

-- | Forcibly make the value 'Clean'.
-- You probably do not want to use this function unless you're implementing
-- some class instance for 'Edit'.
clean :: Edit a -> Edit a
clean = Clean . extract

-- | Forcibly make the value 'Dirty'.
-- You probably do not want to use this function unless you're implementing
-- some class instance for 'Edit'.
dirty :: Edit a -> Edit a
dirty = Dirty . extract

-- | Extract the final value after having done some edits.
--
-- Unlike 'Data.Maybe.Maybe''s 'Data.Maybe.fromMaybe', this function doesn't
-- require a default value for totality as both constructors have a value in
-- them.
fromEdit :: Edit a -> a
fromEdit = \case
  Clean x -> x
  Dirty x -> x

-- | Was an edit made (is the value 'Dirty')? If yes, returns 'Just' otherwise
-- 'Nothing'.
--
-- >>> toMaybe (Clean "Good morning.")
-- Nothing
-- >>> toMaybe (Dirty "Wink, wink.")
-- Just "Wink, wink."
toMaybe :: Edit a -> Maybe a
toMaybe = \case
  Clean _ -> Nothing
  Dirty x -> Just x

-- | Takes a clean value and a possibly dirty value and makes an 'Edit'.
--
-- >>> fromMaybe "Hi" Nothing
-- Clean "Hi"
-- >>> defaultValue = 1000
-- >>> correctedValue = Just 1024
-- >>> fromMaybe defaultValue correctedValue
-- Dirty 1024
fromMaybe :: a -> Maybe a -> Edit a
fromMaybe x = \case
  Just y  -> Dirty y
  Nothing -> Clean x

-- | Takes a function that may dirty a value, and returns another which
-- saves the default value if no modification is done.
--
-- @f \`edits\` x == fromMaybe x (f x)@
edits :: (a -> Maybe a) -> a -> Edit a
edits f x = case f x of
  Just y  -> Dirty y
  Nothing -> Clean x

-- | A 'Dirty' value becomes a 'Left' and a 'Clean' value becomes a 'Right'.
--
-- Mnemonic: having things clean is usually the right situation to be in.
toEither :: Edit a -> Either a a
toEither = \case
  Dirty x -> Left x
  Clean x -> Right x

-- | A 'Left' value becomes a 'Dirty' and a 'Right' value becomes a 'Clean'.
--
-- Mnemonic: having things clean is usually the right situation to be in.
fromEither :: Either a a -> Edit a
fromEither = \case
  Left x -> Dirty x
  Right x -> Clean x

-- | Return 'True' iff the argument has the form @Clean _@.
isClean :: Edit a -> Bool
isClean = \case
  Clean _ -> True
  Dirty _ -> False

-- | Returns 'True' iff the argument has the form @Dirty _@.
isDirty :: Edit a -> Bool
isDirty = \case
  Clean _ -> False
  Dirty _ -> True

#if defined(WITH_COMONAD_INSTANCE)
instance Comonad Edit where
  extract = fromEdit
  duplicate = dup

instance ComonadApply Edit where
  (<@>) = (<*>)
  ( @>) = ( *>)
  (<@ ) = (<* )

#elif 1
-- | @extract = fromEdit@. Provided purely for aesthetic reasons.
extract :: Edit a -> a
extract = fromEdit

-- | Wraps the value according to its current status. Like father, like son.
duplicate :: Edit a -> Edit (Edit a)
duplicate = dup

-- | Keep track of changes while utilizing an extraction map.
--
-- > extend f = fmap f . duplicate
extend :: (Edit a -> b) -> Edit a -> Edit b
extend f = fmap f . duplicate
#endif

dup :: Edit a -> Edit (Edit a)
dup = \case
  Clean x -> Clean (Clean x)
  Dirty x -> Dirty (Dirty x)

-- | 'Dirty' values are put on the left and 'Clean' values are put on the right.
--
-- > partitionEdits = partitionEithers . map toEither
partitionEdits :: [Edit a] -> ([a], [a])
partitionEdits = partitionEithers . map toEither

-- | Keep editing till the result is 'Clean' (find the fixed point).
--
-- >>> g x = if x >= 10 then Clean x else Dirty (x + 2)
-- >>> polish g 3
-- 11
--
-- Conceptually,
--
-- > polish f x = last $ iterations f x
polish :: (a -> Edit a) -> a -> a
polish f x = case f x of
  Clean y -> y
  Dirty y -> polish f y

-- | Keep editing till the result is 'Clean', recording iterations.
--
-- Similar to 'polish' but gets the entire list of arguments tested instead of
-- just the final result. The result is guaranteed to be non-empty because
-- the first element will always be included. If the list is finite, the last
-- element gives a 'Clean' result.
--
-- >>> g x = if x >= 10 then Clean x else Dirty (x + 2)
-- >>> iterations g 3
-- [3,5,7,9,11]
--
-- This can be helpful in debugging your transformation function. For example,
--
-- @
-- [ (before, after)
-- | let xs = iterations f start
-- , (before, after) <- zip xs (tail xs)
-- , sanityCheck before && not (sanityCheck after))
-- ]
-- @
iterations :: (a -> Edit a) -> a -> [a]
iterations f = unfoldr (fmap g') . Just
  where g' y = (y, toMaybe (f y))