-- | Calculates the allocated cells -- the Payee cell and the Account
-- cell. Here is the logic for this process:
--
-- 1. If neither Payee nor Account appears, do nothing.
--
-- 2. Obtain the width of the growing cells, including the
-- spacers. One of the spacers attached to a field might be omitted:
--
-- a. If the rightmost growing field is TotalQty, include all spacers.
--
-- b. If the rightmost growing field is to the left of Payee, include
-- all spacers.
--
-- c. If the rightmost growing field is to the right of Account but is
-- not TotalQty, omit its spacer.
--
-- 2. Obtain the width of the Payee and Account spacers. Include each
-- spacer if its corresponding field appears in the report.
--
-- 3. Subtract from the total report width the width of the the
-- growing cells and the width of the Payee and Account spacers. This
-- gives the total width available for the Payee and Account
-- fields. If there are not at least two columns available, return
-- without including the Payee and Account fields.
--
-- 4. Determine the total width that the Payee and Account fields
-- would obtain if they had all the space they could ever need. This
-- is the "requested width".
--
-- 5. Split up the available width for the Payee and Account fields
-- depending on which fields appear:
--
-- a. If only the one field appears, then it shall be as wide as the
-- total available width or the its requested width, whichever is
-- smaller.
--
-- b. If both fields appear, then calculate the allocated width for
-- each field. If either field's requested width is less than its
-- allocated width, then that field is only as wide as its requested
-- width. The other field is then as wide as (the sum of its allocated
-- width and the leftover width from the other field) or its requested
-- width, whichever is smaller. If neither field's requested width is
-- less than its allocated width, then each field gets ts allocated
-- width.
--
-- 6. Fill cell contents; return filled cells.

module Penny.Cabin.Posts.Allocated (
  payeeAndAcct
  , AllocatedOpts(..)
  , Fields(..)
  , SubAccountLength(..)
  , Alloc
  , alloc
  , unAlloc
  ) where

import Control.Applicative(Applicative((<*>), pure), (<$>))
import Control.Arrow (second)
import Data.Maybe (catMaybes, isJust)
import Data.Monoid (mempty)
import Data.List (intersperse)
import qualified Data.Foldable as Fdbl
import qualified Data.Sequence as Seq
import qualified Data.Traversable as T
import qualified Data.Text as X
import qualified System.Console.Rainbow as Rb
import qualified Penny.Cabin.Row as R
import qualified Penny.Cabin.Posts.Growers as G
import qualified Penny.Cabin.Posts.Meta as M
import qualified Penny.Cabin.Posts.Spacers as S
import qualified Penny.Cabin.Posts.Types as Ty
import qualified Penny.Cabin.Scheme as E
import qualified Penny.Cabin.TextFormat as TF
import qualified Penny.Lincoln as L
import qualified Penny.Lincoln.Bits.Qty as Qty
import qualified Penny.Lincoln.Queries as Q
import qualified Penny.Lincoln.HasText as HT

data Fields a = Fields {
  payee :: a
  , account :: a
  } deriving (Eq, Show)

newtype SubAccountLength =
  SubAccountLength { unSubAccountLength :: Int }
  deriving Show

newtype Alloc = Alloc { unAlloc :: Int }
  deriving Show

alloc :: Int -> Alloc
alloc i =
  if i < 1
  then error $ "allocations must be greater than zero."
       ++ " supplied allocation: " ++ show i
  else Alloc i


-- | All the information needed for allocated cells.
data AllocatedOpts = AllocatedOpts
  { fields :: Fields Bool
  , subAccountLength :: SubAccountLength
  , allocations :: Fields Alloc
  , spacers :: S.Spacers Int
  , growerWidths :: G.Fields (Maybe Int)
  , reportWidth :: Ty.ReportWidth
  }

-- | Creates Payee and Account cells. The user must have requested the
-- cells. In addition, no cells are created if there is not enough
-- space for them in the report. Returns a Fields; each element of the
-- Fields is Nothing if no cells were created (either because the user
-- did not ask for them, or because there was no room) or Just cs i,
-- where cs is a list of all the cells, and i is the width of all the
-- cells.
payeeAndAcct
  :: E.Changers
  -> AllocatedOpts
  -> [(M.PostMeta, L.Posting)]
  -> Fields (Maybe ([R.ColumnSpec], Int))
payeeAndAcct ch ao bs =
  let allBuilders =
        T.traverse (builders ch (subAccountLength ao)) bs
      availWidth = availableWidthForAllocs (growerWidths ao)
                   (spacers ao) (fields ao) (reportWidth ao)
      finals = divideAvailableWidth availWidth (fields ao)
               (allocations ao)
               ( fmap (safeMaximum (Request 0))
                 . fmap (fmap fst) $ allBuilders)
  in fmap (fmap (second unFinal))
     . buildSpecs finals
     . fmap (fmap snd)
     $ allBuilders


