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

-- import           Control.Applicative ((<$>), (<*))
import qualified Data.Char (isSpace)
import qualified Data.Functor.Compose
import qualified Data.Foldable
-- import           Data.Foldable (Foldable)
import qualified Data.List
import qualified Data.List.NonEmpty
import qualified Data.Map.Strict as Data.Map
import qualified Data.Text.Lazy as TL
import qualified Data.Text as Text
import qualified Hcompta.Lib.Leijen as W
import           Hcompta.Lib.Leijen (Doc, (<>))
import           System.IO (Handle)
import qualified Text.Parsec as R hiding (satisfy, char)
import           Text.Parsec (Stream, ParsecT)

import qualified Hcompta.Account as Account
import           Hcompta.Account (Account)
import qualified Hcompta.Amount as Amount
import qualified Hcompta.Amount.Write as Amount.Write
import qualified Hcompta.Format.Ledger as Ledger
import           Hcompta.Format.Ledger
                  ( Comment
                  , Journal(..)
                  , Posting(..), Posting_by_Account, Posting_Type(..)
                  , Tag
                  , Transaction(..)
                  )
import qualified Hcompta.Date.Write as Date.Write
import qualified Hcompta.Format.Ledger.Read as Read
-- import           Hcompta.Lib.Consable (Consable(..))
import qualified Hcompta.Lib.Parsec as R

-- * Write '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

-- ** Measure '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

-- ** Measure 'Amount's

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

-- * Write '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)

-- * Write '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)

-- * Write 'Posting'

posting :: Posting_Lengths -> Posting_Type -> Posting -> Doc
posting max_posting_length type_
 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 posting_amounts of
		 True -> account type_ acct
		 False ->
			let len_acct = account_length type_ acct in
			let len_amts = amounts_length posting_amounts in
			account type_ acct <> do
			W.fill (2 + max_posting_length - (len_acct + len_amts)) (W.space <> W.space) <> do
			W.intercalate
			 (W.space <> (W.bold $ W.yellow $ W.char Read.amount_sep) <> W.space)
			 Amount.Write.amount posting_amounts
	<> (case cmts of
	 [] -> W.empty
	 [c] -> W.space <> comment c
	 _ -> W.line <> do comments (W.text "\t ") cmts)

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

-- ** Measure 'Posting'

type Posting_Lengths = (Int)

postings_lengths
 :: Posting_Type
 -> Posting_by_Account
 -> Posting_Lengths
 -> Posting_Lengths
postings_lengths type_ ps pl =
	Data.Foldable.foldr
	 (\p ->
		max
		 ( account_length type_ (posting_account p)
		 + amounts_length       (posting_amounts p) )
	 ) pl
	 (Data.Functor.Compose.Compose ps)

-- * Write 'Transaction'

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

transactions :: Foldable ts => ts Transaction -> Doc
transactions ts = do
	let transaction_lengths_ =
		Data.Foldable.foldr transaction_lengths 0 ts
	Data.Foldable.foldr
	 (\t doc ->
		transaction_with_lengths transaction_lengths_ t <> W.line <>
		(if W.is_empty doc then W.empty else W.line <> doc)
	 )
	 W.empty
	 ts

transaction_with_lengths :: Transaction_Lengths -> Transaction -> Doc
transaction_with_lengths
 posting_lengths_
 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 transaction_comments_before of
	 [] -> W.empty
	 _  -> comments W.space transaction_comments_before <> W.line) <> do
	(W.hcat $
		Data.List.intersperse
		 (W.char Read.date_sep)
		 (Data.List.map Date.Write.date (first_date:dates))) <> do
	(case status_ of
	 True -> W.space <> status status_
	 False -> W.empty) <> do
	code code_ <> do
	(case transaction_description of
	 "" -> W.empty
	 _  -> W.space <> (W.dullmagenta $ W.strict_text transaction_description)) <> do
	W.line <> do
	(case transaction_comments_after of
	 [] -> W.empty
	 _  -> comments W.space transaction_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_)))
		 (Ledger.posting_by_Signs_and_Account ps))
	 [ (Posting_Type_Regular, transaction_postings)
	 , (Posting_Type_Virtual, transaction_virtual_postings)
	 , (Posting_Type_Virtual_Balanced, transaction_balanced_virtual_postings)
	 ]

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

-- ** Measure 'Transaction'

type Transaction_Lengths = Posting_Lengths

transaction_lengths :: Transaction -> Posting_Lengths -> Posting_Lengths
transaction_lengths
 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, transaction_postings)
	 , (Posting_Type_Virtual, transaction_virtual_postings)
	 , (Posting_Type_Virtual_Balanced, transaction_balanced_virtual_postings)
	 ]

-- * Write 'Journal'

journal ::
 ( Foldable ts
 , Monoid (ts Transaction)
 ) => Journal (ts Transaction) -> Doc
journal Journal{ journal_transactions } =
	transactions journal_transactions

-- * Rendering

data Style
 =   Style
 { style_align :: Bool
 , style_color :: Bool
 }
style :: Style
style =
	Style
	 { style_align = True
	 , style_color = True
	 }

show :: Style -> Doc -> TL.Text
show Style{style_color, style_align} =
	W.displayT .
	if style_align
	then W.renderPretty style_color 1.0 maxBound
	else W.renderCompact style_color

put :: Style -> Handle -> Doc -> IO ()
put Style{style_color, style_align} handle =
	W.displayIO handle .
	if style_align
	then W.renderPretty style_color 1.0 maxBound
	else W.renderCompact style_color