%---------------------------------------------------------------------------%
% vim: ft=mercury ts=4 sw=4 et
%---------------------------------------------------------------------------%
% Copyright (C) 2001-2007 The University of Melbourne.
% Copyright (C) 2015, 2018, 2021-2022 The Mercury team.
% This file is distributed under the terms specified in COPYING.LIB.
%---------------------------------------------------------------------------%
%
% File: sized_pretty.m:
% Author: sthur.
%
% When printing a term during debugging this module allows the user to put
% a limit on the size of the term displayed. This limit is specified by
% setting the number of lines you want and the width of these lines.
%
% How to use sized_pretty.m :
% ---------------------------
%
% Call univ_to_string_line with the following variables:
% univ_to_string_line(Univ, LineWidth, Lines, String) where
% Univ      : is the Term (in univ type) you want to convert
% LineWidth : is the length of the lines
% Lines     : is the number of lines you want the term to be printed on
% String    : output string
%
% EXAMPLES
% --------
%
% Term Used in these examples:
%
%   Term = big(
%             big(
%                big(
%                   small,
%                   "Level 3",
%                   small
%                   ),
%               "Level 2",
%                small
%                ),
%             "Level 1",
%             big(
%                big(
%                   small,
%                   "Level 3",
%                   small
%                   ),
%                "Level 2",
%                small
%                )
%             ).
%
%---------------------------------------------------------------------------%
% Width = 18, Line(s) = 16
%
% big(
%   big(
%     big(
%       small,
%       "Level 3",
%       small),
%     "Level 2",
%     small),
%   "Level 1",
%   big(
%     big(
%       small,
%       "Level 3",
%       small),
%     "Level 2",
%     small))
%
%---------------------------------------------------------------------------%
% Width = 16, Line(s) = 16
%
% big(
%   big(
%     big/3,
%     "Level 2",
%     small),
%   "Level 1",
%   big(
%     big/3,
%     "Level 2",
%     small))
%
%---------------------------------------------------------------------------%
% Width = 50, Line(s) = 4
%
% big(
%   big(big/3, "Level 2", small),
%   "Level 1",
%   big(big/3, "Level 2", small))
%
%---------------------------------------------------------------------------%
% Width = 56, Line(s) = 4
%
% big(
%   big(big(small, "Level 3", small), "Level 2", small),
%   "Level 1",
%   big(big(small, "Level 3", small), "Level 2", small))
%
%---------------------------------------------------------------------------%
% Width = 30, Line(s) = 5
%
% big(big/3, "Level 1", big/3)
%
%---------------------------------------------------------------------------%
% Width = 33, Line(s) = 5
%
% big(
%   big(big/3, "Level 2", small),
%   "Level 1",
%   big(big/3, "Level 2", small))
%
%---------------------------------------------------------------------------%
% Width = 75, Line(s) = 1
%
% big(big(big/3, "Level 2", small), "Level 1", big(big/3, "Level 2", small))
%
%---------------------------------------------------------------------------%
% Width = 20, Line(s) = 10
%
% big(
%   big(
%     big/3,
%     "Level 2",
%     small),
%   "Level 1",
%   big(
%     big/3,
%     "Level 2",
%     small))
%
%---------------------------------------------------------------------------%
% Width = 40, Line(s) = 10
%
% big(
%   big(
%     big(small, "Level 3", small),
%     "Level 2",
%     small),
%   "Level 1",
%   big(
%     big(small, "Level 3", small),
%     "Level 2",
%     small))
%
%---------------------------------------------------------------------------%
% Width = 29, Line(s) = 1
%
% big(big/3, "Level 1", big/3)
%
%---------------------------------------------------------------------------%
% Width = 20, Line(s) = 1
%
% big/3
%
%---------------------------------------------------------------------------%
%---------------------------------------------------------------------------%

:- module mdb.sized_pretty.
:- interface.

:- import_module mdb.browser_info.
:- import_module mdb.browser_term.

:- import_module univ.

%---------------------------------------------------------------------------%

    % univ_to_string_line(Univ, LineWidth, Lines, String):
    %
    % Converts the term in Univ to a string that fits into Lines lines
    % of width LineWidth. It may throw an exception or cause a runtime
    % abort if the term in question has no canonical representation.
    %
:- pred univ_to_string_line(browser_db::in, univ::in,
    int::in, int::in, string::out) is cc_multi.

    % The same as univ_to_string_line, except works on browser_terms.
    %
:- pred browser_term_to_string_line(browser_db::in,
    browser_term::in, int::in, int::in, string::out) is cc_multi.

%---------------------------------------------------------------------------%
%---------------------------------------------------------------------------%

