-----------------------------------------------------------------------------
-- Copyright 2016, Ideas project team. This file is distributed under the
-- terms of the Apache License 2.0. For more information, see the files
-- "LICENSE.txt" and "NOTICE.txt", which are included in the distribution.
-----------------------------------------------------------------------------
-- |
-- Maintainer  :  bastiaan.heeren@ou.nl
-- Stability   :  provisional
-- Portability :  portable (depends on ghc)
--
-- Datatype for representing derivations as a tree. The datatype stores all
-- intermediate results as well as annotations for the steps.
--
-----------------------------------------------------------------------------

module Ideas.Common.DerivationTree
   ( -- * Data types
     DerivationTree
     -- * Constructors
   , singleNode, addBranches, makeTree
     -- * Query
   , root, endpoint, branches, subtrees
   , leafs, lengthMax
     -- * Adapters
   , restrictHeight, restrictWidth, updateAnnotations
   , cutOnStep, mergeMaybeSteps, sortTree, cutOnTerm
     -- * Conversions
   , derivation, randomDerivation, derivations
   ) where

import Control.Arrow
import Control.Monad
import Data.List
import Data.Maybe
import Ideas.Common.Classes
import Ideas.Common.Derivation
import System.Random

-----------------------------------------------------------------------------
-- Data type definitions for derivation trees and derivation lists

data DerivationTree s a = DT
   { root     :: a                           -- ^ The root of the tree
   , endpoint :: Bool                        -- ^ Is this node an endpoint?
   , branches :: [(s, DerivationTree s a)]   -- ^ All branches
   }
 deriving Show

instance Functor (DerivationTree s) where
   fmap = mapSecond

instance BiFunctor DerivationTree where
   biMap f g (DT a b xs) = DT (g a) b (map (biMap f (biMap f g)) xs)

-----------------------------------------------------------------------------
-- Constructors for a derivation tree

-- | Constructs a node without branches; the boolean indicates whether the
-- node is an endpoint or not
singleNode :: a -> Bool -> DerivationTree s a
singleNode a b = DT a b []

-- | Branches are attached after the existing ones (order matters)
addBranches :: [(s, DerivationTree s a)] -> DerivationTree s a -> DerivationTree s a
addBranches new (DT a b xs) = DT a b (xs ++ new)

makeTree :: (a -> (Bool, [(s, a)])) -> a -> DerivationTree s a
makeTree f = rec
 where
   rec a = let (b, xs) = f a
           in addBranches (map (mapSecond rec) xs) (singleNode a b)

-----------------------------------------------------------------------------
-- Inspecting a derivation tree

-- | Returns the annotations at a given node
annotations :: DerivationTree s a -> [s]
annotations = map fst . branches

-- | Returns all subtrees at a given node
subtrees :: DerivationTree s a -> [DerivationTree s a]
subtrees = map snd . branches

-- | Returns all leafs, i.e., final results in derivation. Be careful:
-- the returned list may be very long
leafs :: DerivationTree s a -> [a]
leafs t = [ root t | endpoint t ] ++ concatMap leafs (subtrees t)

-- | The argument supplied is the maximum number of steps; if more steps are
-- needed, Nothing is returned
lengthMax :: Int -> DerivationTree s a -> Maybe Int
lengthMax n = join . fmap (f . derivationLength) . derivation
            . commit . restrictHeight (n+1)
 where
    f i = if i<=n then Just i else Nothing

updateAnnotations :: (a -> s -> a -> t) -> DerivationTree s a -> DerivationTree t a
updateAnnotations f = rec
 where
   rec (DT a b xs) =
      let g (s, t) = (f a s (root t), rec t)
      in DT a b (map g xs)

-----------------------------------------------------------------------------
-- Changing a derivation tree

-- | Restrict the height of the tree (by cutting off branches at a certain depth).
-- Nodes at this particular depth are turned into endpoints
restrictHeight :: Int -> DerivationTree s a -> DerivationTree s a
restrictHeight n t
   | n == 0    = singleNode (root t) True
   | otherwise = t {branches = map f (branches t)}
 where
   f = mapSecond (restrictHeight (n-1))

-- | Restrict the width of the tree (by cutting off branches).
restrictWidth :: Int -> DerivationTree s a -> DerivationTree s a
restrictWidth n = rec
 where
   rec t = t {branches = map (mapSecond rec) (take n (branches t))}

-- | Commit to the left-most derivation (even if this path is unsuccessful)
commit :: DerivationTree s a -> DerivationTree s a
commit = restrictWidth 1

-- | Filter out intermediate steps, and merge its branches (and endpoints) with
-- the rest of the derivation tree
mergeSteps :: (s -> Bool) -> DerivationTree s a -> DerivationTree s a
mergeSteps p = rec
 where
   rec t = addBranches (concat list) (singleNode (root t) isEnd)
    where
      new = map rec (subtrees t)
      (bools, list) = unzip (zipWith f (annotations t) new)
      isEnd = endpoint t || or bools
      f s st
         | p s       = (False, [(s, st)])
         | otherwise = (endpoint st, branches st)

sortTree :: (l -> l -> Ordering) -> DerivationTree l a -> DerivationTree l a
sortTree f t = t {branches = change (branches t) }
 where
   change = map (mapSecond (sortTree f)) . sortBy cmp
   cmp (l1, _) (l2, _) = f l1 l2

mergeMaybeSteps :: DerivationTree (Maybe s) a -> DerivationTree s a
mergeMaybeSteps = mapFirst fromJust . mergeSteps isJust

cutOnStep :: (s -> Bool) -> DerivationTree s a -> DerivationTree s a
cutOnStep p = rec
 where
   rec t = t {branches = map f (branches t)}
   f (s, t)
      | p s       = (s, singleNode (root t) True)
      | otherwise = (s, rec t)

cutOnTerm :: (a -> Bool) -> DerivationTree s a -> DerivationTree s a
cutOnTerm p (DT r e bs) =
    DT r e (map (second (cutOnTerm p)) $ filter (not . p . root . snd) bs)

-----------------------------------------------------------------------------
-- Conversions from a derivation tree

-- | All possible derivations (returned in a list)
derivations :: DerivationTree s a -> [Derivation s a]
derivations t =
   [ emptyDerivation (root t) | endpoint t ] ++
   [ (root t, r) `prepend` d | (r, st) <- branches t, d <- derivations st ]

-- | The first derivation (if any)
derivation :: DerivationTree s a -> Maybe (Derivation s a)
derivation = listToMaybe . derivations

-- | Return  a random derivation (if any exists at all)
randomDerivation :: RandomGen g => g -> DerivationTree s a -> Maybe (Derivation s a)
randomDerivation g t = msum xs
 where
   (xs, g0) = shuffle g list
   list     = [ Just (emptyDerivation (root t)) | endpoint t ] ++
              map make (branches t)
   make (r, st) = do
      d <- randomDerivation g0 st
      return ((root t, r) `prepend` d)

shuffle :: RandomGen g => g -> [a] -> ([a], g)
shuffle g0 xs = rec g0 [] (length xs) xs
 where
   rec g acc n ys =
      case splitAt i ys of
         (as, b:bs) -> rec g1 (b:acc) (n-1) (as++bs)
         _ -> (acc, g)
    where
      (i, g1) = randomR (0, n-1) g