-- | Automatically stretching\/growing (\*when possible) delimiters.
--
-- No need to import this module directly.
--
-- NOTE: these stretching delimiters are composed from overlayed unicode glyphs.
-- They are designed to work with the STIX fonts, other fonts may produce
-- unacceptable results. Transparent rendering is broken too (though that could
-- be fixed in principle).
--
-- TODO: 
-- 
-- * growing ceil\/floor
--
-- * \"compound\" brackets like @[[@, @{|@, etc
--
-- * fix subscripts and superscripts of big delimited things
--
-- * cleaner implementation
--
-- * maybe hardwire the required glyphs from STIX so that it works with any font?
--
-- * fix transparency
--


{-# LANGUAGE BangPatterns, Rank2Types #-}
module Graphics.Rendering.MiniTypeset.Delimiters where

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

import Data.Ord
import Data.Char hiding ( Space )
import Data.List

import Control.Monad

import Graphics.Rendering.MiniTypeset.Common
import Graphics.Rendering.MiniTypeset.Document
import Graphics.Rendering.MiniTypeset.Box

{-
-- used when generating code
import Text.Printf
import qualified Data.Map as Map ; import Data.Map (Map)
import Graphics.Rendering.MiniTypeset.Layout
import Graphics.Rendering.MiniTypeset.MultiFont
-}

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

delimHangAbove =  0.125  :: Double
delimHangBelow = -0.075  :: Double
delimHMargin   =  0.075  :: Double
delimVMargin   =  0.075  :: Double

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

-- | A compound delimiter is built from overlayed Unicode glyphs.
-- It can be either fixed (a single glyph; two glyphs next to each other), or adjustable
-- (glyphs with adjustable overlap)

data CompoundDelim
  = FixDelim   (forall a.           Document a)      -- ^ something whose size is fixed
  | Adjustable (forall a. Double -> Document a)      -- ^ something whose size can be adjusted

sizedCompoundDelim :: CompoundDelim -> Double -> Document a
sizedCompoundDelim cd x = case cd of
  FixDelim   d -> Realign BoundingQuad $ Trim $ UnsafeResize (1,x) d
  Adjustable f -> Realign BoundingQuad $ Trim $ f x

--------------------------------------------------------------------------------
-- * Utility

rescale :: (Double,Double) -> Double -> Double
rescale (!a,!b) !x
  | x < a     = 0
  | x < b     = (x-a)/(b-a)
  | otherwise = 1

rescaleTo :: (Double,Double) -> (Double,Double) -> Double -> Double
rescaleTo (a,b) (c,d) !x = c + (d-c) * rescale (a,b) x

fdivmod :: Double -> Double -> (Int,Double)
fdivmod a b = (n,x) where
  n = floor (a/b)
  x = (a - fromIntegral n * b) / b

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

symmNegMargin :: Double -> Document a -> Document a
symmNegMargin y = AddMargin (Margin 0 0 (-y) (-y))

topNegMargin :: Double -> Document a -> Document a
topNegMargin y = AddMargin (Margin 0 0 (-y) 0)

botNegMargin :: Double -> Document a -> Document a
botNegMargin y = AddMargin (Margin 0 0 0 (-y))

--------------------------------------------------------------------------------
-- * Growing delimiters

type DelimGrower a = Height -> WhichDelim -> Double -> Document a

data DelimImpl
  = GrowingDelim (forall a. DelimGrower a)
  | FixedDelim   !(Char,Char)
--  | NotImplDelim

delimiterImpl :: Delimiter -> DelimImpl
delimiterImpl delim = case delim of
  Paren      -> GrowingDelim  growParen
  Brace      -> GrowingDelim  growBrace
  Square     -> GrowingDelim  growBracket
  VertSingle -> GrowingDelim  growVertical
  VertDouble -> GrowingDelim  growDblVertical
  otherwise  -> FixedDelim   (delimiterChars delim)

finalDelim :: Height -> Delimiter -> Double -> WhichDelim -> Document a
finalDelim = finalDelim' 0

finalDelim' :: Double -> Height -> Delimiter -> Double -> WhichDelim -> Document a
finalDelim' yofs height@(Height h) delim target_in_pixels which = final where
  final = RePosition (Pos 0 vofs) $ AddMargin (Margin mh mh mv mv) $ trimmed
  vofs  = yofs {- - fh * delimHangBelow -}     -- align on bottom!
  mh  = fh * delimHMargin
  mv  = fh * delimVMargin
  trimmed = case delimiterImpl delim of
    GrowingDelim grow    -> grow height which target
    FixedDelim (lch,rch) -> UnsafeResize (1, min 3 target) $
                            Realign BoundingQuad $
                            Trim $ Symbol $
                            case which of { LeftDelim  -> lch ; RightDelim -> rch }
  fh = fromIntegral h :: Double
  extra  = fh * (delimHangAbove - delimHangBelow)
  target = (target_in_pixels + extra) / fh

--------------------------------------------------------------------------------
-- * Figuring out how to build a delimiter of the target size 

-- | Note: target height is relative to the font height, but not including line gap!
-- You have to take line gap into account yourself if necessary.
growBrace :: Height -> WhichDelim -> Double -> Document a
growBrace height which target
  | y < 1.425  = sizedCompoundDelim (buildBrace height 0 which) (rescaleTo (0.648,2.758) (0.700,3.000) y)
  | y < 3.204  = sizedCompoundDelim (buildBrace height 1 which) (rescaleTo (1.425,6.136) (0.700,3.000) y)
  | y < 4.504  = sizedCompoundDelim (buildBrace height 2 which) (rescaleTo (3.204,4.464) (0.000,1.000) y)
  | y < 6.424  = sizedCompoundDelim (buildBrace height 3 which) (rescaleTo (4.504,6.424) (0.000,1.000) y)
  | y < 8.344  = sizedCompoundDelim (buildBrace height 4 which) (rescaleTo (6.424,8.344) (0.000,1.000) y)
  | y < 10.264  = sizedCompoundDelim (buildBrace height 5 which) (rescaleTo (8.344,10.264) (0.000,1.000) y)
  | y < 12.184  = sizedCompoundDelim (buildBrace height 6 which) (rescaleTo (10.264,12.184) (0.000,1.000) y)
  | otherwise = let (n,x) = fdivmod (y - 12.184) 1.920
                    fn = fromIntegral n
                in  sizedCompoundDelim (buildBrace height (n+7) which) (rescaleTo (0.000,1.000) (0.000,1.000) x)
  where
    y = target + (-0.078)

growParen :: Height -> WhichDelim -> Double -> Document a
growParen height which target
  | y < 2.047  = sizedCompoundDelim (buildParen height 0 which) (rescaleTo (0.645,2.750) (0.700,3.000) y)
  | y < 1.761  = sizedCompoundDelim (buildParen height 1 which) (rescaleTo (2.047,2.527) (0.000,1.000) y)
  | y < 2.701  = sizedCompoundDelim (buildParen height 2 which) (rescaleTo (1.761,7.613) (0.700,3.000) y)
  | y < 3.695  = sizedCompoundDelim (buildParen height 3 which) (rescaleTo (2.701,3.761) (0.000,1.000) y)
  | y < 4.909  = sizedCompoundDelim (buildParen height 4 which) (rescaleTo (3.695,4.975) (0.000,1.000) y)
  | y < 6.122  = sizedCompoundDelim (buildParen height 5 which) (rescaleTo (4.909,6.189) (0.000,1.000) y)
  | y < 7.336  = sizedCompoundDelim (buildParen height 6 which) (rescaleTo (6.122,7.403) (0.000,1.000) y)
  | otherwise = let (n,x) = fdivmod (y - 7.336) 1.214
                    fn = fromIntegral n
                in  sizedCompoundDelim (buildParen height (n+7) which) (rescaleTo (0.000,1.054) (0.000,1.000) x)
  where
    y = target + (-0.082)

growBracket :: Height -> WhichDelim -> Double -> Document a
growBracket height which target
  | y < 2.055  = sizedCompoundDelim (buildBracket height 0 which) (rescaleTo (0.648,2.758) (0.700,3.000) y)
  | y < 2.555  = sizedCompoundDelim (buildBracket height 1 which) (rescaleTo (2.055,2.535) (0.000,1.000) y)
  | y < 3.235  = sizedCompoundDelim (buildBracket height 2 which) (rescaleTo (2.555,3.515) (0.000,1.000) y)
  | y < 4.315  = sizedCompoundDelim (buildBracket height 3 which) (rescaleTo (3.235,4.475) (0.000,1.000) y)
  | y < 5.275  = sizedCompoundDelim (buildBracket height 4 which) (rescaleTo (4.315,5.435) (0.000,1.000) y)
  | y < 6.235  = sizedCompoundDelim (buildBracket height 5 which) (rescaleTo (5.275,6.395) (0.000,1.000) y)
  | y < 7.195  = sizedCompoundDelim (buildBracket height 6 which) (rescaleTo (6.235,7.355) (0.000,1.000) y)
  | otherwise = let (n,x) = fdivmod (y - 7.195) 0.960
                    fn = fromIntegral n
                in  sizedCompoundDelim (buildBracket height (n+7) which) (rescaleTo (0.000,1.167) (0.000,1.000) x)
  where
    y = target + (-0.078)

growVertical :: Height -> WhichDelim -> Double -> Document a
growVertical height which target
  | y < 1.455  = sizedCompoundDelim (buildVertical height 0 which) (rescaleTo (0.633,2.703) (0.700,3.000) y)
  | y < 1.929  = sizedCompoundDelim (buildVertical height 1 which) (rescaleTo (1.455,1.785) (0.000,1.000) y)
  | y < 2.792  = sizedCompoundDelim (buildVertical height 2 which) (rescaleTo (1.929,3.529) (0.000,1.000) y)
  | y < 3.654  = sizedCompoundDelim (buildVertical height 3 which) (rescaleTo (2.792,4.392) (0.000,1.000) y)
  | y < 4.516  = sizedCompoundDelim (buildVertical height 4 which) (rescaleTo (3.654,5.254) (0.000,1.000) y)
  | y < 5.379  = sizedCompoundDelim (buildVertical height 5 which) (rescaleTo (4.516,6.116) (0.000,1.000) y)
  | y < 6.241  = sizedCompoundDelim (buildVertical height 6 which) (rescaleTo (5.379,6.979) (0.000,1.000) y)
  | otherwise = let (n,x) = fdivmod (y - 6.241) 0.862
                    fn = fromIntegral n
                in  sizedCompoundDelim (buildVertical height (n+7) which) (rescaleTo (0.000,1.855) (0.000,1.000) x)
  where
    y = target + (-0.098)

growDblVertical :: Height -> WhichDelim -> Double -> Document a
growDblVertical height which target
  | y < 1.455  = sizedCompoundDelim (buildDblVertical height 0 which) (rescaleTo (0.633,2.703) (0.700,3.000) y)
  | y < 1.929  = sizedCompoundDelim (buildDblVertical height 1 which) (rescaleTo (1.455,1.785) (0.000,1.000) y)
  | y < 2.792  = sizedCompoundDelim (buildDblVertical height 2 which) (rescaleTo (1.929,3.529) (0.000,1.000) y)
  | y < 3.654  = sizedCompoundDelim (buildDblVertical height 3 which) (rescaleTo (2.792,4.392) (0.000,1.000) y)
  | y < 4.516  = sizedCompoundDelim (buildDblVertical height 4 which) (rescaleTo (3.654,5.254) (0.000,1.000) y)
  | y < 5.379  = sizedCompoundDelim (buildDblVertical height 5 which) (rescaleTo (4.516,6.116) (0.000,1.000) y)
  | y < 6.241  = sizedCompoundDelim (buildDblVertical height 6 which) (rescaleTo (5.379,6.979) (0.000,1.000) y)
  | otherwise = let (n,x) = fdivmod (y - 6.241) 0.862
                    fn = fromIntegral n
                in  sizedCompoundDelim (buildDblVertical height (n+7) which) (rescaleTo (0.000,1.855) (0.000,1.000) x)
  where
    y = target + (-0.098)

--------------------------------------------------------------------------------
-- * Delimiter builders

type CompoundLevel = Int

type DelimBuilder = Height -> CompoundLevel -> WhichDelim -> CompoundDelim

buildBrace :: DelimBuilder
buildBrace (Height h) n lr
  | n == 0     = FixDelim $ one
  | n == 1     = FixDelim $ vcat [ botNegMargin (0.01*fh) top2 , topNegMargin (0.01*fh) bot2 ]
  | n == 2     = Adjustable $ \x ->
                   let y = 0.02 + (1-x)*0.63
                   in  vcat [ top3 , symmNegMargin (y*fh) mid3 , bot3 ]
  | otherwise  = Adjustable $ \x ->
                   let y   = 0.02 + (1-x)*0.48
                       adj = symmNegMargin (y   *fh) ext3
                       fix = symmNegMargin (0.02*fh) ext3
                   in  vcat (top3 : replicate (n-3) fix ++ adj : mid3 : adj : replicate (n-3) fix ++ bot3 : [])
  where
    fh = fromIntegral h :: Double
    mk ch = Trim (char ch)

    one  = mk $ case lr of { LeftDelim -> '{'       ; RightDelim -> '}'       }

    top2 = mk $ case lr of { LeftDelim -> braceSection1 ; RightDelim -> braceSection2 }
    bot2 = mk $ case lr of { LeftDelim -> braceSection2 ; RightDelim -> braceSection1 }

    top3 = mk $ case lr of { LeftDelim -> lbraceTop ; RightDelim -> rbraceTop }
    mid3 = mk $ case lr of { LeftDelim -> lbraceMid ; RightDelim -> rbraceMid }
    bot3 = mk $ case lr of { LeftDelim -> lbraceBot ; RightDelim -> rbraceBot }
    ext3 = mk $ lrBraceExt

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

buildParen :: DelimBuilder
buildParen (Height h) n lr
  | n == 0     = FixDelim $ one
  | n == 1     = Adjustable $ \x -> let y = 0.02 + (1-x)*0.48 in vcat [ top , topNegMargin (y*fh) bot ]
  | n == 2     = FixDelim $ vcat [ top , topNegMargin (0.02*fh) bot ]   -- parens suck in this font
  | n == 3     = Adjustable $ \x ->
                   let y   = 0.02 + (1-x)*0.53
                       adj = symmNegMargin (y   *fh) ext
                   in  vcat (top : adj : bot : [])
  | otherwise  = Adjustable $ \x ->
                   let y   = 0.02 + (1-x)*0.32
                       adj = symmNegMargin (y   *fh) ext
                       fix = symmNegMargin (0.02*fh) ext
                   in  vcat (top : adj : replicate (n-4) fix ++ adj : bot : [])
  where
    fh = fromIntegral h :: Double
    mk ch = Trim (char ch)

    one = mk $ case lr of { LeftDelim -> '('       ; RightDelim -> ')'       }
    top = mk $ case lr of { LeftDelim -> lparenTop ; RightDelim -> rparenTop }
    ext = mk $ case lr of { LeftDelim -> lparenExt ; RightDelim -> rparenExt }
    bot = mk $ case lr of { LeftDelim -> lparenBot ; RightDelim -> rparenBot }

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

buildBracket :: DelimBuilder
buildBracket (Height h) n lr
  | n == 0     = FixDelim $ one
  | n == 1     = Adjustable $ \x -> let y = 0.02 + (1-x)*0.48 in vcat [ top , topNegMargin (y*fh) bot ]
  | n == 2     = Adjustable $ \x ->
                   let y   = 0.02 + (1-x)*0.53
                       adj = symmNegMargin (y   *fh) ext
                   in  vcat (top : adj : bot : [])
  | n == 3     = Adjustable $ \x ->
                   let y   = 0.02 + (1-x)*0.31
                       adj = symmNegMargin (y   *fh) ext
                   in  vcat (top : adj : adj : bot : [])
  | otherwise  = Adjustable $ \x ->
                   let y   = 0.02 + (1-x)*0.28
                       adj = symmNegMargin (y   *fh) ext
                       fix = symmNegMargin (0.02*fh) ext
                   in  vcat (top : adj : replicate (n-3) fix ++ adj : bot : [])
  where
    fh = fromIntegral h :: Double
    mk ch = Trim (char ch)

    one = mk $ case lr of { LeftDelim -> '['         ; RightDelim -> ']'         }
    top = mk $ case lr of { LeftDelim -> lbracketTop ; RightDelim -> rbracketTop }
    ext = mk $ case lr of { LeftDelim -> lbracketExt ; RightDelim -> rbracketExt }
    bot = mk $ case lr of { LeftDelim -> lbracketBot ; RightDelim -> rbracketBot }

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

buildVertical :: DelimBuilder
buildVertical (Height h) n _lr_
  | n == 0     = FixDelim $ one
  | n == 1     = Adjustable $ \x -> let y = 0.02 + (1-x)*0.33 in vcat [ one , topNegMargin (y*fh) one ]
  | otherwise  = Adjustable $ \x ->
                   let y   = 0.02 + (1-x)*0.40
                       adj = symmNegMargin (y   *fh) one
                       fix = symmNegMargin (0.02*fh) one
                   in  vcat (one : adj : replicate (n-2) fix ++ adj : one : [])
  where
    fh = fromIntegral h :: Double
    mk ch = Trim (char ch)

    one = mk '\x2223'

buildDblVertical :: DelimBuilder
buildDblVertical (Height h) n _lr_
  | n == 0     = FixDelim $ one
  | n == 1     = Adjustable $ \x -> let y = 0.02 + (1-x)*0.33 in vcat [ one , topNegMargin (y*fh) one ]
  | otherwise  = Adjustable $ \x ->
                   let y   = 0.02 + (1-x)*0.40
                       adj = symmNegMargin (y   *fh) one
                       fix = symmNegMargin (0.02*fh) one
                   in  vcat (one : adj : replicate (n-2) fix ++ adj : one : [])
  where
    fh = fromIntegral h :: Double
    mk ch = Trim (char ch)

    one = mk '\x2225'

--------------------------------------------------------------------------------
-- * Unicode codepoint constants

lparenTop = '\x239b'
lparenExt = '\x239c'
lparenBot = '\x239d'

rparenTop = '\x239e'
rparenExt = '\x239f'
rparenBot = '\x23a0'

lbracketTop = '\x23a1'
lbracketExt = '\x23a2'
lbracketBot = '\x23a3'

rbracketTop = '\x23a4'
rbracketExt = '\x23a5'
rbracketBot = '\x23a6'

lbraceTop = '\x23a7'
lbraceMid = '\x23a8'
lbraceBot = '\x23a9'

lrBraceExt = '\x23aa'

rbraceTop = '\x23ab'
rbraceMid = '\x23ac'
rbraceBot = '\x23ad'

intTop = '\x2320'
intExt = '\x23ae'
intBot = '\x2321'

braceSection1 = '\x23b0'
braceSection2 = '\x23b1'

--------------------------------------------------------------------------------
{- /// CODE GENERATION ///

measure :: Ord fontfile => MultiFont fontfile style -> Height -> Document String -> IO Double
measure mf (Height h) doc0 = do
  let doc = Identified ("measure" :: String) $ Trim doc0
  lout <- createLayout mf (Height h) doc
  usertable <- dryrunLayout lout (Pos 0 0)
  let AbsBox _ box = (Map.!) usertable "measure"
  let res = quadHeight $ boxBoundingQuad box
  return (res / fromIntegral h)

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

testDelimBuilder :: Ord fontfile => MultiFont fontfile style -> DelimBuilder -> IO ()
testDelimBuilder mf builder = do
  let h = Height 256
  forM_ [0..5] $ \n -> do
    putStrLn $ "level (n) = " ++ show n
    let cdelim = builder h n LeftDelim
    case cdelim of
      FixDelim mkdoc -> do
        putStr "Fixed      -> "
        list <- forM [0.75,0.875..1.5] $ \x -> measure mf h (Trim $ UnsafeResize (1,x) mkdoc)
        print list
      Adjustable mkdoc -> do
        putStr "Adjustable -> "
        list <- forM [0,0.1..1] $ \x -> measure mf h (mkdoc x)
        print list

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

-- | Generate code for automatically sizing growable delimiters
writeGrowingDelimFromMeasurement :: Ord fontfile => MultiFont fontfile style -> String -> DelimBuilder -> IO [String]
writeGrowingDelimFromMeasurement mf what builder = do
  let ht    = Height 256
      which = LeftDelim
  base <- case (builder ht 0 which) of { FixDelim doc -> measure mf ht doc }
  let ofs = base - 1
  let candi1 n = let cd = builder ht n LeftDelim in case cd of
        FixDelim     doc -> [ ((n,x) , Trim (UnsafeResize (1,x) doc) ) | x <- [0.7,0.75..3.0] ]
        Adjustable mkdoc -> [ ((n,x) , mkdoc x                       ) | x <- [0,0.025..1.0]  ]
  let candidates = concat [ candi1 n | n<-[0..7] ]
  hs <- forM candidates $ \((n,x),doc) -> measure mf ht doc
  let sorted = sortBy (comparing fst) $ zip hs candidates 
      groups = [ [ ((k,x),h) | (h,((k,x),doc)) <- sorted , k==n ] | n <- [0..7] ]
      smallest = map head groups
      largest  = map last groups
   
  let choices = 
        [ "  | y < " ++ printf "%.3f" h2 ++ 
          "  = sizedCompoundDelim (" ++ what ++ " height " ++ show k1a ++ " which) " ++ 
          (printf "(rescaleTo (%.3f,%.3f) (%.3f,%.3f) y)" h1a h1b x1a x1b)
        | ( ( ((k1a,x1a),h1a) , ((k2,x2),h2) ) , ((k1b,x1b),h1b) ) <- zip (pairs smallest) largest
        ] 

  let ( ( ((k1a,x1a),h1a) , ((k2a ,x2a ),h2a ) ) , ( ((k1b,x1b),h1b) , ((k2b,x2b),h2b) ) ) = last (zip (pairs smallest) (pairs largest))
      extrap = 
        [ "  | otherwise = let (n,x) = " ++ printf "fdivmod (y - %.3f) %.3f" h2a (h2a-h1a) 
        , "                    fn = fromIntegral n "                
        , "                in  sizedCompoundDelim (" ++ what ++ " height " ++ printf "(n+%d)" (k1a+1) ++ " which) " ++ 
          (printf "(rescaleTo (%.3f,%.3f) (%.3f,%.3f) x)" ((h1a-h1a)/(h2a-h1a)) ((h1b-h1a)/(h2a-h1a)) x1a x1b)
        ]

  let ls =
        [ "grow :: Height -> WhichDelim -> Double -> Document a"
        , "grow height which target" 
        ] 
        ++ choices 
        ++ extrap ++ 
        [ "  where "
        , "    y = target + " ++ printf "(%.3f)" ofs 
        ]
  putStrLn $ unlines ls
  return ls


pairs :: [a] -> [(a,a)]
pairs (x:xs@(y:_)) = (x,y) : pairs xs
pairs [x] = []
pairs []  = []

-}
--------------------------------------------------------------------------------