:- implementation.

:- import_module bool.
:- import_module deconstruct.
:- import_module int.
:- import_module list.
:- import_module maybe.
:- import_module pair.
:- import_module pprint.
:- import_module std_util.
:- import_module string.

%---------------------------------------------------------------------------%

:- type no_measure_params
    --->    no_measure_params.

:- type measure_params
    --->    measure_params(int).    % This parameter specifies Linewidth

:- type maybe_deconstructed(T)
    --->    not_deconstructed
    ;       deconstructed(
                string,                 % Functor
                int,                    % Arity
                size_annotated_args(T)  % arguments
            ).

    % The exact type indicates the term has been fully deconstructed.
    % The at_least type indicates that the term has been deconstructed
    % upto the point where the specified limit was reached.
:- type size_annotated_term(T)
    --->    exact(
                browser_term,           % univ(Term)
                T,                      % size of the term
                string,                 % Functor
                int,                    % Arity
                size_annotated_args(T)  % arguments
            )
    ;       at_least(
                browser_term,           % univ(Term)
                T,                      % size of the term up to the
                                        % point where it is deconstructed
                maybe_deconstructed(T)
            ).

:- type size_annotated_args(T) ==
    list(maybe(pair(T, size_annotated_term(T)))).

:- typeclass measure(T) where [
    func max_measure(T, T) = T is det,
    func zero_measure = T is det,
    func compare_measures(T, T) = comparison_result is det
].

:- typeclass measure_with_params(T, MeasureParams) <= measure(T) where [
    func add_measures(T, T, MeasureParams) = T is det,
    func subtract_measures(T, T, MeasureParams) = T is det,

        % Given a measurement and the measure parameters,
        % calculate an upper bound on the number of functors
        % that could fit in that space.
    func maximum_functors(T, MeasureParams) = int,

        % Given a term, its arity, and a limit, this method decides
        % the partial limit that each of the arguments should be
        % given. Its arguments in order are:
        %   - the term to print,
        %   - measure parameter(s),
        %   - measurement of the available space,
        %   - arity of the principal functor of the term,
        %   - a boolean which hints that the size of the principal
        %     functors of arguments should be checked before
        %     analyzing the term,
        %   - size of the principal functor of the term,
        %   - maybe an initial estimate of the measurement for
        %     each argument,
        %   - adjusted measurement of available space,
        %   - adjusted measure parameter(s).
    pred measured_split(browser_db::in, browser_term::in,
        MeasureParams::in, T::in, int::in, bool::in, T::out,
        maybe(T)::out, T::out, MeasureParams::out) is cc_multi
].

%---------------------------------------------------------------------------%

    % When the space (to print the term) is given in number of lines there
    % is an initial check that has to be done. This is due to the fact that
    % size_count_split assumes that every term is part of a bigger term and
    % therefore assumes that a term will have a comma and a space after it.
    % This is not true for the biggest term (Head Term) therefore if the
    % Head Term is going to be printed on a single line then it should be
    % given a limit of character_count(LineWidth - 1) instead of
    % character_count(LineWidth - 3).
    %
univ_to_string_line(BrowserDb, Univ, LineWidth, Lines, String) :-
    browser_term_to_string_line(BrowserDb, plain_term(Univ),
        LineWidth, Lines, String).

browser_term_to_string_line(BrowserDb, BrowserTerm, LineWidth, Lines,
        String) :-
    Params = measure_params(LineWidth),
    functor_browser_term_cc(BrowserDb, BrowserTerm, _Functor, Arity,
        MaybeReturn),
    ( if
        Arity \= 0,
        Lines \= 0,
        (Lines - 1) // Arity = 0
    then
        % "- 1" is to account for the newline character
        Limit = character_count(LineWidth - 1)
    else
        Limit = line_count(Lines)
    ),
    annotate_with_size(BrowserDb, BrowserTerm, Params, Limit, AnnotTerm),
    ( if
        MaybeReturn = yes,
        BrowserTerm = synthetic_term(_, _, yes(ReturnValue))
    then
        annotate_with_size(BrowserDb, plain_term(ReturnValue), Params, Limit,
            AnnotReturn),
        to_doc_sized(AnnotTerm, AnnotTermStr),
        to_doc_sized(AnnotReturn, AnnotReturnStr),
        Doc = group(
            AnnotTermStr
            `<>` line
            `<>` nest(2, text(" = ") `<>` AnnotReturnStr)
        )
    else
        to_doc_sized(AnnotTerm, Doc)
    ),
    String = pprint.to_string(LineWidth, Doc).

