{-# LANGUAGE AllowAmbiguousTypes   #-}
{-# LANGUAGE ConstraintKinds       #-}
{-# LANGUAGE DataKinds             #-}
{-# LANGUAGE ExplicitNamespaces    #-}
{-# LANGUAGE FlexibleContexts      #-}
{-# LANGUAGE FlexibleInstances     #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE ScopedTypeVariables   #-}
{-# LANGUAGE TypeApplications      #-}
{-# LANGUAGE TypeFamilies          #-}
{-# LANGUAGE TypeOperators         #-}
{-# LANGUAGE UndecidableInstances  #-}
module Versioning.Upgrade
  ( Adapt (..)
    -- * Upgrading
  , Upgrade
  , upgrade
    -- * Downgrading
  , Downgrade
  , downgrade
  )
where

import           Data.Kind                    (Type)

import           Versioning.Base
import           Versioning.Internal.Equality (type (==))

-- | Adapt from a version to another
class Adapt (v :: V) (w :: V) (a :: V -> Type) where
    adapt :: a v -> a w

-- | Upgrade from a lower to a higher version by calling 'adapt' on all
--   the intermediary steps.
upgrade :: forall v w a. Upgrade v w a => a v -> a w
upgrade = upgrade' @(v == w)

-- | This constraint specifies that a value of type 'a' can be upgraded
--   from version 'v' to version 'w'.
type Upgrade v w a = Upgrade' (v == w) v w a

-- | Upgrade from a lower to a higher version by calling 'adapt' on all
--   the intermediary steps.
--   You do not need to define any instance.
--   They are derived automatically if all the intermediary
--   'Adapt' instances are defined.
class Upgrade' (eq :: Bool) (v :: V) (w :: V) (a :: V -> Type) where
    upgrade' :: a v -> a w

instance (v ~ w) => Upgrade' 'True v w a where
    upgrade' x = x

instance (Adapt v (VSucc v) a, Upgrade' (VSucc v == w) (VSucc v) w a)
  => Upgrade' 'False v w a where
    upgrade' x = upgrade' @(VSucc v == w) @(VSucc v) @w (adapt @v @(VSucc v) x)

-- | Downgrade from a higher to a lower version by calling 'adapt' on all
--   the intermediary steps.
downgrade :: forall v w a. Downgrade v w a => a v -> a w
downgrade = downgrade' @(v == w)

-- | This constraint specifies that a value of type 'a' can be downgraded
--   from version 'v' to version 'w'.
type Downgrade v w a = Downgrade' (v == w) v w a

-- | Downgrade from a higher to a lower version by calling 'adapt' on all
--   the intermediary steps.
--   You do not need to define any instance.
--   They are derived automatically if all the intermediary
--   'Adapt' instances are defined.
class Downgrade' (eq :: Bool) (v :: V) (w :: V) (a :: V -> Type) where
    downgrade' :: a v -> a w

instance (v ~ w) => Downgrade' 'True v w a where
    downgrade' x = x

instance (Adapt v (VPred v) a, Downgrade' (VPred v == w) (VPred v) w a)
  => Downgrade' 'False v w a where
    downgrade' x = downgrade' @(VPred v == w) @(VPred v) @w (adapt @v @(VPred v) x)