module Sifflet.Data.TreeLayout 
    (VColor(..), white, cream, black, lightBlue, lightBlueGreen
    , lightGray, mediumGray, darkGray
    , yellow, darkBlueGreen, blueGreen
    , Style(..), VFont(..), defaultStyle, style0, style1, style2, style3
    , wstyle
    , styleIncreasePadding
    , FontTextExtents(..), setFont, measureText, styleTextExtents, ftExtents
    , TextBox(..), makeTextBox, tbWidth, tbHeight, tbBottom
    , tbCenter, tbTextCenter, tbBoxCenter, offsetTextBoxCenters
    , tbSetWidth
    , treeSizes
    , GNode(..), treeGNodes, gnodeText, gnodeTextBB
    , Iolet(..), ioletCenter, makeIolets, makeIoletsRow
    , pointInIolet
    , IoletCounter, zeroIoletCounter
    , makeGNode
    , TreeLayout, LayoutNode(..)
    , layoutNodeSource, layoutRootBB, layoutTreeBB
    , treeLayout
    , treeLayoutPaddedSize, treeLayoutSize, treeLayoutWidth
    , layoutTreeMoveCenterTo
    , pointInLayoutNode, pointInGNode, findRect
    , treeLayoutWiden
    )
where
import System.IO.Unsafe
import Graphics.Rendering.Cairo (FontExtents(..),
                                 FontSlant(..), FontWeight(..),
                                 Format(..), Render,
                                 TextExtents(..),
                                 fontExtents,
                                 renderWith,
                                 textExtents,
                                 selectFontFace, setFontSize,
                                 withImageSurface)
import Graphics.UI.Gtk (Rectangle)
import Data.Traversable () 
import Sifflet.Data.Geometry
import Sifflet.Data.Tree as T hiding (tree)
import Sifflet.Text.Repr ()
import Sifflet.Util
data VColor = ColorRGB Double Double Double |
              ColorRGBA Double Double Double Double
black, white, cream, lightBlue, lightBlueGreen :: VColor
black = ColorRGB 0 0 0
white = ColorRGB 1 1 1
cream = ColorRGB 0.94902 0.94902 0.82745
lightBlue = ColorRGB 0.43529 0.43922 0.95686
lightBlueGreen = ColorRGB 0 1 1
lightGray, mediumGray, darkGray :: VColor
lightGray = ColorRGBA 0.75 0.75 0.75 0.5
mediumGray = ColorRGB 0.5 0.5 0.5
darkGray = ColorRGB 0.25 0.25 0.25
yellow, darkBlueGreen, blueGreen :: VColor
yellow = ColorRGB 0.9 0.9 0
darkBlueGreen = ColorRGB 0 0.3 0.3
blueGreen = ColorRGB 0 0.4 0.4
data VFont = VFont {vfontFace :: String, vfontSlant :: FontSlant,
                    vfontWeight :: FontWeight,
                    vfontSize :: Double}
data Style = 
    Style {styleFont :: VFont,
           lineWidth :: Double,
           textMargin :: Double, 
           hpad, vpad :: Double, 
           exomargin :: Double,  
           vtinypad :: (Double, Double), 
           styleFramePad :: Double,      
           styleNormalTextColor, styleNormalFillColor, 
           styleNormalEdgeColor,
           styleActiveTextColor, styleActiveFillColor,
           styleActiveEdgeColor,
           styleSelectedTextColor, styleSelectedFillColor, 
           styleSelectedEdgeColor,
           styleTetherColor :: VColor,
           
           styleAuxOffset :: Position, 
           styleAuxColor :: VColor,
           styleAuxFont :: VFont,
           styleIoletRadius :: Double, 
           styleShowNodeBoxes :: Bool, 
           styleShowNodePorts :: Bool  
          }
styleIncreasePadding :: Style -> Double -> Style
styleIncreasePadding style factor =
  style {hpad = factor * hpad style,
         vpad = factor * vpad style}