%---------------------------------------------------------------------------%

    % first_pass gives an idea of how much space each term takes.
    % In this pass the space is unevenly distributed; first come first
    % served. And once the space runs out, the term is not deconstructed
    % further.
    % In the second pass, the space is evenly distributed between
    % the terms and therefore the subterms are deconstructed evenly.
    %
:- pred annotate_with_size(browser_db::in, browser_term::in, MeasureParams::in,
    T::in, size_annotated_term(T)::out) is cc_multi
    <= measure_with_params(T, MeasureParams).

annotate_with_size(BrowserDb, BrowserTerm, Params, Limit, SizedTerm2) :-
    first_pass(BrowserDb, BrowserTerm, Params, Limit, SizedTerm1),
    second_pass(BrowserDb, SizedTerm1, Params, Limit, SizedTerm2).

%---------------------------------------------------------------------------%

:- pred first_pass(browser_db::in, browser_term::in, MeasureParams::in, T::in,
    size_annotated_term(T)::out) is cc_multi
    <= measure_with_params(T, MeasureParams).

first_pass(BrowserDb, BrowserTerm, Params, Limit, Size) :-
    MaxFunctors = maximum_functors(Limit, Params),
    limited_deconstruct_browser_term_cc(BrowserDb, BrowserTerm,
        MaxFunctors, MaybeFunctorArityArgs, _MaybeReturn),
    (
        MaybeFunctorArityArgs = yes({Functor, Arity, Args}),
        measured_split(BrowserDb, BrowserTerm, Params, Limit, Arity, yes,
            FunctorSize, MaybeInitArgLimit, NewLimit, NewParams),
        ( if
            Arity \= 0,
            MaybeInitArgLimit = no
        then
            Exact0 = no
        else
            Exact0 = yes
        ),
        annotate_args_with_size(BrowserDb, Args, MaybeInitArgLimit, NewParams,
            NewLimit, FunctorSize, SoFar, Exact0, Exact, MaybeArgSizes),
        (
            Exact = no,
            Size = at_least(BrowserTerm, SoFar,
                deconstructed(Functor, Arity, MaybeArgSizes))
        ;
            Exact = yes,
            Size = exact(BrowserTerm, SoFar, Functor, Arity, MaybeArgSizes)
        )
    ;
        MaybeFunctorArityArgs = no,
        Size = at_least(BrowserTerm, zero_measure, not_deconstructed)
    ).

%---------------------------------------------------------------------------%

    % Annotating the arguments.
    %
:- pred annotate_args_with_size(browser_db::in, list(univ)::in, maybe(T)::in,
    MeasureParams::in, T::in, T::in, T::out, bool::in, bool::out,
    size_annotated_args(T)::out) is cc_multi
    <= measure_with_params(T, MeasureParams).

annotate_args_with_size(_, [], _, _, _, SoFar, SoFar, Exact, Exact, []).
annotate_args_with_size(BrowserDb, [Arg | Args], MaybeInitArgLimit, Params,
        Limit, SoFar0, SoFar, Exact0, Exact,
        [MaybeArgSize | MaybeArgSizes]) :-
    (
        MaybeInitArgLimit = yes(InitArgLimit),
        ( if compare_measures(SoFar0, Limit) = (>) then
            AppliedArgLimit = InitArgLimit
        else
            AppliedArgLimit = max_measure(InitArgLimit,
                subtract_measures(Limit, SoFar0, Params))
        ),
        first_pass(BrowserDb, plain_term(Arg), Params, AppliedArgLimit, Size),
        MaybeArgSize = yes(InitArgLimit - Size),
        extract_size_from_annotation(Size) = ArgSize,
        SoFar1 = add_measures(SoFar0, ArgSize, Params),
        (
            Size = exact(_, _, _, _, _),
            Exact1 = Exact0
        ;
            Size = at_least(_, _, _),
            Exact1 = no
        )
    ;
        MaybeInitArgLimit = no,
        MaybeArgSize = no,
        SoFar1 = SoFar0,
        Exact1 = Exact0
    ),
    ( if compare_measures(SoFar1, Limit) = (>) then
        Exact2 = no
    else
        Exact2 = Exact1
    ),
    annotate_args_with_size(BrowserDb, Args, MaybeInitArgLimit, Params,
        Limit, SoFar1, SoFar, Exact2, Exact, MaybeArgSizes).

%---------------------------------------------------------------------------%

:- func extract_size_from_annotation(size_annotated_term(T)) = T.

extract_size_from_annotation(exact(_, Size, _, _, _)) = Size.
extract_size_from_annotation(at_least(_, Size, _)) = Size.

%---------------------------------------------------------------------------%

