{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
module Hcompta.Format.Ledger.Write where

import           Control.Applicative ((<$>), (<*))
import           Control.Arrow ((***))
import           Data.Decimal (DecimalRaw(..))
import qualified Data.Char (isSpace)
import           Data.Fixed (showFixed)
import qualified Data.Foldable
import qualified Data.List
import qualified Data.List.NonEmpty
import qualified Data.Map.Strict as Data.Map
import           Data.Maybe (fromMaybe)
import qualified Data.Text.Lazy as TL
import qualified Data.Text as Text
import qualified Data.Time.Calendar  as Time (toGregorian)
import qualified Data.Time.LocalTime as Time (LocalTime(..), TimeOfDay(..), TimeZone(..), timeZoneOffsetString, ZonedTime(..))
import qualified Hcompta.Lib.Leijen as W
import           Hcompta.Lib.Leijen (Doc, (<>))
import           System.IO (Handle)
import qualified Text.Parsec as R
import           Text.Parsec (Stream, ParsecT)

import qualified Hcompta.Model.Account as Account
import           Hcompta.Model.Account (Account)
import qualified Hcompta.Model.Amount as Amount
import           Hcompta.Model.Amount (Amount)
import qualified Hcompta.Model.Amount.Quantity as Quantity
import           Hcompta.Model.Amount.Quantity (Quantity)
import qualified Hcompta.Model.Amount.Style as Style
import           Hcompta.Model.Amount.Style (Style)
-- import qualified Hcompta.Model.Amount.Unit as Unit
import           Hcompta.Model.Amount.Unit (Unit)
import qualified Hcompta.Model.Transaction as Transaction
import           Hcompta.Model.Transaction (Comment, Tag, Transaction)
import qualified Hcompta.Model.Transaction.Posting as Posting
import           Hcompta.Model.Transaction (Posting)
import qualified Hcompta.Model.Journal as Journal
import           Hcompta.Model.Journal (Journal)
-- import qualified Hcompta.Model.Transaction.Tag as Tag
-- import           Hcompta.Model.Transaction (Tag)
-- import qualified Hcompta.Model.Date as Date
import           Hcompta.Model.Date (Date)
-- import           Hcompta.Format.Ledger.Journal as Journal
import qualified Hcompta.Format.Ledger.Read as Read
import qualified Hcompta.Lib.Parsec as R


-- * Printing 'Account'

account :: Posting.Type -> Account -> Doc
account type_ =
	case type_ of
	 Posting.Type_Regular -> account_
	 Posting.Type_Virtual -> \acct ->
		W.char Read.posting_type_virtual_begin <> do
		account_ acct <> do
		W.char Read.posting_type_virtual_end
	 Posting.Type_Virtual_Balanced -> \acct ->
		W.char Read.posting_type_virtual_balanced_begin <> do
		account_ acct <> do
		W.char Read.posting_type_virtual_balanced_end
	where
		account_ :: Account -> Doc
		account_ acct =
			W.align $ W.hcat $
				Data.List.NonEmpty.toList $
				Data.List.NonEmpty.intersperse
				 (W.bold $ W.yellow $ W.char Read.account_name_sep)
				 (Data.List.NonEmpty.map account_name acct)

account_name :: Account.Name -> Doc
account_name = W.strict_text

-- ** Mesuring 'Account'

account_length :: Posting.Type -> Account -> Int
account_length type_ acct =
	Data.Foldable.foldl
	 (\acc -> (1 +) . (acc +) . Text.length)
	 (- 1) acct +
	case type_ of
	 Posting.Type_Regular -> 0
	 Posting.Type_Virtual -> 2
	 Posting.Type_Virtual_Balanced -> 2

-- * Printing 'Amount'

amount :: Amount -> Doc
amount Amount.Amount
 { Amount.quantity=qty
 , Amount.style = style@(Style.Style
	 { Style.unit_side
	 , Style.unit_spaced
	 })
 , Amount.unit=unit_
 } = do
	case unit_side of
	 Just Style.Side_Left ->
		(unit unit_)
		<> (case unit_spaced of { Just True | unit_ /= "" -> W.space; _ -> W.empty })
	 _ -> W.empty
	<> quantity style qty
	<> case unit_side of
	 (Just Style.Side_Right) ->
		(case unit_spaced of { Just True | unit_ /= "" -> W.space; _ -> W.empty })
		<> unit unit_
	 Nothing ->
		(case unit_spaced of { Just True | unit_ /= "" -> W.space; _ -> W.empty })
		<> unit unit_
	 _ -> W.empty

unit :: Unit -> Doc
unit = W.yellow . W.strict_text

quantity :: Style -> Quantity -> Doc
quantity Style.Style
 { Style.fractioning
 , Style.grouping_integral
 , Style.grouping_fractional
 , Style.precision
 } qty = do
	let Decimal e n = Quantity.round precision qty
	let num = Prelude.show $ abs $ n
	let sign = W.bold $ W.yellow $ W.strict_text (if n < 0 then "-" else "")
	case e == 0 || precision == 0 of
	 True  -> sign <> do W.bold $ W.blue $ (W.strict_text $ Text.pack num)
	 False -> do
		let num_len = length num
		let padded =
			Data.List.concat
			 [ replicate (fromIntegral e + 1 - num_len) '0'
			 , num
			 , replicate (fromIntegral precision - fromIntegral e) '0'
			 ]
		let (int, frac) = Data.List.splitAt (max 1 (num_len - fromIntegral precision)) padded
		let default_fractioning =
			Data.List.head $
			del_grouping_sep grouping_integral $
			del_grouping_sep grouping_fractional $
			['.', ',']
		sign <> do
		W.bold $ W.blue $ do
			W.text (TL.pack $ maybe id (\g -> reverse . group g . reverse) grouping_integral $ int) <> do
			(W.yellow $ W.char (fromMaybe default_fractioning fractioning)) <> do
			W.text (TL.pack $ maybe id group grouping_fractional frac)
	where
		group :: Style.Grouping -> [Char] -> [Char]
		group (Style.Grouping sep sizes_) =
			Data.List.concat . reverse .
			Data.List.map reverse . fst .
			Data.List.foldl
			 (flip (\digit -> \x -> case x of
				 ([], sizes) -> ([[digit]], sizes)
				 (digits:groups, []) -> ((digit:digits):groups, [])
				 (digits:groups, curr_sizes@(size:sizes)) ->
					if length digits < size
					then (      (digit:digits):groups, curr_sizes)
					else ([digit]:[sep]:digits:groups, if sizes == [] then curr_sizes else sizes)
			 ))
			 ([], sizes_)
		del_grouping_sep grouping =
			case grouping of
			 Just (Style.Grouping sep _) -> Data.List.delete sep
			 _ -> id

-- ** Mesuring 'Amount'

amount_length :: Amount -> Int
amount_length Amount.Amount
 { Amount.quantity=qty
 , Amount.style = style@(Style.Style
	 { Style.unit_spaced
	 })
 , Amount.unit=unit_
 } = do
	Text.length unit_
	+ (case unit_spaced of { Just True | unit_ /= "" -> 1; _ -> 0 })
	+ quantity_length style qty

amounts_length :: Amount.By_Unit -> Int
amounts_length amts =
	if Data.Map.null amts
	then 0
	else
		Data.Map.foldr
		 (\n -> (3 +) . (+) (amount_length n))
		 (-3) amts

quantity_length :: Style -> Quantity -> Int
quantity_length Style.Style
 { Style.grouping_integral
 , Style.grouping_fractional
 , Style.precision
 } qty =
	let Decimal e n = Quantity.round precision qty in
	let sign_len = if n < 0 then 1 else 0 in
	let fractioning_len = if e > 0 then 1 else 0 in
	let num_len = if n == 0 then 0 else (1 +) $ truncate $ logBase 10 $ (fromIntegral (abs n)::Double) in
	let pad_left_len  = max 0 (fromIntegral e + 1 - num_len) in
	let pad_right_len = max 0 (fromIntegral precision - fromIntegral e) in
	let padded_len = pad_left_len + num_len + pad_right_len in
	let int_len  = max 1 (num_len - fromIntegral precision) in
	let frac_len = max 0 (padded_len - int_len) in
	( sign_len
	+ fractioning_len
	+ padded_len
	+ maybe 0 (group int_len)  grouping_integral
	+ maybe 0 (group frac_len) grouping_fractional
	)
	where
		group :: Int -> Style.Grouping -> Int
		group num_len (Style.Grouping _sep sizes_) =
			if num_len <= 0
			then 0
			else loop 0 num_len sizes_
			where
				loop :: Int -> Int -> [Int] -> Int
				loop pad len =
					\x -> case x of
					 [] -> 0
					 sizes@[size] ->
						let l = len - size in
						if l <= 0 then pad
						else loop (pad + 1) l sizes
					 size:sizes ->
						let l = len - size in
						if l <= 0 then pad
						else loop (pad + 1) l sizes

-- * Printing 'Date'

date :: Date -> Doc
date (Time.ZonedTime
 (Time.LocalTime day tod)
 tz@(Time.TimeZone tz_min _ tz_name)) = do
	let (y, mo, d) = Time.toGregorian day
	(if y == 0 then W.empty else W.integer y <> sep '/') <> do
	int2 mo <> do
	sep '/' <> int2 d <> do
	(case tod of
	 Time.TimeOfDay 0 0 0 -> W.empty
	 Time.TimeOfDay h m s ->
		W.space <> int2 h <> do
		sep ':' <> int2 m <> do
		(case s of
		 0 -> W.empty
		 _ -> sep ':' <> do
			(if s < 10 then W.char '0' else W.empty) <> do
			W.strict_text $ Text.pack $ showFixed True s)) <> do
	(case tz_min of
	 0 -> W.empty
	 _ | tz_name /= "" -> W.space <> do W.strict_text $ Text.pack tz_name
	 _ -> W.space <> do W.strict_text $ Text.pack $ Time.timeZoneOffsetString tz)
	where
		int2 :: Int -> Doc
		int2 i = if i < 10 then W.char '0' <> W.int i else W.int i
		sep :: Char -> Doc
		sep = W.bold . W.dullblack . W.char

-- * Printing 'Comment'

comment :: Comment -> Doc
comment com =
	W.cyan $ do
		W.char Read.comment_begin
		<> (case Text.uncons com of
		 Just (c, _) | not $ Data.Char.isSpace c -> W.space
		 _ -> W.empty)
		<> do W.if_color colorize (W.strict_text com)
	where
		colorize :: Doc
		colorize =
			case R.runParser (do
				pre <- R.many $ R.try $ do
					ns <- R.many $ R.satisfy (\c -> c /= Read.tag_value_sep && not (Data.Char.isSpace c))
					sh <- R.space_horizontal
					return (ns ++ [sh])
				((W.text $ TL.pack $ concat pre) <>) <$> tags <* R.eof)
			 () "" com of
			 Left _ -> W.strict_text com
			 Right doc -> doc
		tags :: Stream s m Char => ParsecT s u m Doc
		tags = do
			x <- tag_
			xs <- W.hcat <$> R.many (R.try (tag_sep >>= (\s -> (s <>) <$> tag_)))
			return $ x <> xs
			where
				tag_sep :: Stream s m Char => ParsecT s u m Doc
				tag_sep = do
					s <- R.char Read.tag_sep
					sh <- R.many R.space_horizontal
					return $
						do W.bold $ W.dullblack $ W.char s
						<> do W.text $ TL.pack sh
				tag_ :: Stream s m Char => ParsecT s u m Doc
				tag_ = do
					n <- Read.tag_name
					s <- R.char Read.tag_value_sep
					v <- Read.tag_value
					return $
						(W.yellow $ W.strict_text n)
						<> (W.bold $ W.dullblack $ W.char s)
						<> (W.red $ W.strict_text v)

comments :: Doc -> [Comment] -> Doc
comments prefix =
	W.hcat .
	Data.List.intersperse W.line .
	Data.List.map (\c -> prefix <> comment c)

-- * Printing 'Tag'

tag :: Tag -> Doc
tag (n, v) =
	(W.dullyellow $ W.strict_text n)
	<> W.char Read.tag_value_sep
	<> (W.dullred $ W.strict_text v)

-- * Printing 'Posting'

posting :: Posting_Lengths -> Posting.Type -> Posting -> Doc
posting
 ( max_account_length
 , max_amount_length
 )
 type_
 Posting.Posting
 { Posting.account=acct
 , Posting.amounts
 , Posting.comments=cmts
 -- , Posting.dates
 , Posting.status=status_
 -- , Posting.tags
 } =
	W.char '\t' <> do
	status status_ <> do
		case Data.Map.null amounts of
		 True -> account type_ acct
		 False ->
			W.fill (max_account_length + 2)
			 (account type_ acct) <> do
			W.fill (max 0
			 ( max_amount_length
			 - (fromIntegral $ amounts_length amounts) )) W.empty <> do
			W.intercalate
			 (W.space <> (W.bold $ W.yellow $ W.char Read.amount_sep) <> W.space)
			 amount amounts
	<> (case cmts of
	 [] -> W.empty
	 [c] -> W.space <> comment c
	 _ -> W.line <> do comments (W.text "\t ") cmts)

status :: Transaction.Status -> Doc
status = \x -> case x of
	 True  -> W.char '!'
	 False -> W.empty

-- ** Mesuring 'Posting'

type Posting_Lengths = (Int, Int)

nil_Posting_Lengths :: Posting_Lengths
nil_Posting_Lengths = (0, 0)

postings_lengths :: Posting.Type -> Posting.By_Account -> Posting_Lengths -> Posting_Lengths
postings_lengths type_ =
	flip $ Data.Map.foldl $ Data.List.foldl $
	flip $ \p ->
	(max (account_length type_ (Posting.account p)))
	***
	(max (amounts_length (Posting.amounts p)))

-- * Printing 'Transaction'

transaction :: Transaction -> Doc
transaction t = transaction_with_lengths (transaction_lengths t nil_Transaction_Lengths) t

transaction_with_lengths :: Transaction_Lengths -> Transaction -> Doc
transaction_with_lengths
 posting_lengths_
 Transaction.Transaction
 { Transaction.code=code_
 , Transaction.comments_before
 , Transaction.comments_after
 , Transaction.dates=(first_date, dates)
 , Transaction.description
 , Transaction.postings
 , Transaction.virtual_postings
 , Transaction.balanced_virtual_postings
 , Transaction.status=status_
 -- , Transaction.tags
 } = do
	(case comments_before of
	 [] -> W.empty
	 _  -> comments W.space comments_before <> W.line) <> do
	(W.hcat $
		Data.List.intersperse
		 (W.char Read.date_sep)
		 (Data.List.map date (first_date:dates))) <> do
	(case status_ of
	 True -> W.space <> status status_
	 False -> W.empty) <> do
	code code_ <> do
	(case description of
	 "" -> W.empty
	 _  -> W.space <> (W.dullmagenta $ W.strict_text description)) <> do
	W.line <> do
	(case comments_after of
	 [] -> W.empty
	 _  -> comments W.space comments_after <> W.line) <> do
	W.vsep $ Data.List.map
	 (\(type_, ps) ->
		W.intercalate W.line
		 (W.intercalate W.line
			 (W.vsep . Data.List.map
				 (posting posting_lengths_ type_)))
		 (Posting.by_signs_and_account ps))
	 [ (Posting.Type_Regular, postings)
	 , (Posting.Type_Virtual, virtual_postings)
	 , (Posting.Type_Virtual_Balanced, balanced_virtual_postings)
	 ]

code :: Transaction.Code -> Doc
code = \x -> case x of
	 "" -> W.empty
	 t  -> W.space <> W.char '(' <> W.strict_text t <> W.char ')'

-- ** Mesuring 'Transaction'

type Transaction_Lengths = Posting_Lengths

nil_Transaction_Lengths :: Posting_Lengths
nil_Transaction_Lengths = nil_Posting_Lengths

transaction_lengths :: Transaction -> Posting_Lengths -> Posting_Lengths
transaction_lengths
 Transaction.Transaction
 { Transaction.postings
 , Transaction.virtual_postings
 , Transaction.balanced_virtual_postings
 } posting_lengths_ = do
	Data.List.foldl
	 (flip (\(type_, ps) -> postings_lengths type_ ps))
	 posting_lengths_
	 [ (Posting.Type_Regular, postings)
	 , (Posting.Type_Virtual, virtual_postings)
	 , (Posting.Type_Virtual_Balanced, balanced_virtual_postings)
	 ]

-- * Printing 'Journal'

journal :: Journal -> Doc
journal Journal.Journal
 { Journal.transactions
 } = do
	let transaction_lengths_ =
		Data.Map.foldl
		 (Data.List.foldl (flip transaction_lengths))
		 nil_Transaction_Lengths
		 transactions
	Data.Map.foldl
	 (Data.List.foldl (\doc t ->
		(if W.is_empty doc then W.empty else doc <> W.line)
			<> transaction_with_lengths transaction_lengths_ t <> W.line
		))
	 W.empty
	 transactions

-- * Rendering

show :: Bool -> Doc -> TL.Text
show with_color = W.displayT . W.renderPretty with_color 1.0 maxBound

put :: Bool -> Handle -> Doc -> IO ()
put with_color handle = W.displayIO handle . W.renderPretty with_color 1.0 maxBound