style0, style1, style2, style3, defaultStyle :: Style
style0 = 
    let green = ColorRGB 0.1 0.9 0.1
        veryDarkGray = ColorRGB 0.1 0.1 0.1
        brighterGreen = ColorRGB 0.5 0.95 0.5
    in Style {styleFont = VFont "serif" FontSlantNormal FontWeightNormal 18, 
              lineWidth = 2, 
              textMargin = 18.0, 
              hpad = 27, vpad = 36, 
              exomargin = 0,
              vtinypad = (4.5, 4.5),  
              styleFramePad = 35,          
              
              styleNormalTextColor = green,
              styleNormalEdgeColor = green,
              styleNormalFillColor = veryDarkGray,
              styleActiveTextColor = brighterGreen,
              styleActiveEdgeColor = brighterGreen,
              styleActiveFillColor = mediumGray,
              styleSelectedTextColor = blueGreen,
              styleSelectedEdgeColor = blueGreen,
              styleSelectedFillColor = darkGray,
              styleTetherColor = white,
              styleAuxOffset = Position 0.0 (20.0),
              styleAuxColor = lightGray,
              styleAuxFont = 
                  VFont "serif" FontSlantItalic FontWeightNormal 12,
              styleIoletRadius = 5,
              styleShowNodeBoxes = True, 
              styleShowNodePorts = True  
             }
style1 = 
    let veryDarkBlue = ColorRGB 0 0 0.1
        pinkIHope = ColorRGB 1 0.9 0.9
    in style0 {styleNormalTextColor = veryDarkBlue,
               styleNormalEdgeColor = veryDarkBlue,
               styleNormalFillColor = lightGray,
               styleActiveTextColor = pinkIHope,
               styleActiveEdgeColor = pinkIHope
              }
style2 = 
    let darkPink = ColorRGB 1 0.5 0.5
    in style1 {styleNormalFillColor = white,
               styleActiveTextColor = darkPink,
               styleActiveEdgeColor = darkPink}
style3 = 
    let semiTranspBlue = ColorRGBA 0 0.2 0.9 0.5
    in style0 {exomargin = 10,
               styleNormalTextColor = yellow,
               styleNormalEdgeColor = yellow,
               styleNormalFillColor = darkBlueGreen,
               styleActiveTextColor = yellow,
               styleActiveEdgeColor = yellow,
               styleActiveFillColor = blueGreen,
               styleAuxColor = semiTranspBlue
              } 
defaultStyle = style3
wstyle :: Style
wstyle = 
    style3 { 
           
           styleFont = VFont "serif" FontSlantNormal FontWeightNormal 14,
           textMargin = 8.0,
           hpad = 10.0, vpad = 24.0, 
           vtinypad = (0.0, 0.0),
           exomargin = 10.0,    
           styleFramePad = 15,
           
           styleNormalTextColor = yellow,
           styleNormalEdgeColor = mediumGray,
           styleNormalFillColor = darkBlueGreen,
           styleActiveTextColor = yellow,
           styleActiveEdgeColor = mediumGray,
           styleActiveFillColor = blueGreen,
           styleSelectedTextColor = black,
           styleSelectedEdgeColor = lightBlueGreen,
           styleSelectedFillColor = lightBlueGreen,
           styleTetherColor =
               ColorRGBA 0.7 0.7 0.7 0.3, 
           
           
           styleAuxOffset = Position 0 (16),
           styleAuxFont = VFont "serif" FontSlantItalic FontWeightNormal 12,
           styleAuxColor = ColorRGBA 0.9 0.5 0.5 1.0,
               
           styleIoletRadius = 5
         }
 
pointInLayoutNode :: Position -> LayoutNode e -> Bool
pointInLayoutNode point = pointInGNode point . nodeGNode
pointInGNode :: Position -> GNode e -> Bool
pointInGNode point = pointInBB point . gnodeNodeBB
findNode :: Position -> TreeLayout e -> Maybe (LayoutNode e)
findNode point (T.Node node@(LayoutNode _rootGNode treeBB) sublayouts) =
    if pointInBB point treeBB
    then if pointInLayoutNode point node
         then Just node
         else findInSubs point sublayouts
    else Nothing
    where findInSubs :: Position -> [TreeLayout e] -> Maybe (LayoutNode e)
          findInSubs _p [] = Nothing
          findInSubs p (l:ls) = 
              let found = findNode p l in
              case found of
                (Just _) -> found
                Nothing -> findInSubs p ls
findRect :: Position -> TreeLayout e -> Maybe Rectangle
findRect point tlo = 
    case findNode point tlo of
      Nothing -> Nothing
      Just node -> Just (bbToRect (gnodeNodeBB (nodeGNode node)))
setFont :: VFont -> Render ()
setFont (VFont face slant weight size) = do
  selectFontFace face slant weight
  setFontSize size