:- func extract_browser_term_from_annotation(size_annotated_term(T)) =
    browser_term.

extract_browser_term_from_annotation(exact(BrowserTerm, _, _, _, _)) =
    BrowserTerm.
extract_browser_term_from_annotation(at_least(BrowserTerm, _, _)) =
    BrowserTerm.

%---------------------------------------------------------------------------%

    % This predicate basically ensures that the arguments that take up
    % smaller "Space" than their fair share are fully printed, and the rest
    % of Space is shared equally between the other terms, which could take up
    % more than their share. If a term can be fully printed within
    % the given space, ("exact" type) then the Term is not altered.
    %
:- pred second_pass(browser_db::in, size_annotated_term(T)::in,
    MeasureParams::in, T::in, size_annotated_term(T)::out) is cc_multi
    <= measure_with_params(T, MeasureParams).

second_pass(BrowserDb, OldSizeTerm, Params, Limit, NewSizeTerm) :-
    (
        OldSizeTerm = exact(_BrowserTerm, _Size, _, _Arity, _MaybeArgs),
        NewSizeTerm = OldSizeTerm
    ;
        OldSizeTerm = at_least(_BrowserTerm, _Size, not_deconstructed),
        NewSizeTerm = OldSizeTerm
    ;
        OldSizeTerm = at_least(BrowserTerm, _Size,
            deconstructed(Functor, Arity,MaybeArgs)),
        measured_split(BrowserDb, BrowserTerm, Params, Limit, Arity,
            yes, FSize, MaybeInitLimit, NewLimit, NewParams),
        (
            MaybeInitLimit = yes(InitLimit),
            check_args(NewParams, MaybeArgs, InitLimit, Passed, FSize, Used),
            LeftOver = add_measures(subtract_measures(NewLimit,
                Used, Params), FSize, Params),
            measured_split(BrowserDb, BrowserTerm, Params, LeftOver,
                Arity - Passed, no, _, MaybeSplitLimit, _, _),
            (
                MaybeSplitLimit = yes(SplitLimit),
                process_args(BrowserDb, NewParams, MaybeArgs,
                    InitLimit, SplitLimit, NewArgs, NewSize0),
                NewSize = add_measures(FSize, NewSize0, NewParams),
                Result0 = list.map(check_if_exact, NewArgs),
                list.remove_adjacent_dups(Result0, Result),
                ( if Result = [yes] then
                    NewSizeTerm = exact(BrowserTerm, NewSize, Functor, Arity,
                        NewArgs)
                else
                    NewSizeTerm = at_least(BrowserTerm, NewSize,
                    deconstructed(Functor, Arity, NewArgs))
                )
            ;
                MaybeSplitLimit = no,
                NewSizeTerm = at_least(BrowserTerm, FSize, not_deconstructed)
            )
        ;
            MaybeInitLimit = no,
            NewSizeTerm = at_least(BrowserTerm, FSize, not_deconstructed)
        )
    ).

%---------------------------------------------------------------------------%

    % Given a list of size annotated terms(ie arguments) and a Limit,
    % this predicate returns "Passed" and "Used", where "Passed"
    % represents the number of terms that obey the Limit and are fully
    % represented("exact"), and "Used" represents the space that
    % these terms take up.
    %
:- pred check_args(MeasureParams::in, size_annotated_args(T)::in, T::in,
    int::out, T::in, T::out) is det
    <= measure_with_params(T, MeasureParams).

check_args(_, [], _, 0, !Used).
check_args(Params, [HeadArg | Rest], ArgLimit, Passed, !Used) :-
    (
        HeadArg = yes(X),
        X = _ - STerm,
        Size = extract_size_from_annotation(STerm),
        (
            STerm = exact(_, _, _, _, _),
            ( if compare_measures(ArgLimit, Size) = (<) then
                check_args(Params, Rest, ArgLimit, Passed, !Used)
            else
                Passed = 1 + PassedRest,
                !:Used = add_measures(!.Used, Size, Params),
                check_args(Params, Rest, ArgLimit, PassedRest, !Used)
            )
        ;
            STerm = at_least(_, _, _),
            check_args(Params, Rest, ArgLimit, Passed, !Used)
        )
    ;
        HeadArg = no,
        check_args(Params, Rest, ArgLimit, Passed, !Used)
    ).

%---------------------------------------------------------------------------%

    % This predicate accepts a list of size annotated terms(paired with a flag)
    % and returns a list of the same type. This new list would consist of the
    % same number of terms as the other but the terms which do not obey the
    % limit or not fully represented would be annotated again with a new limit
    % (SplitLimit). The rest of the terms are left alone.
    %
