{-# LANGUAGE ImplicitParams, OverloadedStrings, AllowAmbiguousTypes, GADTs, CPP, ExistentialQuantification, TypeSynonymInstances, FlexibleInstances, MultiParamTypeClasses, FlexibleContexts, ScopedTypeVariables, UndecidableInstances #-}
module Graphics.UI.FLTK.Theme.Light.Common
  (
    -- * Theme Loading
    configureTheme,
    -- * Common Colors & Fonts
    commonColor,
    commonDarkGreyColor,
    commonFillColor,
    commonFont,
    commonFontSize,
    commonLargeFontSize,
    commonSelectionColor,
    lightBackground,
    -- * Common Calculations For Drawing Widgets
    angleToCoordinate,
    centerInRectangle,
    centerInRectangleByRelativePosition,
    degreesPerRadian,
    fromFltkAngle,
    insideRectangle,
    intDiv,
    isWidget,
    percentOf,
    percentOfSmallerEllipseRadius,
    positionInside,
    splitVertically,
    splitVerticallyByPixels,
    splitHorizontally,
    splitHorizontallyByPixels,
    withPositionX,
    withPositionY,
    withSizeHeight,
    withSizeWidth,
    -- * Common Widget Drawing Helpers
    withCustomBoxDraw,
    BorderBoxSpec(..),
    drawBorderBox,
    FillSpec(..),
    makeFillSpec,
    borderRectangle,
    fillRectangle,
    handleHover,
    OpenBorder(..),
    roundedBoxPoints
  )
where
import Control.Monad
import Graphics.UI.FLTK.LowLevel.Dispatch
import Graphics.UI.FLTK.LowLevel.FLTKHS hiding (colorAverage, isHorizontal, inactive, tabPositionsCustom, tabHeightCustom, tabDrawCustom, find, tabWhichCustom, tabRedrawTabs)
import Graphics.UI.FLTK.LowLevel.Fl_Enumerations
import Graphics.UI.FLTK.Theme.Light.Assets
import qualified Graphics.UI.FLTK.LowLevel.FL as FL

-- | For drawing a rectangular widget that is borderless on the top or bottom. Guess
-- it could be passed into a function as yet another Bool but we have too many of those
-- as it is.
data OpenBorder =
    OpenBorderTop    -- ^ Signifies a bordered rectangle with the top side. Useful, for example, for a bottom tab
  | OpenBorderBottom -- ^ Signifies a bordered rectangle without a bottom side. Useful, for example, for a top tab

-- | Specifies how to draw a border around a 'Rectangle'.
data BorderBoxSpec =
  BorderBoxSpec
  {
    borderBoxBounds :: Rectangle, -- ^ The bounds of the rectangle to border
    borderBoxFocusedColor :: Color, -- ^ The color of the border if the widget is focused
    borderBoxHoveringColor :: Color, -- ^ The color of the border if the mouse pointer is over the widget
    borderBoxColor :: Color, -- ^ The default color of the border
    borderBoxFillColor :: Color -- ^ The fill color of the rectangle. Even though 'FillSpec' exists this is useful if you want a simple solid color and don't care about gradients.
  } deriving Show


-- | Specifies how a 'Rectangle' should be filled. Allows for a vertical color gradient.
data FillSpec =
  FillSpec
  {
    fillBounds :: Rectangle -- ^ The bounds of the rectangle to be filled
  , fillTopColor :: Color  -- ^ The color at the top of the rectangle
  , fillTopSelectionColor :: Color -- ^ The color at the top of the rectangle if this widget is selected (mostly of use to buttons)
  , fillTopFillPercentage :: Int  -- ^ The % of the rectangle at that top that is filled with 'fillTopColor' before the gradient begins
  , fillBottomColor :: Color -- ^ The color at the bottom of the rectangle
  , fillBottomSelectionColor :: Color -- ^ The color at the bottom of the rectangle if this widget is selected (mostly of use to buttons)
  , fillBottomFillPercentage :: Int -- ^ The % of the rectangle at the bottom that is filled with 'fillBottomColor' after the gradient ends
  , fillCornerRadius :: Int -- ^ The roundedness of the corners
  , fillBorderColor :: Color -- ^ The border color of this rectangle
  , fillBorderFocusColor :: Color -- ^ The border color of this rectangle if it is focused
  , fillBorderHovered :: Color -- ^ The border color of this rectangle if the mouse is over it
  }

commonFont :: (?assets :: Assets) => Font
commonFont = dejaVuSans

commonFontSize :: FontSize
commonFontSize = FontSize 12

commonLargeFontSize :: FontSize
commonLargeFontSize = FontSize 22

commonColor :: IO Color
commonColor = rgbColorWithRgb (0xF9, 0xF9, 0xF9)

commonDarkGreyColor :: IO Color
commonDarkGreyColor = rgbColorWithRgb (80,81,84)

commonFillColor :: IO Color
commonFillColor = rgbColorWithRgb (0x66, 0x94, 0xE3)

lightBackground :: Color
lightBackground = whiteColor

-- | Converts from an FLTK angle to a unit circle's. FLTK starts its angles counter-clockwise
-- from 270 so, for example, 45 degrees in FLTK is 270-45 = 225 degrees on a unit circle.
-- Angle unit is in degrees.
fromFltkAngle :: PreciseAngle -> PreciseAngle
fromFltkAngle (PreciseAngle a) =
  let unitCircleAngle = (270 - (truncate a)) `mod` 360
  in
    PreciseAngle
      (if (unitCircleAngle < 0)
       then 360.0 - (abs (fromIntegral unitCircleAngle))
       else (fromIntegral unitCircleAngle))

-- | pi/180
degreesPerRadian :: Double
degreesPerRadian = pi/180

-- | Given a 'PreciseAngle' /in degrees/ return the corresponding location on the unit circle
angleToCoordinate :: PreciseAngle -> PrecisePosition
angleToCoordinate (PreciseAngle a) =
  case a of
     _ | a == 360 -> PrecisePosition (PreciseX (cos 0)) (PreciseY (sin 0))
       | otherwise ->
           let radians = a * degreesPerRadian
           in PrecisePosition (PreciseX (cos radians)) (PreciseY (sin radians))

-- | Straightfoward % of the given 'Int', the 'Double' % must be between 0.0 and 1.0
percentOf :: Double -> Int -> Int
percentOf p a = truncate ((fromIntegral a) * p)

-- | Given the width and height of an ellipse via 'PreciseSize', return
-- the 'Double' % of the smaller radius. % must be between 0.0 and 1.0.
-- Used in 'Dial's and 'Clock's to measure the length of hands.
percentOfSmallerEllipseRadius :: Double -> PreciseSize -> Double
percentOfSmallerEllipseRadius percent (PreciseSize (PreciseWidth rW) (PreciseHeight rH)) = ((if (rW < rH) then rW else rH) * percent) / 100.0

commonSelectionColor :: IO Color
commonSelectionColor = rgbColorWithRgb (50, 100, 201)

-- | Check if the given references point to the same 'Widget'.
isWidget :: (Parent a Widget) => Ref a -> IO (Maybe (Ref b)) -> IO Bool
isWidget this thatM = thatM >>= maybe (return False) (refPtrEquals this)

-- | Check if the given 'Position' is inside the 'Rectangle'. Useful for
-- checking if an event happened within some bounds.
insideRectangle :: Position -> Rectangle -> Bool
insideRectangle (Position (X x) (Y y)) rect =
  let (rx,ry,rw,rh) = fromRectangle rect
  in x >= rx && x <= rx+rw && y >= ry && y <= ry+rh

-- | Return a set of vertices that circumscribe a region bounded by 'Rectangle'.
-- The 'Maybe' 'Int' when provided means the rectangle has rounded corners of
-- 'Int' radius. In a rounded rectangle the vertices don't meet stopping short
-- of the corners, the client is responsible for filling in the arcs.
--
-- 'Maybe' 'OpenBorder' determines whether the rectangle is whole (if not
-- provided) or has an open top or bottom.
roundedBoxPoints :: Rectangle -> Maybe Int -> Maybe OpenBorder -> [Position]
roundedBoxPoints (Rectangle (Position (X x) (Y y)) (Size (Width w) (Height h))) maybeRadius maybeOpen =
  let insideW = w-1
      insideH = h-1
  in
    map toPosition
      (case (maybeRadius,maybeOpen) of
         (Just radius, Nothing) ->
           [
             (x+radius,y)
           , (x+insideW-radius,y)
           , (x+insideW,y+radius)
           , (x+insideW,y+insideH-radius)
           , (x+insideW-radius,y+insideH)
           , (x+radius,y+insideH)
           , (x,y+insideH-radius)
           , (x,y+radius)
           , (x+radius,y)
           ]
         (Just radius, Just OpenBorderBottom) ->
           [
             (x,y+h)
           , (x,y+radius)
           , (x+radius,y)
           , (x+insideW-radius,y)
           , (x+insideW,y+radius)
           , (x+insideW,y+h)
           ]
         (Just radius, Just OpenBorderTop) ->
           [
             (x+insideW,y)
           , (x+insideW,y+insideH-radius)
           , (x+insideW-radius,y+insideH)
           , (x+radius,y+insideH)
           , (x,y+insideH-radius)
           , (x,y)
           ]
         (Nothing, Just OpenBorderBottom) ->
           [
             (x,y+insideH)
           , (x,y)
           , (x+insideW,y)
           , (x+insideW,y+insideH)
           ]
         (Nothing, Just OpenBorderTop) ->
           [
             (x,y)
           , (x,y+insideH)
           , (x+insideW,y+insideH)
           , (x+insideW,y)
           ]
         (Nothing, Nothing) ->
           [
             (x,y)
           , (x+insideW,y)
           , (x+insideW,y+insideH)
           , (x,y+insideH)
           , (x,y)
           ])

-- | Draw a bordered rectangle for the given widget according to 'BorderBoxSpec', the 'Bool'
-- determines if it should be filled with 'borderBoxFillColor'.
drawBorderBox :: (Parent a Widget) => Ref a -> BorderBoxSpec -> Bool -> IO ()
drawBorderBox w spec shouldFill = do
  oldColor <- flcColor
  focused <- isWidget w FL.focus
  hovering <- isWidget w FL.belowmouse
  when shouldFill (flcRectfWithColor (borderBoxBounds spec) (borderBoxFillColor spec))
  if focused
    then flcSetColor (borderBoxFocusedColor spec)
    else if hovering
         then flcSetColor (borderBoxHoveringColor spec)
         else flcSetColor (borderBoxColor spec)
  flcBeginLine
  mapM_ (flcVertex . toPrecisePosition) (roundedBoxPoints (borderBoxBounds spec) Nothing Nothing)
  flcEndLine
  flcSetColor oldColor

-- | The default FLTK widgets don't react to a mouse pointer hovering over them.
-- This handler when applied to a customized widget initiates a 'redraw' when the
-- mouse enters and leaves the widget area.
handleHover ::
  (
    Parent orig Widget,
    Match x ~ FindOp orig orig (Redraw ()),
    Op (Redraw ()) x orig (IO ()),
    Match y ~ FindOp orig orig (HandleSuper ()),
    Op (HandleSuper ()) y orig (Event -> IO (Either UnknownEvent ()))
  )
  => Ref orig -> Event -> IO (Either UnknownEvent ())
handleHover b e = do
  case e of
    Enter -> do
      () <- redraw b
      return (Right())
    Leave -> do
      () <- redraw b
      return (Right ())
    _ -> handleSuper b e

-- | Temporarily swap out the FLTK's box drawing function for a given 'Boxtype'
-- with 'BoxDrawF'. The 'IO' '()' action will typically use the custom function
-- that was just swapped in some kind custom drawing routine. For example, if a
-- custom widget used a 'BorderBox' on the FLTK side but you don't like the
-- default 'BorderBox' look, you can override the 'draw' function of the widget
-- with this one where the 'IO' '()' action calls 'drawSuper'. For an example
-- use-case see 'Graphics.UI.FLTK.Theme.Light.Input.inputDraw'.
--
-- NOTE: The 'IO' '()' action is /not/ exception safe.
withCustomBoxDraw :: Boxtype -> BoxDrawF -> IO () -> IO ()
withCustomBoxDraw boxtype customBoxDrawF action = do
  fptr <- FL.getBoxtypePrim boxtype
  dx <- FL.boxDx boxtype
  dy <- FL.boxDy boxtype
  dw <- FL.boxDw boxtype
  dh <- FL.boxDh boxtype
  FL.setBoxtype boxtype (FL.FromSpec customBoxDrawF 0 0 0 0)
  action
  FL.setBoxtype boxtype (FL.FromFunPtr fptr (fromIntegral dx) (fromIntegral dy) (fromIntegral dw) (fromIntegral dh))

-- | Make the default 'FillSpec' used by most of the theme color graded with the initial 'Color'. The second 'Color'
-- is used to determine the border color.
makeFillSpec :: Rectangle -> Color -> Color -> IO FillSpec
makeFillSpec rect color selectionColor = do
  (colorR, colorG, colorB) <- FL.getColorRgb color
  pressedColor <- rgbColorWithRgb (colorR - 21, colorG - 21, colorB - 21)
  let slightlyDarker = colorAverage color blackColor 0.93
  let slightlyLighter = colorAverage whiteColor pressedColor 0.93
  hoverColor <- rgbColorWithRgb (0xBB, 0xBB, 0xBB)
  let darkerSelectionColor = darker selectionColor
  return
    FillSpec
      {
        fillBounds = rect,
        fillTopColor = color,
        fillTopSelectionColor = pressedColor,
        fillTopFillPercentage = 30,
        fillBottomColor = slightlyDarker,
        fillBottomSelectionColor = slightlyLighter,
        fillBottomFillPercentage = 70,
        fillCornerRadius = 2,
        fillBorderColor = slightlyDarker,
        fillBorderFocusColor = darkerSelectionColor,
        fillBorderHovered = hoverColor
      }

-- | Draw the border around the 'Rectangle' specified by 'FillSpec' possibly changing the border color
-- if the widget (usually a Button) is currently focused or if the mouse pointer is hovering over it.
borderRectangle :: FillSpec -> Bool -> Bool -> IO ()
borderRectangle spec focused hovering = do
  oldColor <- flcColor
  if focused
    then flcSetColor (fillBorderFocusColor spec)
    else if hovering
         then flcSetColor (fillBorderHovered spec)
         else flcSetColor (fillBorderColor spec)
  flcBeginLine
  mapM_ (flcVertex . toPrecisePosition) (roundedBoxPoints (fillBounds spec) (Just (fillCornerRadius spec)) Nothing)
  flcEndLine
  flcLineStyle (LineDrawStyle (Just LineStyleSolid) Nothing Nothing) Nothing Nothing
  flcSetColor oldColor

-- | Draw a box according to the 'FillSpec'. It was originally intended for custom "Graphics.UI.FLTK.Theme.Light.Button"
-- which is why it takes a 'Bool' specifies if the button is pressed but is used widely in the theme for any rounded
-- rectangular area.
fillRectangle :: FillSpec -> Bool -> IO ()
fillRectangle spec pressed = do
  if pressed
    then drawButton (fillTopSelectionColor spec) (fillBottomSelectionColor spec)
    else drawButton (fillTopColor spec) (fillBottomColor spec)
  where
    drawButton topColor bottomColor = do
      let diameter = (fillCornerRadius spec) * 2
          quarterCircle x y a1 a2 = do
            fillColor <- flcColor
            flcPie (toRectangle (x,y,diameter,diameter)) a1 a2
            flcSetColor fillColor
      oldColor <- flcColor
      -- top left/right
      let (x,y,w,h) = fromRectangle (fillBounds spec)
          insideW = w
          insideH = h
          topFillFraction :: Double
          topFillFraction = (fromIntegral (fillTopFillPercentage spec)) / 100.0
          topHeight = truncate (fromIntegral insideH * topFillFraction)
      flcSetColor topColor
      flcRectf (toRectangle (x + (fillCornerRadius spec), y, insideW - diameter, (fillCornerRadius spec)))
      flcRectf (toRectangle (x, y + (fillCornerRadius spec), insideW , topHeight - (fillCornerRadius spec)))
      -- bottom left/right
      let bottomFillFraction :: Double
          bottomFillFraction = (fromIntegral (fillBottomFillPercentage spec)) / 100.0
          bottomHeight = truncate (fromIntegral insideH * bottomFillFraction)
      flcSetColor bottomColor
      when (diameter > 0)
        (do
           quarterCircle x y (PreciseAngle 90.0) (PreciseAngle 180.0)
           quarterCircle (x + (insideW - diameter)) y (PreciseAngle 0) (PreciseAngle 90.0)
           quarterCircle x (y + (insideH - diameter)) (PreciseAngle 180.0) (PreciseAngle 270.0)
           quarterCircle (x + (insideW - diameter)) (y + (insideH - diameter)) (PreciseAngle 270.0) (PreciseAngle 360.0)
           flcRectf (toRectangle (x + (fillCornerRadius spec), y + h - (fillCornerRadius spec), insideW - (diameter), (fillCornerRadius spec))))
      flcRectf (toRectangle (x, y + bottomHeight, insideW, h - bottomHeight - (fillCornerRadius spec)))
      -- middle gradient
      let middleFraction = fromIntegral (fillBottomFillPercentage spec - fillTopFillPercentage spec) / 100.0
          totalSteps :: Double
          totalSteps = fromIntegral h * middleFraction
          stepSize :: Double
          stepSize = 1.0 / totalSteps
      mapM_
        (
          \step -> do
            let weight = 1.0 - (stepSize * fromIntegral step)
                blendColor =
                  colorAverage
                    topColor
                    bottomColor
                    weight
            flcSetColor blendColor
            flcXyline (toPosition (x,y + topHeight + step)) (X (x + insideW - 1))
        )
        [0 .. truncate totalSteps - 1]
      flcSetColor oldColor

-- | Return the 'Position' which centers the given 'Size' in 'Rectangle'.
centerInRectangle :: Rectangle -> Size -> Position
centerInRectangle r (Size (Width sW) (Height sH)) =
  let (rX,rY,rW,rH) = fromRectangle r
      posX = rX + ((rW-sW) `intDiv` 2)
      posY = rY + ((rH-sH) `intDiv` 2)
  in toPosition (posX,posY)

-- | Return the 'Rectangle' that is centered in the given 'Rectangle' by the
-- 'X' and 'Y' /offset/ , so for example, (X 20) (Y 20) returns
-- a 'Rectangle' with a top left coordinate which is 20 pixels over and 20
-- pixels below the top left of the given 'Rectangle'.
centerInRectangleByRelativePosition :: Rectangle -> X -> Y -> Rectangle
centerInRectangleByRelativePosition r (X xOff) (Y yOff) =
  let (rX,rY,rW,rH) = fromRectangle r
      r2W = rW - (2*xOff)
      r2H = rH - (2*yOff)
  in toRectangle (rX+xOff,rY+yOff,r2W,r2H)

-- | Return a 'Rectangle' of 'Size' positioned at 'X' and 'Y' inside the given
-- 'Rectangle'. It's on the caller to make sure that the resulting 'Rectangle'
-- actually fits inside the one passed in. 'Size', 'X' and 'Y' are
-- not checked.
positionInside :: Rectangle -> Size -> X -> Y -> Rectangle
positionInside r (Size (Width sW) (Height sH)) (X xOff) (Y yOff) =
  let (rX,rY,rW,rH) = fromRectangle r
  in toRectangle (rX+xOff,rY+yOff,sW,sH)

-- | Transform the height.
withSizeHeight :: Size -> (Height -> Height) -> Size
withSizeHeight (Size w h) f = Size w (f h)

-- | Transform the width.
withSizeWidth :: Size -> (Width -> Width) -> Size
withSizeWidth (Size w h) f = Size (f w) h

-- | Transform the X coordinate of a location
withPositionX :: Position -> (X -> X) -> Position
withPositionX (Position x y) f = Position (f x) y

-- | Transform the Y coordinate of a location
withPositionY :: Position -> (Y -> Y) -> Position
withPositionY (Position x y) f = Position x (f y)

-- | Split the width of the 'Rectangle' into two side-by-side
-- ('Rectangle','Rectangle') by the 'Double' %. % must be between 0.0 and 1.0.
-- The width of the 2 returned rectangles will always sum to the width of the
-- one passed in.
splitVertically :: Rectangle -> Double -> (Rectangle, Rectangle)
splitVertically r percent =
  let (rx,ry,rw,rh) = fromRectangle r
      r1w = truncate ((fromIntegral rw) * percent)
      r2w = rw-r1w
      r2x = rx+r1w
  in (toRectangle (rx,ry,r1w,rh), toRectangle (r2x,ry,r2w,rh))

-- | Like 'splitVertically' but takes a 'Int' pixel width instead of a %. If the
-- width is greater than the 'Rectangle' the first of the pair will be the
-- 'Rectangle' passed in and the second will have the same dimensions but a 0
-- width.
splitVerticallyByPixels :: Rectangle -> Int -> (Rectangle, Rectangle)
splitVerticallyByPixels r ps =
  let (rx,ry,rw,rh) = fromRectangle r
      r1w = if (ps >= rw) then rw else ps
      r2w = rw-r1w
      r2x = rx+r1w
  in (toRectangle (rx,ry,r1w,rh), toRectangle (r2x,ry,r2w,rh))

-- | Split the height of the 'Rectangle' into two stacked
-- ('Rectangle','Rectangle') by the 'Double' %. % must be between 0.0 and 1.0.
-- The height of the 2 returned rectangles will always sum to the height of the
-- one passed in.
splitHorizontally :: Rectangle -> Double -> (Rectangle, Rectangle)
splitHorizontally r percent =
  let (rx,ry,rw,rh) = fromRectangle r
      r1h = truncate ((fromIntegral rh) * percent)
      r2h = rh-r1h
      r2y = ry+r1h
  in (toRectangle (rx,ry,rw,r1h), toRectangle (rx,r2y,rw,r2h))

-- | Like 'splitHorizontally' but takes a 'Int' pixel height instead of a %. If the
-- height is greater than the 'Rectangle' the first of the pair will be the
-- 'Rectangle' passed in and the second will have the same dimensions but a 0
-- height.
splitHorizontallyByPixels :: Rectangle -> Int -> (Rectangle,Rectangle)
splitHorizontallyByPixels r ps =
  let (rx,ry,rw,rh) = fromRectangle r
      r1h = if (ps >= rh) then rh else ps
      r2h = rh-r1h
      r2y = ry+r1h
  in (toRectangle (rx,ry,rw,r1h), toRectangle (rx,r2y,rw,r2h))

-- | A lossy division of 'Int'/'Int'. The result is 'truncate'd but it's good
-- enough for widget dimensions which is the primary use-case.
intDiv :: Int -> Int -> Int
intDiv num denom = truncate (((fromIntegral num) / (fromIntegral denom)) :: Double)

-- | Needs to be called by every app to load the themes's resources and do some
-- minor setup See "Graphics.UI.FLTK.Theme.Light#GettingStarted" for more
-- information.
configureTheme :: IO Assets
configureTheme = do
  assets <- loadAssets
  let ?assets = assets
  FL.setColor background2Color lightBackground
  tooltipSetFont commonFont
  tooltipSetSize commonFontSize
  return assets