{-# LANGUAGE
      CPP,
      DerivingVia
  #-}

module Data.Mapping.MapWithDefault where

#if MIN_VERSION_GLASGOW_HASKELL(9,6,0,0)
#else
import Control.Applicative (liftA2)
#endif
import Data.Algebra.Boolean
import Data.List (foldl', groupBy)
import Data.Map.Strict (Map)
import qualified Data.Map.Strict as M
import qualified Data.Map.Merge.Strict as M
import Data.Mapping
import Data.Maybe (fromMaybe, mapMaybe)
import qualified Data.Set as S
import Data.Mapping.Util


-- | Mappings constant except on an enumerated set of values
data MapWithDefault k v = MapWithDefault {
  common :: v,
  exceptions :: Map k v
} deriving (Eq, Ord)

fromList :: (Ord k, Eq v) => v -> [(k,v)] -> MapWithDefault k v
fromList a = MapWithDefault a . M.fromList . mapMaybe (traverse (nonDefault a))

instance (Show k, Show v) => Show (MapWithDefault k v) where
  showsPrec d (MapWithDefault x l) =
    ("fromList " <>) .
    showsPrec d x .
    showList (M.toList l)

fromListWithKey :: (Ord k, Eq v) => v -> (k -> u -> v -> v) -> [(k, u)] -> MapWithDefault k v
fromListWithKey a f = let
  g m (k, x) = M.alter (nonDefault a . f k x . fromMaybe a) k m
  in MapWithDefault a . foldl' g M.empty

instance Foldable (MapWithDefault k) where
  foldMap p (MapWithDefault a f) = p a <> foldMap p f

instance Ord k => Mapping k (MapWithDefault k) where
  cst x = MapWithDefault x M.empty
  mmap p (MapWithDefault a f) = let
    b = p a
    q x = let
      y = p x
      in if b == y then Nothing else Just y
    in MapWithDefault b $ M.mapMaybe q f
  mtraverse p (MapWithDefault a f) = let
    b = p a
    e x y = if x == y then Nothing else Just y
    g _ x = liftA2 e b (p x)
    in liftA2 MapWithDefault b $ M.traverseMaybeWithKey g f
  act (MapWithDefault a f) x = fromMaybe a (M.lookup x f)
  isConst (MapWithDefault a f) = if M.null f then Just a else Nothing
  mergeA h (MapWithDefault a f) (MapWithDefault b g) = let
    e x y = if x == y then Just x else Nothing
    c = h a b
    l = M.traverseMissing (\_ x -> h x b)
    r = M.traverseMissing (\_ y -> h a y)
    h' _ x y = liftA2 e c $ h x y
    t = M.zipWithMaybeAMatched h'
    combine = M.mergeA l r t
    in liftA2 MapWithDefault c $ combine f g
  merge h (MapWithDefault a f) (MapWithDefault b g) = let
    c = h a b
    l = M.mapMissing (\_ x -> h x b)
    r = M.mapMissing (\_ y -> h a y)
    h' _ x y = let
      z = h x y
      in if z == c then Nothing else Just z
    t = M.zipWithMaybeMatched h'
    combine = M.merge l r t
    in MapWithDefault c $ combine f g

-- | This instance assumes that k is unbounded
--
-- It would be possible to do something valid in greater generality (for
-- example, a MaybeBounded class), which might be a good idea.
instance (Enum k, Eq k) => Neighbourly (MapWithDefault k) where
  neighbours (MapWithDefault a f) = let
    c (x,_) (y,_) = succ x == y
    d l = zip ([a] <> l) (l <> [a])
    in S.fromList . concatMap (d . fmap snd) . groupBy c $ M.toAscList f

deriving via (AlgebraWrapper k (MapWithDefault k) b)
  instance (Ord k, Ord b, Semigroup b) => Semigroup (MapWithDefault k b)

deriving via (AlgebraWrapper k (MapWithDefault k) b)
  instance (Ord k, Ord b, Monoid b) => Monoid (MapWithDefault k b)

deriving via (AlgebraWrapper k (MapWithDefault k) b)
  instance (Ord k, Ord b, Num b) => Num (MapWithDefault k b)

deriving via (AlgebraWrapper k (MapWithDefault k) b)
  instance (Ord k, Ord b, Boolean b) => Boolean (MapWithDefault k b)