:- pred process_args(browser_db::in, MeasureParams::in,
    size_annotated_args(T)::in, T::in, T::in, size_annotated_args(T)::out,
    T::out) is cc_multi <= measure_with_params(T, MeasureParams).

process_args(_, _, [], _, _, [], zero_measure).
process_args(BrowserDb, Params, [HeadArg | Rest], ArgLimit, SplitLimit,
        [NewHeadArg | NewRest], SizeOut) :-
    (
        HeadArg = yes(X),
        X = _ - STerm,
        Size = extract_size_from_annotation(STerm),
        BrowserTerm = extract_browser_term_from_annotation(STerm),
        ( if
            STerm = exact(_, _, _, _, _),
            (
                compare_measures(ArgLimit, Size) = (>)
            ;
                compare_measures(ArgLimit, Size) = (=)
            )
        then
            NewHeadArg = HeadArg
        else
            NewHeadArg = yes(pair(SplitLimit, NewSTerm)),
            annotate_with_size(BrowserDb, BrowserTerm, Params, SplitLimit,
                NewSTerm)
        )
    ;
        HeadArg = no,
        NewHeadArg = no
    ),
    (
        NewHeadArg = yes(_ - Term),
        NewSize = extract_size_from_annotation(Term),
        SizeOut = add_measures(NewSize, RestSize, Params)
    ;
        NewHeadArg = no,
        SizeOut = RestSize
    ),
    process_args(BrowserDb, Params, Rest, ArgLimit, SplitLimit, NewRest,
        RestSize).

%---------------------------------------------------------------------------%

    % Check if a size-annotated arg is an exact type (fully represented).
    %
:- func check_if_exact(maybe(pair(T, size_annotated_term(T)))) = bool.

check_if_exact(no) = no.
check_if_exact(yes(_ - Term)) = Result:-
    (
        Term = exact(_, _, _, _, _),
        Result = yes
    ;
        Term = at_least(_, _, _),
        Result = no
    ).

%---------------------------------------------------------------------------%

    % Convert a size annotated term to a 'doc'.
    % (The doc type is defined in pprint.m).
    %
:- pred to_doc_sized(size_annotated_term(T)::in, doc::out) is cc_multi.

to_doc_sized(at_least(BrowserTerm, _, not_deconstructed), Doc) :-
    (
        BrowserTerm = plain_term(Univ),
        functor(univ_value(Univ), include_details_cc, Functor, Arity),
        Doc = text(Functor) `<>` text("/") `<>` poly(i(Arity))
    ;
        BrowserTerm = synthetic_term(Functor, Args, MaybeReturn),
        list.length(Args, Arity),
        (
            MaybeReturn = yes(_),
            Doc = text(Functor) `<>` text("/") `<>`
                poly(i(Arity)) `<>` text("+1")
        ;
            MaybeReturn = no,
            Doc = text(Functor) `<>` text("/") `<>` poly(i(Arity))
        )
    ).
to_doc_sized(at_least(_, _, deconstructed(Functor, Arity, MaybeArgs)), Doc) :-
    to_doc_sized_2(Functor, Arity, MaybeArgs, Doc).
to_doc_sized(exact(_, _, Functor, Arity, MaybeArgs), Doc) :-
    to_doc_sized_2(Functor, Arity, MaybeArgs, Doc).

%---------------------------------------------------------------------------%

    % Assumes that every argument must be on a different line
    % or all of them should be on the same line.
    %
:- pred to_doc_sized_2(string::in, int::in, size_annotated_args(T)::in,
    doc::out) is cc_multi.

to_doc_sized_2(Functor, _Arity, [], text(Functor)).
to_doc_sized_2(Functor, Arity, [HeadArg | Tail], Doc) :-
    list.map(handle_arg, [HeadArg | Tail], Args),
    list.remove_adjacent_dups(Args, NewArgs),
    ( if NewArgs = [nil] then
        Doc = text(Functor) `<>` text("/") `<>` poly(i(Arity))
    else
        Doc = text(Functor) `<>`
            parentheses(group(nest(2,
                line `<>` separated(id, comma_space_line, Args))))
    ).

%---------------------------------------------------------------------------%

:- pred handle_arg(maybe(pair(T,size_annotated_term(T)))::in, doc::out)
    is cc_multi.

handle_arg(yes(_ - Arg_Term), Doc) :-
    to_doc_sized(Arg_Term, Doc).
handle_arg(no, nil).

%---------------------------------------------------------------------------%

    % functor_count is a representation where the size of a term
    % is measured by the number of function symbols.
:- type functor_count
    --->    functor_count(int).     % No of function symbols