safeMaximum :: Ord a => a -> [a] -> a
safeMaximum d ls = case ls of
  [] -> d
  xs -> maximum xs

payeeAndAccountSpacerWidth
  :: Fields Bool
  -> S.Spacers Int
  -> Int
payeeAndAccountSpacerWidth flds ss = pye + act
  where
    pye = if payee flds then abs (S.payee ss) else 0
    act = if account flds then abs (S.account ss) else 0

newtype AvailableWidth = AvailableWidth Int
        deriving (Eq, Ord, Show)

availableWidthForAllocs
  :: G.Fields (Maybe Int)
  -> S.Spacers Int
  -> Fields Bool
  -> Ty.ReportWidth
  -> AvailableWidth
availableWidthForAllocs growers ss flds (Ty.ReportWidth w) =
  AvailableWidth $ max 0 diff
  where
    tot = sumGrowersAndSpacers growers ss
          + payeeAndAccountSpacerWidth flds ss
    diff = w - tot

-- | Sums spacers for growing cells. This function is intended for use
-- only by the functions that allocate cells for the report, so it
-- assumes that either the Payee or the Account field is showing. Sums
-- all spacers, UNLESS the rightmost field is from PostingDrCr to
-- TotalCmdty, in which case the rightmost spacer is omitted. Apply to
-- the second element of the tuple returned by growCells (which
-- reflects which fields actually have width) and to the accompanying
-- Spacers.
sumSpacers ::
  G.Fields (Maybe a)
  -> S.Spacers Int
  -> Int
sumSpacers fs =
  sum
  . map fst
  . appearingSpacers
  . catMaybes
  . Fdbl.toList
  . fmap toWidth
  . pairedWithSpacers fs


-- | Takes a triple:
--
-- * The first element is Just _ if the field appears in the report;
-- Nothing if not
--
-- * The second element is Maybe Int for the width of the spacer
-- (TotalQty has no spacer, so it will be Nothing)
--
-- * The third element is the EFields tag
--
-- Returns Nothing if the field does not appear in the report. Returns
-- Just a pair if the field does appear in the report, where the first
-- element is the width of the spacer, and the second element is the
-- EFields tag.
toWidth :: (Maybe a, Maybe Int, t) -> Maybe (Int, t)
toWidth (maybeShowing, maybeWidth, tag) =
  if isJust maybeShowing
  then case maybeWidth of
    Just w -> Just (w, tag)
    Nothing -> Just (0, tag)
  else Nothing


-- | Given a list of all spacers that are attached to the fields that
-- are present in a report, return a list of the spacers that will
-- actually appear in the report. The rightmost spacer does not appear
-- if it is to the right of Account (unless there is a TotalQty field,
-- in which case, all spacers appear because TotalQty has no spacer.)
appearingSpacers :: [(Int, G.EFields)] -> [(Int, G.EFields)]
appearingSpacers ss = case ss of
  [] -> []
  l -> case snd $ last l of
    G.ETotalQty -> l
    t -> if t > G.ENumber
         then init l
         else l

-- | Applied to two arguments: first, a Fields, and second, a
-- Spacers. Combines each Field with its corresponding Spacer and with
-- the GFields, which indicates each particular field.
pairedWithSpacers ::
  G.Fields a
  -> S.Spacers b
  -> G.Fields (a, Maybe b, G.EFields)
pairedWithSpacers f s =
  (\(a, b) c -> (a, b, c))
  <$> G.pairWithSpacer f s
  <*> G.eFields

-- | Sums the widths of growing cells and their accompanying
-- spacers; makes the adjustments described in sumSpacers.
sumGrowersAndSpacers ::
  G.Fields (Maybe Int)
  -> S.Spacers Int
  -> Int
sumGrowersAndSpacers fs ss = spcrs + flds where
  spcrs = sumSpacers fs ss
  flds = Fdbl.foldr f 0 fs where
    f maybeI acc = case maybeI of
      Nothing -> acc
      Just i -> acc + i

newtype Request = Request { unRequest :: Int }
        deriving (Eq, Ord, Show)

newtype Final = Final { unFinal :: Int }
        deriving (Eq, Ord, Show)


buildSpecs
  :: Fields (Maybe Final)
  -> Fields ([Final -> R.ColumnSpec])
  -> Fields (Maybe ([R.ColumnSpec], Final))
buildSpecs finals bs = f <$> finals <*> bs
  where
    f mayFinal gs = case mayFinal of
      Nothing -> Nothing
      Just fin -> Just ((gs <*> pure fin), fin)


-- | Divide the total available width between the two fields.
divideAvailableWidth
  :: AvailableWidth
  -> Fields Bool
  -> Fields Alloc
  -> Fields Request
  -> Fields (Maybe Final)
