--  GenI surface realiser
--  Copyright (C) 2009 Eric Kow
--
--  This program is free software; you can redistribute it and/or
--  modify it under the terms of the GNU General Public License
--  as published by the Free Software Foundation; either version 2
--  of the License, or (at your option) any later version.
--
--  This program is distributed in the hope that it will be useful,
--  but WITHOUT ANY WARRANTY; without even the implied warranty of
--  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
--  GNU General Public License for more details.
--
--  You should have received a copy of the GNU General Public License
--  along with this program; if not, write to the Free Software
--  Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.

{-# OPTIONS_GHC -fno-warn-orphans #-}
{-# LANGUAGE ExplicitForAll        #-}
{-# LANGUAGE FlexibleContexts      #-}
{-# LANGUAGE FlexibleInstances     #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings     #-}
{-# LANGUAGE TypeSynonymInstances  #-}
-- | Outputting core GenI data to graphviz.
module NLP.GenI.GraphvizShow
where

import           Control.Applicative               ((<$>))
import           Data.FullList                     (fromFL)
import           Data.Int                          (Int64)
import           Data.List                         (nub)
import           Data.Maybe
import           Data.Monoid                       ((<>))
import qualified Data.Text                         as T
import qualified Data.Text.Lazy                    as TL

import           Data.GraphViz
import           Data.GraphViz.Attributes.Complete

import           NLP.GenI.FeatureStructure         (AvPair (..), Flist)
import           NLP.GenI.GeniVal                  (GeniVal (..))
import           NLP.GenI.Graphviz
import           NLP.GenI.Pretty                   hiding ((<>))
import           NLP.GenI.Semantics                (Sem)
import           NLP.GenI.Tag
import           NLP.GenI.TreeSchema               (AdjunctionConstraint(..),
                                                    GNode (..), GType (..))

-- ----------------------------------------------------------------------
--
-- ----------------------------------------------------------------------

-- | Imagine some kind of menu system that displays a list of items
--   and displays the selected item
data GvItem flg itm = GvHeader T.Text      -- ^ no actual item
                    | GvItem T.Text flg itm

gvItemLabel :: GvItem a b -> T.Text
gvItemLabel (GvHeader h)   = h
gvItemLabel (GvItem l _ _) = l

gvItemSetFlag :: f -> GvItem f a -> GvItem f a
gvItemSetFlag _  g@(GvHeader _) = g
gvItemSetFlag f2 (GvItem l _ x) = GvItem l f2 x

instance GraphvizShow a => GraphvizShow (GvItem () a) where
    graphvizShowAsSubgraph _ (GvHeader _ )   = []
    graphvizShowAsSubgraph p (GvItem _ () b) = graphvizShowAsSubgraph p b

    graphvizLabel (GvHeader _)     = ""
    graphvizLabel (GvItem _ () b)  = graphvizLabel b

    graphvizParams (GvHeader _)    = []
    graphvizParams (GvItem _ () b) = graphvizParams b

instance Functor (GvItem flg) where
    fmap _  (GvHeader h)     = GvHeader h
    fmap fn (GvItem l flg x) = GvItem l flg (fn x)

-- ----------------------------------------------------------------------
-- For GraphViz
-- ----------------------------------------------------------------------

type GNodeHighlights = (Bool, Highlights (GNode GeniVal))
type Highlights a    = (a -> Maybe Color)

nullHighlighter :: Highlights a
nullHighlighter = const Nothing

addNullHighlighter :: GvItem Bool x -> GvItem GNodeHighlights x
addNullHighlighter (GvHeader h)   = GvHeader h
addNullHighlighter (GvItem l f x) = GvItem l (f, nullHighlighter) x

instance GraphvizShow (GvItem Bool TagElem) where
    graphvizShowAsSubgraph p = graphvizShowAsSubgraph p . addNullHighlighter
    graphvizLabel  = graphvizLabel  . addNullHighlighter
    graphvizParams = graphvizParams . addNullHighlighter

instance TagItem t => GraphvizShow (GvItem GNodeHighlights t) where
    graphvizShowAsSubgraph _      (GvHeader _) = []
    graphvizShowAsSubgraph prefix (GvItem _ (sf, hfn) te) =
        [ gvShowTree (prefix `TL.append` "DerivedTree0")
                     (fmap toDetails (tgTree te))
        ]
      where
        toDetails x = Details { ddetails = sf
                              , dcolour  = hfn x
                              , dnode    = x
                              }

    graphvizLabel (GvHeader _)    = ""
    graphvizLabel (GvItem _ _ te) =
        gvUnlines [ treename, semlist ]
      where
        -- we display the tree semantics as the graph label
        treename   = "name: "      `TL.append` TL.fromChunks [tgIdName te]
        semlist    = "semantics: " `TL.append` gvShowSem (tgSemantics te)

    graphvizParams _ =
        [ GraphAttrs [ FontSize 10, RankSep [0.3] ]
        , NodeAttrs  [ FontSize 10 ]
        , EdgeAttrs  [ FontSize 10, ArrowHead normal ]
        ]

gvShowSem :: Sem -> TL.Text
gvShowSem = TL.fromStrict. squeezed 70 . map pretty

-- ----------------------------------------------------------------------
-- Helper functions for the TagElem GraphvizShow instance
-- ----------------------------------------------------------------------

data Details n = Details
    { ddetails :: Bool
    , dcolour  :: Maybe Color
    , dnode    :: n
    }

instance GraphvizShowNode (Details (GNode GeniVal)) where
    -- compact -> (node, mcolour) -> String
    graphvizShowNode prefix dn =
        DotNode prefix (body : shapeParams ++ colorParams)
      where
        -- attributes
        filledParam         = Style [SItem Filled []]
        fillcolorParam      = FillColor [toWC (X11Color LemonChiffon)]
        shapeRecordParam    = Shape Record
        shapePlaintextParam = Shape PlainText
        --
        colorParams = FontColor <$> maybeToList (dcolour dn)
        shapeParams = if ddetails dn
                         then [ shapeRecordParam, filledParam, fillcolorParam ]
                         else [ shapePlaintextParam ]
        -- content
        gn    = dnode dn
        stub  = showGnStub gn
        extra = showGnDecorations gn
        summary = if TL.null extra
                     then FieldLabel stub
                     else FlipFields [ FieldLabel stub, FieldLabel extra ]
        body = Label $
            if not (ddetails dn)
               then (StrLabel (graphvizShow_ gn))
               else RecordLabel [ FlipFields $
                                     [ summary
                                     , FieldLabel . showFs $ gup gn
                                     ] ++ maybeFs (gdown gn)
                                ]
          where
            showFs = gvUnlines . map graphvizShow_
            maybeFs fs = if null fs then [] else [FieldLabel (showFs fs)]

instance GraphvizShowString (GNode GeniVal) where
    graphvizShow gn =
        stub `TL.append` extra
      where
        stub  = showGnStub gn
        extra = showGnDecorations gn

instance GraphvizShowString (AvPair GeniVal) where
    graphvizShow (AvPair a v) = TL.fromChunks [a, ":"] `TL.append` graphvizShow_ v

instance GraphvizShowString GeniVal where
    graphvizShow g =
        case (gLabel g, gConstraints g) of
            (Nothing, Nothing) -> "?_"
            (Nothing, Just cs) -> constraints cs
            (Just l,  Nothing) -> plainVar l
            (Just l,  Just cs) -> TL.concat [plainVar l, "/", constraints cs]
      where
        plainVar l = '?' `TL.cons` TL.fromChunks [l]
        constraints cs = TL.intercalate "!" $ map TL.fromChunks [fromFL cs]

showGnDecorations :: GNode GeniVal -> TL.Text
showGnDecorations gn =
    case (gtype gn, gaconstr gn) of
        (Subs, _) -> "↓"
        (Foot, _) -> "*"
        (_, ExplicitNoAdj) -> "ᴺᴬ"
        (_, InferredNoAdj) -> "ᴵᴺᴬ"
        (_, MaybeAdj)      -> ""

showGnStub :: GNode GeniVal -> TL.Text
showGnStub gn =
    TL.intercalate ":" $ filter (not . TL.null) [ cat, idx, lexeme ]
  where
    cat = case getGnVal gup "cat" gn of
              Nothing -> ""
              Just v  -> graphvizShow_ v
    getIdx f = case getGnVal f "idx" gn of
                   Nothing -> ""
                   Just v  -> if isJust (gConstraints v)
                                 then graphvizShowShort 8 v
                                 else ""
    idxT = getIdx gup
    idxB = getIdx gdown
    idx  = tackOn "." idxT idxB
    --
    lexeme  = TL.intercalate "!" $ filter (/= cat)
        [ TL.fromChunks [n] | n <- getLexeme gn ]

getGnVal :: (GNode GeniVal -> Flist GeniVal)
         -> T.Text
         -> GNode GeniVal
         -> Maybe GeniVal
getGnVal getFeat attr gn =
    listToMaybe [ v | AvPair a v <- getFeat gn, a == attr ]

-- | @x `tackOn p` y@` is @TL.concat [x, p, y]@ if @y@ is neither null
--   nor identical to @x@.  Otherwise it is just @x@
tackOn :: TL.Text -> TL.Text -> TL.Text -> TL.Text
tackOn p x y = if TL.null y || x == y then x else TL.concat [ x, p, y ]

graphvizShow_ :: GraphvizShowString a => a -> TL.Text
graphvizShow_ = graphvizShow

-- | If too wide, truncate and display ellipsis
graphvizShowShort :: GraphvizShowString a => Int64 -> a -> TL.Text
graphvizShowShort mx x =
    if TL.length t > mx then TL.take mx t <> "…" else t
  where
    t = graphvizShow x

-- ----------------------------------------------------------------------
-- Derivation tree
-- ----------------------------------------------------------------------

graphvizShowDerivation :: TagDerivation -> [DotSubGraph TL.Text]
graphvizShowDerivation = maybeToList . derivationToGv

derivationToGv :: TagDerivation -> Maybe (DotSubGraph TL.Text)
derivationToGv deriv =
    if null histNodes
        then Nothing
        else Just $ DotSG False Nothing $ DotStmts atts [] nodes edges
  where
    atts = [ NodeAttrs [ Shape PlainText ]
           , EdgeAttrs [ ArrowHead noArrow ]
           ]
    nodes = map mkNode histNodes
    edges = mapMaybe mkEdge deriv
    --
    histNodes = reverse $ nub $ concatMap (\d -> dsChild d : maybeToList (dsParent d)) deriv
    mkNode n  =
        DotNode (gvDerivationLab n) [ Label . StrLabel $ label n ]
    mkEdge ds = do
        p <- dsParent ds
        return $ DotEdge
            (gvDerivationLab p)
            (gvDerivationLab (dsChild ds))
            (edgeStyle ds)
    edgeStyle (AdjunctionStep {}) = [Style [SItem Dashed []]]
    edgeStyle _ = []
    label n =
        case T.splitOn ":" n of
            name:fam:tree:_ -> TL.fromChunks [ name <> ":" <> fam <> "\n" <> tree ]
            _               -> TL.fromChunks [n] `TL.append` " (geni/gv ERROR)"

gvDerivationLab :: T.Text -> TL.Text
gvDerivationLab xs = "Derivation" `TL.append` gvMunge xs

-- | Node names can't have hyphens in them
gvMunge :: T.Text -> TL.Text
gvMunge = TL.fromChunks . T.split (`elem` [':', '-']) . T.replace "." "x"