:- func add_functor_count(functor_count, functor_count,
    no_measure_params) = functor_count.

add_functor_count(functor_count(A), functor_count(B), _) =
    functor_count(A + B).

:- func subtract_functor_count(functor_count, functor_count,
    no_measure_params) = functor_count.

subtract_functor_count(functor_count(A), functor_count(B), _) =
    functor_count(A - B).

:- func maximum_functor_count(functor_count, no_measure_params) = int.

maximum_functor_count(functor_count(N), _) = N.

:- func compare_functor_count(functor_count, functor_count)
    = comparison_result.

compare_functor_count(functor_count(A), functor_count(B)) = R :-
    compare(R, A, B).

:- func max_functor_count(functor_count, functor_count) = functor_count.

max_functor_count(functor_count(A), functor_count(B)) = functor_count(Max) :-
    int.max(A, B, Max).

:- func zero_functor_count = functor_count.

zero_functor_count = functor_count(0).

:- pred functor_count_split(browser_db::in, browser_term::in,
    no_measure_params::in, functor_count::in, int::in, bool::in,
    functor_count::out, maybe(functor_count)::out, functor_count::out,
    no_measure_params::out) is cc_multi.

functor_count_split(_, _, Params, functor_count(Limit), Arity, _,
        functor_count(1), MaybeArgLimit, functor_count(Limit), Params) :-
    ( if Arity = 0 then
        % This artificial detism cast shuts up a warning about
        % the true detism of functor_count_split being det.
        MaybeArgLimit0 = no,
        cc_multi_equal(MaybeArgLimit0, MaybeArgLimit)
    else
        ( if Limit =< Arity + 1 then
            MaybeArgLimit = no
        else
            RoundUp = (Limit + Arity - 1) // Arity,
            MaybeArgLimit = yes(functor_count(RoundUp))
        )
    ).

:- instance measure(functor_count) where [
    func(compare_measures/2) is compare_functor_count,
    func(max_measure/2) is max_functor_count,
    func(zero_measure/0) is zero_functor_count
].

:- instance measure_with_params(functor_count, no_measure_params) where [
    func(add_measures/3) is add_functor_count,
    func(subtract_measures/3) is subtract_functor_count,
    func(maximum_functors/2) is maximum_functor_count,
    pred(measured_split/10) is functor_count_split
].

%---------------------------------------------------------------------------%

    % char_count is a representation where the size of a term is
    % measured by the number of characters.
:- type char_count
    --->    char_count(int).    % No of characters

:- func add_char_count(char_count, char_count, no_measure_params) = char_count.

add_char_count(char_count(A), char_count(B), _) = char_count(A + B).

:- func subtract_char_count(char_count, char_count,
    no_measure_params) = char_count.

subtract_char_count(char_count(A), char_count(B), _) =
    char_count(A - B).

:- func maximum_char_count(char_count, no_measure_params) = int.

maximum_char_count(char_count(N), _) = Max :-
    % Each functor except the first takes a minimum of three chars.
    Max = (N + 2) div 3.

:- func compare_char_count(char_count, char_count) = comparison_result.

compare_char_count(char_count(A), char_count(B)) = R :-
    compare(R, A, B).

:- func max_char_count(char_count, char_count) = char_count.

max_char_count(char_count(A), char_count(B)) = char_count(Max) :-
    int.max(A, B, Max).

:- func zero_char_count = char_count.

zero_char_count = char_count(0).

:- pred char_count_split(browser_db::in, browser_term::in,
    no_measure_params::in, char_count::in, int::in, bool::in,
    char_count::out, maybe(char_count)::out, char_count::out,
    no_measure_params::out) is cc_multi.

char_count_split(BrowserDb, BrowserTerm, Params, char_count(Limit), Arity,
        Check, char_count(FunctorSize), MaybeArgLimit, char_count(Limit),
        Params) :-
    deconstruct_browser_term_cc(BrowserDb, BrowserTerm, Functor, _, Args,
        MaybeReturn),
    (
        Check = yes,
        get_arg_length(Args, TotalLength, _)
    ;
        Check = no,
        TotalLength = 0
    ),
    (
        MaybeReturn = yes(_),
        % Arity-1 times the string ", ", once "()", and once " = "
        FunctorSize = string.length(Functor) + 2 * Arity + 3
    ;
        MaybeReturn = no,
        % Arity-1 times the string ", ", and once "()"
        FunctorSize = string.length(Functor) + 2 * Arity
    ),
    ( if Arity = 0 then
        MaybeArgLimit = no
    else
        ( if Limit =< (FunctorSize + TotalLength) then
            MaybeArgLimit = no
        else
            RoundUp = (Limit + Arity - FunctorSize) // Arity,
            MaybeArgLimit = yes(char_count(RoundUp))
        )
    ).