data FontTextExtents = FontTextExtents {
      fontAscent::Double, fontDescent::Double, fontHeight::Double,
      fontMaxXadvance::Double, fontMaxYadvance::Double,
      textXbearing::Double, textYbearing::Double,
      extTextWidth::Double, extTextHeight::Double,
      textXadvance::Double, textYadvance::Double}
                       deriving (Eq, Read, Show)
measureText :: Style -> String -> Size
measureText style str =
    let extents = styleTextExtents style str
        in Size (textXadvance extents)
                (fontHeight extents + fontDescent extents)
styleTextExtents :: Style -> String -> FontTextExtents
styleTextExtents style str = 
    unsafePerformIO $ 
    withImageSurface FormatARGB32 0 0 $ \ surface ->
        renderWith surface $ 
          setFont (styleFont style) >>
          ftExtents str
ftExtents :: String -> Render FontTextExtents
ftExtents text = do
  FontExtents asc desc fheight maxxadv maxyadv <- fontExtents
  TextExtents xbear ybear twidth theight xadv yadv <- textExtents text
  return (FontTextExtents asc desc fheight maxxadv maxyadv
                          xbear ybear twidth theight xadv yadv)
data TextBox = TextBox {tbText :: String,
                        tbTextBB :: BBox, 
                        tbBoxBB :: BBox 
                       }
             deriving (Eq, Read, Show)
makeTextBox :: Style -> String -> TextBox
makeTextBox style text =
  let extents = styleTextExtents style text
      Size textW textH = measureText style text
      
      
      margin = textMargin style
      boxW = textW + 2.0 * margin 
      boxH = textH + 2.0 * margin
      boxX = 0
      boxY = 0
      textX = (boxW  textW) / 2.0
      
      
      
      raise = 0.5 * fontDescent extents
      textY = fontHeight extents + (boxH  textH) / 2.0  raise
      textBB = BBox textX textY textW textH
      boxBB = BBox boxX boxY boxW boxH
  in TextBox text textBB boxBB
instance Widen TextBox where
    widen tb@(TextBox _text textBB boxBB) minWidth =
        let w = bbWidth boxBB
        in if w >= minWidth
           then tb
           else let dw = minWidth  w 
                in tb {tbTextBB = translate (dw / 2) 0 textBB,
                       tbBoxBB = widen boxBB minWidth}
instance Translate TextBox where
    translate dx dy (TextBox text textBB boxBB) = 
        TextBox text (translate dx dy textBB) (translate dx dy boxBB)