divideAvailableWidth (AvailableWidth aw) appear allocs rws = Fields pye act
  where
    minFinal i1 i2 =
      let m = min i1 i2
      in if m > 0 then Just . Final $ m else Nothing
    pairAtLeast i1 i2 = (atLeast i1, atLeast i2)
      where atLeast i = if i > 0 then Just . Final $ i else Nothing
    reqP = unRequest . payee $ rws
    reqA = unRequest . account $ rws
    (pye, act) = case (payee appear, account appear) of
      (False, False) -> (Nothing, Nothing)
      (True, False) -> (minFinal reqP aw, Nothing)
      (False, True) -> (Nothing, minFinal reqA aw)
      (True, True) ->
        let votes = [unAlloc . payee $ allocs, unAlloc . account $ allocs]
            allocRslt = Qty.largestRemainderMethod (fromIntegral aw)
                        (map fromIntegral votes)
            (allocP, allocA) = case allocRslt of
              x:y:[] -> (fromIntegral x, fromIntegral y)
              _ -> error "divideAvailableWidth error"
        in case (allocP > reqP, allocA > reqA) of
            (True, True) -> pairAtLeast reqP reqA
            (True, False) ->
              pairAtLeast reqP $ (min (allocA + (allocP - reqP))) reqA
            (False, True) ->
              pairAtLeast (min reqP (allocP + (allocA - reqA))) reqA
            (False, False) -> pairAtLeast allocP allocA


builders
  :: E.Changers
  -> SubAccountLength
  -> (M.PostMeta, L.Posting)
  -> Fields (Request, Final -> R.ColumnSpec)
builders ch sl b = Fields (buildPayee ch b) (buildAcct ch sl b)

buildPayee
  :: E.Changers
  -> (M.PostMeta, L.Posting)
  -> (Request, Final -> R.ColumnSpec)
  -- ^ Returns a tuple. The first element is the maximum width that
  -- this cell needs to display its value perfectly. The second
  -- element is a function that, when applied to an actual width,
  -- returns a ColumnSpec.

buildPayee ch i = (maxW, mkSpec)
  where
    pb = snd i
    eo = E.fromVisibleNum . M.visibleNum . fst $ i
    j = R.LeftJustify
    ps = (E.Other, eo)
    md = E.getEvenOddLabelValue E.Other eo ch
    mayPye = Q.payee pb
    maxW = Request $ maybe 0 (X.length . HT.text) mayPye
    mkSpec (Final w) = R.ColumnSpec j (R.Width w) ps sq
      where
        sq = case mayPye of
          Nothing -> []
          Just pye ->
            let wrapped =
                  Fdbl.toList
                  . TF.unLines
                  . TF.wordWrap w
                  . TF.txtWords
                  . HT.text
                  $ pye
                toBit (TF.Words seqTxts) =
                  md
                  . Rb.Chunk mempty
                  . X.unwords
                  . Fdbl.toList
                  $ seqTxts
            in fmap toBit wrapped


buildAcct
  :: E.Changers
  -> SubAccountLength
  -> (M.PostMeta, L.Posting)
  -> (Request, Final -> R.ColumnSpec)
  -- ^ Returns a tuple. The first element is the maximum width that
  -- this cell needs to display its value perfectly. The second
  -- element is a function that, when applied to an actual width,
  -- returns a ColumnSpec.

buildAcct ch sl i = (maxW, mkSpec)
  where
    pb = snd i
    eo = E.fromVisibleNum . M.visibleNum . fst $ i
    ps = (E.Other, eo)
    aList = L.unAccount . Q.account $ pb
    maxW = Request
           $ (sum . map (X.length . L.unSubAccount) $ aList)
           + max 0 (length aList - 1)
    md = E.getEvenOddLabelValue E.Other eo ch
    mkSpec (Final aw) = R.ColumnSpec R.LeftJustify (R.Width aw) ps sq
      where
        target = TF.Target aw
        shortest = TF.Shortest . unSubAccountLength $ sl
        ws = TF.Words . Seq.fromList . map L.unSubAccount $ aList
        (TF.Words shortened) = TF.shorten shortest target ws
        sq = [ md
               . Rb.Chunk mempty
               . X.concat
               . intersperse (X.singleton ':')
               . Fdbl.toList
               $ shortened ]

instance Functor Fields where
  fmap f i = Fields {
    payee = f (payee i)
    , account = f (account i) }

instance Applicative Fields where
  pure a = Fields a a
  ff <*> fa = Fields {
    payee = payee ff (payee fa)
    , account = account ff (account fa) }

instance Fdbl.Foldable Fields where
  foldr f z flds =
    f (payee flds) (f (account flds) z)

instance T.Traversable Fields where
  traverse f flds =
    Fields <$> f (payee flds) <*> f (account flds)