:- instance measure(char_count) where [
    func(compare_measures/2) is compare_char_count,
    func(max_measure/2) is max_char_count,
    func(zero_measure/0) is zero_char_count
].

:- instance measure_with_params(char_count, no_measure_params) where [
    func(add_measures/3) is add_char_count,
    func(subtract_measures/3) is subtract_char_count,
    func(maximum_functors/2) is maximum_char_count,
    pred(measured_split/10) is char_count_split
].

%---------------------------------------------------------------------------%

    % size_count is a representation where the size of a term is
    % measured by number of lines or number of characters.
:- type size_count
    --->    line_count(int)         % no of lines
    ;       character_count(int).   % no of characters

:- func add_size_count(size_count, size_count, measure_params) = size_count.

add_size_count(character_count(A), character_count(B), Params) = Result :-
    Params = measure_params(LineWidth),
    CharSum = A + B,
    ( if CharSum > LineWidth then
        Result = line_count(1)
    else
        Result = character_count(CharSum)
    ).

add_size_count(character_count(A), line_count(B), _) = Result :-
    ( if A > 0 then
        Result = line_count(B + 1)
    else
        Result = line_count(B)
    ).

add_size_count(line_count(A), character_count(B), _) = Result :-
    ( if B > 0 then
        Result = line_count(A + 1)
    else
        Result = line_count(A)
    ).

add_size_count(line_count(A), line_count(B), _) = line_count(A + B).

    % Rounding up the Lines and subtracting works because we assume that
    % each argument is a different line or they are all on the same line.
    % But this requires you to determine which case likely to happen
    % before hand. For example if a term is to be on one line, you should
    % do subtract_size_count(character_count(LineLength),
    % character_count(arglength)) rather than
    % subtract_size_count(line_count(1), character_count(arglength)).
    % The reason that this situation cannot be detected in this code is:
    % A term can be printed on a single line only if all of its arguments
    % can be printed on the same line. And you cannot determine which case
    % is likely to happen in this code using the information it has.
    % Therefore size_count_split determines which case is true,
    % and changes the limit accordingly.
    %
:- func subtract_size_count(size_count, size_count,measure_params)
    = size_count.

subtract_size_count(character_count(A), character_count(B), _) = Result :-
    CharDiff = A - B,
    ( if CharDiff < 0 then
        Result = character_count(0)
    else
        Result = character_count(CharDiff)
    ).

subtract_size_count(character_count(A), line_count(B), _) = Result :-
    ( if B = 0 then
        Result = character_count(A)
    else
        Result = character_count(0)
    ).

subtract_size_count(line_count(A), character_count(B), _) = Result :-
    ( if B = 0 then
        Result = line_count(A)
    else
        ( if A - 1 >= 0 then
            Result = line_count(A - 1)
        else
            Result = line_count(0)
        )
    ).

subtract_size_count(line_count(A), line_count(B), _) = Result :-
    ( if A - B >= 0 then
        Result = line_count(A - B)
    else
        Result = line_count(0)
    ).

:- func maximum_size_count(size_count, measure_params) = int.

maximum_size_count(character_count(N), _) = Max :-
    % Each functor except the first takes a minimum of three chars.
    Max = (N + 2) div 3.

maximum_size_count(line_count(N), measure_params(LineWidth)) = Max :-
    % We can't assume any particular layout, so we go by how many functors
    % will fit in the area covered by N lines. Each functor except the first
    % takes a minimum of three chars.
    Area = N * LineWidth,
    Max = (Area + 2) div 3.

:- func compare_size_count(size_count, size_count) = comparison_result.

compare_size_count(character_count(C1), character_count(C2)) = R :-
    compare(R, C1, C2).
compare_size_count(character_count(_), line_count(_)) = (<).
compare_size_count(line_count(_), character_count(_)) = (>).
compare_size_count(line_count(L1), line_count(L2)) = R :-
    compare(R, L1, L2).

:- func max_size_count(size_count, size_count) = size_count.

max_size_count(A, B) = Max :-
    ( if compare_size_count(A, B) = (>) then
        Max = A
    else
        Max = B
    ).

:- func zero_size_count = size_count.

zero_size_count = character_count(0).

    % We assume that all arguments have to be on separate lines, or
    % the whole term should be printed on a single line.
    %
:- pred size_count_split(browser_db::in, browser_term::in, measure_params::in,
    size_count::in, int::in, bool::in, size_count::out,
    maybe(size_count)::out, size_count::out, measure_params::out)
    is cc_multi.

