{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE FunctionalDependencies #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE PolyKinds #-}
{-# OPTIONS_GHC -fno-warn-redundant-constraints #-}
module Data.Grid.Internal.Transpose where

import           Data.Grid.Internal.Grid
import           Data.Grid.Internal.Errors
import           Data.Grid.Internal.Coord
import           GHC.TypeNats
import           GHC.TypeLits                  as TL
import           Data.Singletons.Prelude
import           Data.Singletons.Prelude.List  as L
import           Data.Singletons.Prelude.Maybe
import           Data.Functor.Rep
import           Data.Vector                   as V

type family Permuted (key :: [Nat]) (from :: [Nat]) :: [Nat] where
  Permuted '[] _ = '[]
  Permuted (x:xs) from = (from !! x) : Permuted xs from

type ValidPermutation key from =
  (Sort key == EnumFromTo 0 (Length from TL.- 1)) ?!
    ('Text "Malformed permutation hint: " ':<>: 'ShowType key
                ':$$: 'Text "When permuting matrix of size: " ':<>: 'ShowType from
                ':$$: 'Text "Key must be a permutation of "  ':<>: 'ShowType (EnumFromTo 0 (Length from TL.- 1))
                ':$$: 'Text "e.g. the identity permutation for 2x2 is @[0, 1]"
                ':$$: 'Text "e.g. matrix transpose for 2x2 is @[1, 0]"
  )

-- | Permute dimensions of a 'Grid'. This is similar to MatLab's permute
-- function
--
-- 'permute' requires a type application containing a permutation pattern;
-- The pattern is a re-ordering of the list @[0..n]@ which represents the new
-- dimension order. For example the permutation pattern @[1, 2, 0]@ when
-- applied to the dimensions @[4, 5, 6]@ results in the dimensions @[5, 6, 4]@.
--
-- For 2 dimensional matrixes, a permutation using @[1, 0]@ is simply a 
-- matrix 'transpose'
--
-- > λ> small
-- > fromNestedLists
-- >   [[0,1,2]
-- >   ,[3,4,5]
-- >   ,[6,7,8]]
-- >
-- > λ> permute @[1, 0] small
-- > fromNestedLists
-- >   [[0,3,6]
-- >   ,[1,4,7]
-- >   ,[2,5,8]]
permute
  :: forall (key :: [Nat]) from a invertedKey
   . ( SingI invertedKey
     , invertedKey ~ InvertKey (EnumFromTo 0 (Length from TL.- 1)) key
     , ValidPermutation key from
     , Dimensions from
     , Dimensions (Permuted key from)
     )
  => Grid from a
  -> Grid (Permuted key from) a
permute (Grid v) = result
 where
  result :: Grid (Permuted key from) a
  result = tabulate
    ((v V.!) . fromEnum  . permuteCoord @invertedKey @from)

-- | Permute the dimensions of a coordinate according to a permutation pattern.
-- see 'permute' regarding permutation patterns
permuteCoord
  :: forall (key :: [Nat]) to from
   . (SingI key)
  => Coord from
  -> Coord to
permuteCoord (Coord cs) = Coord newCoord
 where
  key :: [Int]
  key = fromIntegral <$> demote @key
  newCoord :: [Int]
  newCoord = (cs !!) <$> key

-- | Transpose a 2 dimensional matrix. Equivalent to:
--
-- > permute @[1, 0]
transpose :: (KnownNat x, KnownNat y) => Grid '[x, y] a -> Grid '[y, x] a
transpose = permute @'[1, 0]

-- | Get the inverse of a permutation pattern, used internally
type family InvertKey ref key :: [Nat] where
  InvertKey '[] xs = '[]
  InvertKey (n:ns) xs = FromJust (ElemIndex n xs) : InvertKey ns xs