tbWidth :: TextBox -> Double
tbWidth = bbWidth .tbBoxBB
tbSetWidth :: TextBox -> Double -> TextBox
tbSetWidth tbox w = 
    let TextBox text textBB boxBB = tbox
        boxBB' = bbSetWidth boxBB w
        (dx, dy) = positionDelta (bbCenter boxBB) (bbCenter boxBB')
    in TextBox text (translate dx dy textBB) boxBB'
tbHeight :: TextBox -> Double
tbHeight = bbHeight . tbBoxBB
tbBottom :: TextBox -> Double
tbBottom = bbBottom . tbBoxBB
tbCenter :: TextBox -> Position
tbCenter = tbBoxCenter
 
tbBoxCenter :: TextBox -> Position
tbBoxCenter = bbCenter . tbBoxBB
tbTextCenter :: TextBox -> Position
tbTextCenter = bbCenter . tbTextBB
offsetTextBoxCenters :: Position -> TextBox -> TextBox -> TextBox
offsetTextBoxCenters offset anchor floater =
  
  
  
  let Position ax ay = tbBoxCenter anchor
      Position fx fy = tbBoxCenter floater
      Position ox oy = offset
      
      
      dx = ax + ox  fx
      dy = ay + oy  fy
  in translate dx dy floater
data GNode e = GNode {gnodeValue :: e, 
                      gnodeTextBoxes :: [TextBox], 
                      gnodeNodeBB :: BBox,    
                      gnodeInlets :: [Iolet], 
                      gnodeOutlets :: [Iolet] 
                     }
               deriving (Eq)
gnodeText :: GNode e -> String
gnodeText = tbText . head . gnodeTextBoxes
gnodeTextBB :: GNode e -> BBox
gnodeTextBB = tbTextBB . head . gnodeTextBoxes
instance (Show e) => Show (GNode e) where
    show (GNode v tbs nodeBB inlets outlets) = 
        par "GNode" [show v, show tbs, show nodeBB, show inlets, show outlets]
instance Translate (GNode e) where
    translate dx dy (GNode value textboxes nodeBB inlets outlets) = 
          GNode value 
                (map (translate dx dy) textboxes)
                (translate dx dy nodeBB)
                (translate dx dy inlets)
                (translate dx dy outlets)
type IoletCounter e = e -> (Int, Int)
zeroIoletCounter :: IoletCounter e
zeroIoletCounter _node = (0, 0)
makeGNode :: (Repr e) => Style -> IoletCounter e -> e -> GNode e
makeGNode style countIolets value =
  
  
  let textboxes1 = map (makeTextBox style) (reprl value)
      textboxes2 = case textboxes1 of
                     [_tb] -> textboxes1
                     [tb1, tb2] -> 
                         [tb1, 
                          offsetTextBoxCenters (styleAuxOffset style) tb1 tb2]
                     _ -> wrong textboxes1
      nodeBB = case textboxes2 of
                 [tb] -> tbBoxBB tb
                 [tb1, tb2] -> bbMerge (tbBoxBB tb1) (tbBoxBB tb2)
                 _ -> wrong textboxes2
      wrong tbs = 
          errcats ["makeGNode: wrong no. of text boxes;",
                   "expected 1 or 2, but got", show (length tbs)]
      (inlets, outlets) = makeIolets style nodeBB (countIolets value)
  in GNode value textboxes2 nodeBB inlets outlets
makeIolets :: Style -> BBox -> (Int, Int) -> ([Iolet], [Iolet])
makeIolets style bbox (nin, nout) =
    
    (makeIoletsRow style (bbXCenter bbox) (bbBottom bbox) nin,
     makeIoletsRow style (bbXCenter bbox) (bbTop bbox) nout)
makeIoletsRow :: Style -> Double -> Double -> Int -> [Iolet]
makeIoletsRow style cx cy n =
    
    let radius = styleIoletRadius style
        diam = 2 * radius 
        w = fromIntegral n * diam
        x1 = cx  w / 2 + radius     
        x i = x1 + fromIntegral (i  1) * diam
        make i = Iolet (Circle (Position (x i) cy) radius)
    in map make [1..n]
data Iolet = Iolet Circle
             deriving (Eq, Read, Show)
instance Translate Iolet where
    translate dx dy (Iolet circle) = Iolet (translate dx dy circle)
ioletCenter :: Iolet -> Position
ioletCenter (Iolet circle) = circleCenter circle
pointInIolet :: Position -> Iolet -> Bool
pointInIolet point (Iolet circle) = pointInCircle point circle
treeSizes :: Style -> Tree (GNode e) -> Tree Size
treeSizes style gTree = 
    let subtreeSizes = map (treeSizes style) (subForest gTree)
        (BBox _ _ rootWidth rootHeight) = gnodeNodeBB (rootLabel gTree)
        treeWidth = max rootWidth (paddedWidth style subtreeSizes)
        treeHeight = rootHeight + paddedHeight style subtreeSizes
    in T.Node (Size treeWidth treeHeight) subtreeSizes
paddedWidth :: Style -> [Tree Size] -> Double
paddedWidth _style [] = 0
paddedWidth style subtrees =
    sum [w | (T.Node (Size w _h) _) <- subtrees] +
    hpad style * fromIntegral (length subtrees  1)
paddedHeight :: Style -> [Tree Size] -> Double
paddedHeight _style [] = 0
paddedHeight style subtrees = 
    vpad style + maximum [h | (T.Node (Size _w h) _) <- subtrees]
type TreeLayout e = Tree (LayoutNode e)
data LayoutNode e = LayoutNode {nodeGNode :: GNode e,
                                nodeTreeBB :: BBox}
                  deriving (Eq)
layoutNodeSource :: LayoutNode e -> e
layoutNodeSource = gnodeValue . nodeGNode
instance (Show e) => Show (LayoutNode e) where
  show (LayoutNode gnode treebb) = par "LayoutNode" [show gnode, show treebb]
instance Translate (LayoutNode e) where
    translate dx dy (LayoutNode gnode treeBB) = 
        LayoutNode (translate dx dy gnode)
                   (translate dx dy treeBB)
instance Widen (LayoutNode e) where
  widen node@(LayoutNode gNode treeBB) minWidth =
    let dw = bbWidth treeBB  minWidth
    in if dw <= 0
       then node
       else LayoutNode (translate (dw / 2) 0 gNode)
                       (widen treeBB minWidth)
layoutRootBB :: TreeLayout e -> BBox
layoutRootBB = gnodeNodeBB . nodeGNode . rootLabel
layoutTreeBB :: TreeLayout e -> BBox
layoutTreeBB = nodeTreeBB . rootLabel
treeLayout :: (Repr e) => Style -> IoletCounter e -> Tree e -> TreeLayout e
treeLayout style counter tree = 
  let t1 = treeGNodes style counter tree
      t2 = treeSizes style t1
      start = Position (hpad style) (vpad style)
      t3 = treeLayout2 style start tree t1 t2
  in treeLayoutAddMargin t3 (exomargin style)
treeGNodes :: Repr e => 
  Style -> IoletCounter e -> Tree e -> Tree (GNode e)
treeGNodes style counter tree = fmap (makeGNode style counter) tree
treeLayout2 :: (Repr e) =>
               Style 
            -> Position  
            -> Tree e  
                       
                       
            -> Tree (GNode e) 
            -> Tree Size 
            -> TreeLayout e
treeLayout2 style
            (Position startX startY)
            (T.Node _root subtrees)
            (T.Node gnode subGNodes)                          
            (T.Node (Size treeWidth treeHeight) subTreeSizes) 
    = 
      let 
        nodeHeight = bbHeight (gnodeNodeBB gnode)
        
        
        
        
        
        subtreesTotalWidth =  paddedWidth style subTreeSizes
        subX = startX + (treeWidth  subtreesTotalWidth) / 2 
        subY = startY + nodeHeight + vpad style
        sublayouts :: (Repr e) =>
                      Double -> [Tree e] -> [Tree (GNode e)] -> [Tree Size] -> 
                      [TreeLayout e]
        sublayouts _ [] [] [] = []
        sublayouts x (t:ts) (g:gs) (s:ss) = 
            treeLayout2 style (Position x subY) t g s :
            sublayouts (x + sizeW (rootLabel s) + hpad style) ts gs ss
        sublayouts _ _ _ _ = error "treeLayout2: mismatched list lengths"
    in T.Node (LayoutNode 
               (centerGNode gnode startX startY treeWidth nodeHeight) 
               (BBox startX startY treeWidth treeHeight)) 
           (sublayouts subX subtrees subGNodes subTreeSizes)
centerGNode :: GNode e -> Double -> Double -> Double -> Double -> GNode e
centerGNode gnode startx starty awidth aheight =
    let cx = startx + awidth / 2 
        cy = starty + aheight / 2
        Position cgx cgy = bbCenter (gnodeNodeBB gnode)
    in translate (cx  cgx) (cy  cgy) gnode
    
treeLayoutPaddedSize :: Style -> TreeLayout e -> Size
treeLayoutPaddedSize style tlo =
  let Size w h = treeLayoutSize tlo
  in Size (w + 2.0 * hpad style) (h + 2.0 * vpad style)
treeLayoutSize :: TreeLayout e -> Size
treeLayoutSize tlo = 
    let BBox _x _y w h = layoutTreeBB tlo in Size w h
layoutTreeMoveCenterTo :: 
    Double -> Double -> TreeLayout e -> TreeLayout e
layoutTreeMoveCenterTo newX newY layoutTree =
    let Position oldX oldY = bbCenter (nodeTreeBB (rootLabel layoutTree))
    in translate (newX  oldX) (newY  oldY) layoutTree
treeLayoutAddMargin :: TreeLayout e -> Double -> TreeLayout e
treeLayoutAddMargin tree margin =
    let LayoutNode {nodeGNode = rootGNode, nodeTreeBB = treeBB} =
            rootLabel tree
        subtrees = subForest tree
        BBox x y w h = treeBB
        treeBB' = BBox (x  margin) (y  margin) 
                  (w + 2 * margin) (h + 2 * margin)
        root' = translate margin margin (LayoutNode rootGNode treeBB')
        subtrees' = translate margin margin subtrees
    in T.Node root' subtrees'
        
treeLayoutWidth :: TreeLayout e -> Double
treeLayoutWidth = bbWidth . layoutTreeBB
treeLayoutWiden :: TreeLayout e -> Double -> TreeLayout e
treeLayoutWiden tlo minWidth =
    let w = treeLayoutWidth tlo
    in
      if w >= minWidth
      then tlo
      else let dw = minWidth  w
               T.Node root subs = translate (dw / 2) 0 tlo
               LayoutNode rootGNode treeBB = root
               root' = LayoutNode rootGNode 
                                  (translate (dw / 2) 
                                             0 
                                             (widen treeBB minWidth))
           in T.Node root' subs