import printer.*;
import printer.platform.*;



//
// Formatter types
//

Formatter?(X) = false;

formatter(F, forward x, forward ..params)
    = F[Type(captureValue(x))](captureValue(x), ..params);

// formatTo(stream, formatter, value)
define formatTo;

[F | Formatter?(F)]
overload printTo(stream, forward formatter: F) {
    formatTo(stream, formatter, forwardValue(formatter.value));
}



//
// Repr formatter
//

private record ReprFormatter[T] (values: T);

repr(forward ..values) = ReprFormatter(captureValues(..values));

[T] overload printTo(stream, f:ReprFormatter[T]) {
    printReprValuesTo(stream, ..forwardValues(f.values));
}


//
// Number base formatters
//

private record HexFormatter[T] (value: T);
private record HEXFormatter[T] (value: T);
private record OctFormatter[T] (value: T);

[T] overload Formatter?(static HexFormatter[T]) = true;
[T] overload Formatter?(static HEXFormatter[T]) = true;
[T] overload Formatter?(static OctFormatter[T]) = true;

[I | Integer?(I)]
hex(forward x: I) = formatter(HexFormatter, x);
[I | Integer?(I)]
HEX(forward x: I) = formatter(HEXFormatter, x);
[I | Integer?(I)]
oct(forward x: I) = formatter(OctFormatter, x);

private _formatBase(stream, value, fmt) {
    if (value < 0) {
        printTo(stream, '-');
        printWithSprintf(stream, fmt, -value);
    } else {
        printWithSprintf(stream, fmt, value);
    }
}

[U | UnsignedInteger?(U)]
overload _formatBase(stream, value:U, fmt) {
    printWithSprintf(stream, fmt, value);
}

private formatBase(stream, value, fmt, fmt64) {
    _formatBase(stream, value, fmt);
}
overload formatBase(stream, value: Int64, fmt, fmt64) {
    _formatBase(stream, value, fmt64);
}
overload formatBase(stream, value: UInt64, fmt, fmt64) {
    printWithSprintf(stream, fmt64, value);
}

[T] overload formatTo(stream, f: HexFormatter[T], value) {
    formatBase(stream, value, "%x", formatForHexInt64);
}

[T] overload formatTo(stream, f: HEXFormatter[T], value) {
    formatBase(stream, value, "%X", formatForHEXInt64);
}

[T] overload formatTo(stream, f: OctFormatter[T], value) {
    formatBase(stream, value, "%o", formatForOctInt64);
}



//
// hexString
//

record HexStringFormatter[T] (value: T);
[T] overload Formatter?(static HexStringFormatter[T]) = true;

HexStringSequence?(S) = Sequence?(S) and TypeSize(SequenceElementType(S)) == 1;

[S | HexStringSequence?(S)]
hexString(forward s:S) = formatter(HexStringFormatter, s);

[T] overload formatTo(stream, f: HexStringFormatter[T], value) {
    for (b in value)
        printWithSprintf(stream, "%02X", bitcast(b, UInt8));
}



//
// printedWidth function
//

// printedWidth(object) SizeT
define printedWidth;

[S | String?(S)]
overload printedWidth(s: S) = size(s);

[N, B | Integer?(N) and Integer?(B)]
widthInBase(n:N, base:B) {
    var nn = n;
    var len = SizeT(1);
    if (n < 0) {
        len += 1;
        while(nn <= -base) {
            len += 1;
            nn = Type(nn)(nn/base);
        }
    } else {
        while(nn >= base) {
            len += 1;
            nn = Type(nn)(nn/base);
        }
    }
    return ref len;
}

[I | Integer?(I)]
overload printedWidth(n: I) = widthInBase(n, 10);

[T]
overload printedWidth(f: HexFormatter[T]) = widthInBase(capturedRef(f.value), 16);
[T]
overload printedWidth(f: HEXFormatter[T]) = widthInBase(capturedRef(f.value), 16);
[T]
overload printedWidth(f: OctFormatter[T]) = widthInBase(capturedRef(f.value),  8);

overload printedWidth(c: Char) = 1;
overload printedWidth(b: Bool) = if (b) 4 else 5;



//
// minimum-width alignment formatter
//

private record LeftAlignedFormatter[T] (
    value: T,
    width: SizeT,
    filledWidth: SizeT,
    pad: Char,
);
private record RightAlignedFormatter[T] (
    value: T,
    width: SizeT,
    filledWidth: SizeT,
    pad: Char,
);

private AlignedFormatter?(T) = false;
[T] overload AlignedFormatter?(static LeftAlignedFormatter[T]) = true;
[T] overload AlignedFormatter?(static RightAlignedFormatter[T]) = true;
[T | AlignedFormatter?(T)] overload Formatter?(static T) = true;

[I | Integer?(I)]
private alignedFormatter(F, width: I, pad: Char, forward ..values) {
    // If printedWidth isn't available for all objects, print them to a string
    // and use the width of that
    var s = str(..values);
    var filledWidth = size(s);
    return formatter(F, Tuple(move(s)), SizeT(width), filledWidth, pad);
}

private filledWidth(..values)
    = foldValues(add, SizeT(0), ..mapValues(printedWidth, ..values));

[I, ..TT | Integer?(I) and allValues?(T => CallDefined?(printedWidth, T), ..TT)]
overload alignedFormatter(F, width: I, pad: Char, forward ..values: TT) {
    var cvalues = captureValues(..values);
    return F(cvalues, SizeT(width), filledWidth(..capturedRefs(cvalues)), pad);
}

leftAlignedWith(width, pad, forward ..values)
    = alignedFormatter(LeftAlignedFormatter, width, pad, ..values);
rightAlignedWith(width, pad, forward ..values)
    = alignedFormatter(RightAlignedFormatter, width, pad, ..values);

leftAligned(width, forward ..values)
    = alignedFormatter(LeftAlignedFormatter, width, ' ', ..values);
rightAligned(width, forward ..values)
    = alignedFormatter(RightAlignedFormatter, width, ' ', ..values);

[T]
overload printTo(stream, f: LeftAlignedFormatter[T]) {
    printTo(stream, ..forwardValues(f.value));
    var i = f.filledWidth;
    while (i < f.width) {
        printTo(stream, f.pad);
        i += 1;
    }
}
[T]
overload printTo(stream, f: RightAlignedFormatter[T]) {
    var i = f.filledWidth;
    while (i < f.width) {
        printTo(stream, f.pad);
        i += 1;
    }
    printTo(stream, ..forwardValues(f.value));
}