size_count_split(BrowserDb, BrowserTerm, Params, Limit, Arity, Check,
        FunctorSize, MaybeArgLimit, NewLimit, NewParams) :-
    % LineWidth is length of the line in which the functor is printed.
    Params = measure_params(LineWidth),
    deconstruct_browser_term_cc(BrowserDb, BrowserTerm, Functor, ActualArity,
        Args, MaybeReturn),
    FSize = string.length(Functor) + 2 * (ActualArity),
    (
        Check = yes,
        get_arg_length(Args, TotalLength, MaxArgLength),
        int.max(MaxArgLength, (string.length(Functor) + 1), MaxLength)
    ;
        Check = no,
        TotalLength = 0,
        MaxLength = 0
    ),
    ( if Arity = 0 then
        MaybeArgLimit = no,
        FunctorSize = character_count(FSize),
        NewLimit = Limit,
        NewParams = Params
    else
        ( if
            Limit = line_count(LineLimit),
            % we need one line for the functor and at least one line
            % for each argument.
            LineLimit >= (Arity + 1),
            % Linewidth is decreased by two characters to account for
            % indentation.
            (LineWidth - 2) >= MaxLength
        then
            Line = (LineLimit - 1) // Arity,
            MaybeArgLimit = yes(line_count(Line)),
            FunctorSize = line_count(1),
            NewLimit = Limit,
            NewParams = measure_params(LineWidth - 2)
        else if
            Limit = line_count(LineLimit),
            LineLimit > 0,
            % Since every term is part of a bigger term (in this context
            % anyway) it will have a comma, space and a newline at the end
            % of it (Hence the "- 3").
            LineWidth - 3 >= (FSize + TotalLength)
        then
            % "Arity - 1" is for rounding up.
            Char = (LineWidth - 3 - FSize + Arity - 1) // Arity,
            MaybeArgLimit = yes(character_count(Char)),
            FunctorSize = character_count(FSize),
            NewLimit = character_count(LineWidth - 3),
            NewParams = Params
        else if
            Limit = character_count(CharLimit),
            CharLimit >= (FSize + TotalLength)
        then
            Char = (CharLimit - FSize + Arity - 1) // Arity,
            MaybeArgLimit = yes(character_count(Char)),
            FunctorSize = character_count(FSize),
            NewLimit = Limit,
            NewParams = Params
        else
            MaybeArgLimit = no,
            % If an ordinary term is not deconstructed, it is printed as
            % "functor/Arity". If a synthetic function term is not
            % deconstructed, it is printed as "functor/Arity+1".
            (
                MaybeReturn = yes(_),
                FuncSuffixChars = 2
            ;
                MaybeReturn = no,
                FuncSuffixChars = 0
            ),
            FunctorLength = string.length(Functor),
            string.int_to_string(Arity, ArityStr),
            string.length(ArityStr, ArityChars),
            FunctorSize = character_count(FunctorLength + 1
                + ArityChars + FuncSuffixChars),
            NewLimit = Limit,
            NewParams = Params
        )
    ).

:- instance measure(size_count) where [
    func(compare_measures/2) is compare_size_count,
    func(max_measure/2) is max_size_count,
    func(zero_measure/0) is zero_size_count
].

:- instance measure_with_params(size_count, measure_params) where [
    func(add_measures/3) is add_size_count,
    func(subtract_measures/3) is subtract_size_count,
    func(maximum_functors/2) is maximum_size_count,
    pred(measured_split/10) is size_count_split
].

%---------------------------------------------------------------------------%

    % This predicate determines how many characters it will take
    % to print the functors of the arguments. Also determines the
    % length of biggest functor.
    %
:- pred get_arg_length(list(univ)::in, int::out, int::out) is cc_multi.

get_arg_length([], 0, 0).
get_arg_length([HeadUniv | Rest], TotalLength, MaxLength) :-
    functor(univ_value(HeadUniv), include_details_cc, Functor, Arity),
    (
        Rest = [],
        Correction = 2
    ;
        Rest = [_ | _],
        Correction = 3
    ),
    ( if Arity = 0 then
        Length = string.length(Functor)
    else
        % Since the term has arguments, then the shortest way to print it
        % is "functor/Arity", which adds one for the / and *at least* one
        % for the Arity.
        Length = string.length(Functor) + 2
    ),
    TotalLength = Length + RestTotalLength,
    int.max((Length + Correction), RestMaxLength, MaxLength),
    get_arg_length(Rest, RestTotalLength, RestMaxLength).

%---------------------------------------------------------------------------%
:- end_module mdb.sized_pretty.
%---------------------------------------------------------------------------%
