{-# LANGUAGE DefaultSignatures #-}

-- | Equality and rendering of 'AST's

module Language.Syntactic.Interpretation
    ( -- * Equality
      Equality (..)
      -- * Rendering
    , Render (..)
    , renderArgsSmart
    , render
    , StringTree (..)
    , stringTree
    , showAST
    , drawAST
    , writeHtmlAST
      -- * Default interpretation
    , equalDefault
    , hashDefault
    ) where



import Data.Tree (Tree (..))

import Data.Hash (Hash, combine, hashInt)
import qualified Data.Hash as Hash
import Data.Tree.View

import Language.Syntactic.Syntax



----------------------------------------------------------------------------------------------------
-- * Equality
----------------------------------------------------------------------------------------------------

-- | Higher-kinded equality
class Equality e
  where
    -- | Higher-kinded equality
    --
    -- Comparing elements of different types is often needed when dealing with expressions with
    -- existentially quantified sub-terms.
    equal :: e a -> e b -> Bool
    default equal :: Render e => e a -> e b -> Bool
    equal = equalDefault

    -- | Higher-kinded hashing. Elements that are equal according to 'equal' must result in the same
    -- hash:
    --
    -- @equal a b  ==>  hash a == hash b@
    hash :: e a -> Hash
    default hash :: Render e => e a -> Hash
    hash = hashDefault

instance Equality sym => Equality (AST sym)
  where
    equal (Sym s1)   (Sym s2)   = equal s1 s2
    equal (s1 :$ a1) (s2 :$ a2) = equal s1 s2 && equal a1 a2
    equal _ _                   = False

    hash (Sym s)  = hashInt 0 `combine` hash s
    hash (s :$ a) = hashInt 1 `combine` hash s `combine` hash a

instance Equality sym => Eq (AST sym a)
  where
    (==) = equal

instance (Equality sym1, Equality sym2) => Equality (sym1 :+: sym2)
  where
    equal (InjL a) (InjL b) = equal a b
    equal (InjR a) (InjR b) = equal a b
    equal _ _               = False

    hash (InjL a) = hashInt 0 `combine` hash a
    hash (InjR a) = hashInt 1 `combine` hash a

instance (Equality sym1, Equality sym2) => Eq ((sym1 :+: sym2) a)
  where
    (==) = equal

instance Equality Empty
  where
    equal = error "equal: Empty"
    hash  = error "hash: Empty"

instance Equality sym => Equality (Typed sym)
  where
    equal (Typed s1) (Typed s2) = equal s1 s2
    hash (Typed s) = hash s



----------------------------------------------------------------------------------------------------
-- * Rendering
----------------------------------------------------------------------------------------------------

-- | Render a symbol as concrete syntax. A complete instance must define at least the 'renderSym'
-- method.
class Render sym
  where
    -- | Show a symbol as a 'String'
    renderSym :: sym sig -> String

    -- | Render a symbol given a list of rendered arguments
    renderArgs :: [String] -> sym sig -> String
    renderArgs []   s = renderSym s
    renderArgs args s = "(" ++ unwords (renderSym s : args) ++ ")"

instance (Render sym1, Render sym2) => Render (sym1 :+: sym2)
  where
    renderSym (InjL s) = renderSym s
    renderSym (InjR s) = renderSym s
    renderArgs args (InjL s) = renderArgs args s
    renderArgs args (InjR s) = renderArgs args s

-- | Implementation of 'renderArgs' that handles infix operators
renderArgsSmart :: Render sym => [String] -> sym a -> String
renderArgsSmart []   sym = renderSym sym
renderArgsSmart args sym
    | isInfix   = "(" ++ unwords [a,op,b] ++ ")"
    | otherwise = "(" ++ unwords (name : args) ++ ")"
  where
    name  = renderSym sym
    [a,b] = args
    op    = init $ tail name
    isInfix
      =  not (null name)
      && head name == '('
      && last name == ')'
      && length args == 2

-- | Render an 'AST' as concrete syntax
render :: forall sym a. Render sym => ASTF sym a -> String
render = go []
  where
    go :: [String] -> AST sym sig -> String
    go args (Sym s)  = renderArgs args s
    go args (s :$ a) = go (render a : args) s

instance Render Empty
  where
    renderSym  = error "renderSym: Empty"
    renderArgs = error "renderArgs: Empty"

instance Render sym => Render (Typed sym)
  where
    renderSym (Typed s)  = renderSym s
    renderArgs args (Typed s) = renderArgs args s

instance Render sym => Show (ASTF sym a)
  where
    show = render



-- | Convert a symbol to a 'Tree' of strings
class Render sym => StringTree sym
  where
    -- | Convert a symbol to a 'Tree' given a list of argument trees
    stringTreeSym :: [Tree String] -> sym a -> Tree String
    stringTreeSym args s = Node (renderSym s) args

instance (StringTree sym1, StringTree sym2) => StringTree (sym1 :+: sym2)
  where
    stringTreeSym args (InjL s) = stringTreeSym args s
    stringTreeSym args (InjR s) = stringTreeSym args s

instance StringTree Empty

instance StringTree sym => StringTree (Typed sym)
  where
    stringTreeSym args (Typed s) = stringTreeSym args s

-- | Convert an 'AST' to a 'Tree' of strings
stringTree :: forall sym a . StringTree sym => ASTF sym a -> Tree String
stringTree = go []
  where
    go :: [Tree String] -> AST sym sig -> Tree String
    go args (Sym s)  = stringTreeSym args s
    go args (s :$ a) = go (stringTree a : args) s

-- | Show a syntax tree using ASCII art
showAST :: StringTree sym => ASTF sym a -> String
showAST = showTree . stringTree

-- | Print a syntax tree using ASCII art
drawAST :: StringTree sym => ASTF sym a -> IO ()
drawAST = putStrLn . showAST

-- | Write a syntax tree to an HTML file with foldable nodes
writeHtmlAST :: StringTree sym => FilePath -> ASTF sym a -> IO ()
writeHtmlAST file
    = writeHtmlTree Nothing file
    . fmap (\n -> NodeInfo InitiallyExpanded n "") . stringTree



----------------------------------------------------------------------------------------------------
-- * Default interpretation
----------------------------------------------------------------------------------------------------

-- | Default implementation of 'equal'
equalDefault :: Render sym => sym a -> sym b -> Bool
equalDefault a b = renderSym a == renderSym b

-- | Default implementation of 'hash'
hashDefault :: Render sym => sym a -> Hash
hashDefault = Hash.hash . renderSym
