{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE LambdaCase #-}

module Imj.Graphics.UI.RectContainer
        (
          -- * RectContainer
            {- | 'RectContainer' represents a rectangular UI container. It
            contains the 'Size' of its /content/, and an upper left coordinate.

            Being 'Colorable', it can be wrapped in a 'Colored' to gain the notion of color. -}
          RectContainer(..)
        , getSideCentersAtDistance
          -- * Reexports
        , Colorable(..)
        ) where

import           Imj.Prelude

import           Data.List( mapAccumL, zip )
import           Control.Monad.IO.Class(MonadIO)
import           Control.Monad.Reader.Class(MonadReader)

import           Imj.Geo.Discrete
import           Imj.Graphics.Class.DiscreteColorableMorphing
import           Imj.Graphics.Render
import           Imj.Graphics.UI.RectContainer.MorphParallel4

{-|

@
r----------------------------+
| u--+                       |
| |//|                       |
| |//|                       |
| +--l                       |
|                            |
+----------------------------+

r = Terminal origin, at (0,0)
/ = RectContainer's content, of size (2,2)
u = RectContainer's upper left corner, at (2,1)
l = RectContainer's lower left corner, at (5,4)
@
-}
data RectContainer = RectContainer {
    _rectFrameContentSize :: !Size
    -- ^ /Content/ size.
  , _rectFrameUpperLeft :: !(Coords Pos)
    -- ^ Upper left corner.
} deriving(Eq, Show)

-- TODO notion "continuous closed path" to factor 'ranges' and 'renderRectFrameInterpolation' logics.

instance Colorable RectContainer where
  drawUsingColor = renderWhole
  {-# INLINABLE drawUsingColor #-}

-- | Smoothly transforms the 4 sides of the rectangle simultaneously, from their middle
-- to their extremities.
instance DiscreteDistance RectContainer where
  {-# INLINABLE distance #-}
  distance c@(RectContainer s  _)
           c'@(RectContainer s' _)
    | c == c'   = 1
    | otherwise = 1 + quot (1 + max (maxLength s) (maxLength s')) 2


instance DiscreteColorableMorphing RectContainer where
  {-# INLINABLE drawMorphingUsingColor #-}
  drawMorphingUsingColor from to frame color
    | frame <= 0         = drawUsingColor from color
    | frame >= lastFrame = drawUsingColor to color
    | otherwise          = renderRectFrameInterpolation from to lastFrame frame color
    where
      lastFrame = pred $ distance from to

{-# INLINABLE renderWhole #-}
renderWhole :: (Draw e, MonadReader e m, MonadIO m)
            => RectContainer
            -> LayeredColor
            -> m ()
renderWhole (RectContainer sz upperLeft) =
  renderPartialRectContainer sz (upperLeft, 0, countRectContainerChars sz - 1)

{-# INLINABLE renderRectFrameInterpolation #-}
renderRectFrameInterpolation :: (Draw e, MonadReader e m, MonadIO m)
                             => RectContainer
                             -> RectContainer
                             -> Int
                             -> Int
                             -> LayeredColor
                             -> m ()
renderRectFrameInterpolation rf1@(RectContainer sz1 upperLeft1)
                 rf2@(RectContainer sz2 upperLeft2) lastFrame frame color = do
  let (Coords _ (Coord dc)) = diffCoords upperLeft1 upperLeft2
      render di1 di2 = do
        let fromRanges = ranges (lastFrame-(frame+di1)) sz1 FromBs
            toRanges   = ranges (lastFrame-(frame+di2)) sz2 FromAs
        mapM_ (renderRectFrameRange rf1 color) fromRanges
        mapM_ (renderRectFrameRange rf2 color) toRanges
  if dc >= 0
    then
      -- expanding animation
      render dc 0
    else
      -- shrinking animation
      render 0 (negate dc)


{-# INLINABLE renderRectFrameRange #-}
renderRectFrameRange :: (Draw e, MonadReader e m, MonadIO m)
                     => RectContainer
                     -> LayeredColor
                     -> (Int, Int)
                     -> m ()
renderRectFrameRange (RectContainer sz r) color (min_, max_) =
  renderPartialRectContainer sz (r, min_, max_) color

-- | Considering a closed continuous path with an even number of points labeled
--  A and B and alternating along the path : A,B,A,B,A,B
--
-- (Think of a rectangle, the middles of the sides being
-- the A points, the extremities being the B points)
--
--
-- FromBs is the complement, i.e the same as above, but replacing As with Bs and vice-versa.
data BuildFrom = FromAs
               -- ^ First draw A points, then expand the drawn regions
               -- to the right and left of A points, until B points are reached.
               | FromBs
               -- ^ First draw B points, then expand the drawn regions
               -- to the right and left of B points, until A points are reached.

ranges :: Int
       -- ^ Progress of the interpolation
       -> Size
       -- ^ Size of the content, /not/ the container
       -> BuildFrom
       -- ^ The building strategy
       -> [(Int, Int)]
ranges progress sz =
  let h = countRectContainerVerticalChars sz
      w = countRectContainerHorizontalChars sz

      diff = quot (w - h) 2 -- vertical and horizontal animations should start at the same time

      extW = rangeByRemovingFromTotal progress w
      extH = rangeByRemovingFromTotal (max 0 $ progress-diff) h

      exts = [extW, extH, extW, extH]
      lengths = [w,h,w,h]

      (total, starts) = mapAccumL (\acc v -> (acc + v, acc)) 0 lengths
      res = map (\(ext, s) -> ext s) $ zip exts starts
  in \case
        FromAs -> res
        FromBs -> complement 0 (total-1) res

complement :: Int -> Int -> [(Int, Int)] -> [(Int, Int)]
complement a max_ []          = [(a, max_)]
complement a max_ l@((b,c):_) = (a, pred b) : complement (succ c) max_ (tail l)

rangeByRemovingFromTotal :: Int -> Int -> Int -> (Int, Int)
rangeByRemovingFromTotal remove total start =
  let min_ = remove
      max_ = total - 1 - remove
  in (start + min_, start + max_)


-- TODO split : function to make the container at a distance, and function to take the centers.
{- | Returns points centered on the sides of a container which is at a given distances
(dx and dy) from the reference container.

[container at a distance from another container]
In this illustration, @cont'@ is at dx = dy = 3 from @cont@:

@
    cont'
    +--------+..-
    |        |  |  dy = 3
    |  cont  |  |
    |  +--+..|..-
    |  |  |  |
    |  |  |  |
    |  +--+  |
    |  .     |
    |  .     |
    +--------+
    .  .
    .  .
   >|--|<
    dx = 3
@

[Favored direction for centers of horizontal sides]
When computing the /center/ of an horizontal side, if the side has an /even/ length,
we must favor a 'Direction'.
(Note that if the side has an /odd/ length, there is no ambiguity.)

In 'Text.Alignment.align' implementation, 'Text.Alignment.Centered' alignment
favors the 'RIGHT' 'Direction':

@
   1
   12
  123
  1234
   ^
@


* If we, too, favor the 'RIGHT' 'Direction', when the returned point is used as
reference for a 'Centered' alignment, the text will tend to be too far to the 'RIGHT',
as illustrated here (@^@ indicates the chosen center):

@
   1
 +--+
   12
 +--+
  123
 +--+
  1234
 +--+
   ^
@

* So we will favor the 'LEFT' 'Direction', to counterbalance the choice made in
'Text.Alignment.align' 's implementation:

@
  1
 +--+
  12
 +--+
 123
 +--+
 1234
 +--+
  ^
@
-}
getSideCentersAtDistance :: RectContainer
                         -- ^ Reference container
                         -> Length Width
                         -- ^ Horizontal distance
                         -> Length Height
                         -- ^ Horizontal distance
                         -> (Coords Pos, Coords Pos, Coords Pos, Coords Pos)
                         -- ^ (center Up, center Down, center Left, center Right)
getSideCentersAtDistance (RectContainer (Size rs' cs') upperLeft') dx dy =
  (centerUp, centerDown, leftMiddle, rightMiddle)
 where
  deltaLength dist =
    2 *    -- in both directions
      (1 +   -- from inner content to outer container
       dist) -- from container to container'
  rs = rs' + fromIntegral (deltaLength dy)
  cs = cs' + fromIntegral (deltaLength dx)
  upperLeft = translate' (fromIntegral $ -dy) (fromIntegral $ -dx) upperLeft'

  cHalf = quot (cs-1) 2 -- favors 'LEFT' 'Direction', see haddock comments.
  rHalf = quot (rs-1) 2 -- favors 'Up' 'Direction'
  rFull = rs-1

  centerUp    = translate' 0     cHalf upperLeft
  centerDown  = translate' rFull cHalf upperLeft
  leftMiddle  = translate' rHalf 0     upperLeft
  rightMiddle = translate' rHalf (cs-1) upperLeft