import std.io
import std.typeset {
  BreakCluster, BreakPolicy, BreakMode, KinsokuRules, BreakOpportunity, Paragraph, Item,
  Box, Penalty, BreakRule, DistanceGrid, CostModel, LineGeometry, JustificationPolicy,
  JustificationMode, KnuthPlassPolicy, ParagraphPlan, PlanLine, classifyBreaks,
  breakParagraph,
}

// HAZE adapter. First stdin line is width in millipoints, followed by the sentence.
let hazeInput = io.readStdin().split("\n")
let lineWidth = hazeInput[0].toInt() ?? 100000

// Widths are in thousandths of a point. A Hangul syllable is 10 points wide and a space 2.5 points.
function cluster(text: string) -> BreakCluster {
    BreakCluster { text: text, advance: if text == " " { 2500 } else { 10000 }, trackingAfter: 0 }
}

// Unicode line breaking (UAX #14) plus the Korean line-breaking rules (KLReq). A word that fits on a line stays whole.
function koreanRules(clusters: Array<BreakCluster>) -> BreakPolicy {
    BreakPolicy {
        mode: BreakMode.Word, rules: KinsokuRules.Unicode16Ko,
        forbidBefore: [], forbidAfter: [], boundaryAllowed: clusters.map((c: BreakCluster) => true),
        generatedHyphenEnds: [], protectFittingLatin: false, preserveShortWords: false,
        narrowestWordTarget: lineWidth, bodyWordTarget: lineWidth
    }
}

// One box per cluster and a zero penalty at each break opportunity. The last opportunity always ends the line.
function paragraph(clusters: Array<BreakCluster>, breaks: Array<BreakOpportunity>) -> Paragraph {
    let mut items: Array<Item> = []
    for index in 0..<clusters.length {
        let c = clusters[index]
        items.push(Item.BoxItem(Box { unitIndex: index, text: c.text, width: c.advance, trackingAfter: 0 }))
    }
    for opportunity in breaks {
        let last = opportunity.unitEnd == clusters.length
        items.push(Item.PenaltyItem(Penalty {
            unitEnd: opportunity.unitEnd, width: 0, kind: opportunity.kind, flagged: false,
            rule: if last { BreakRule.Forced } else { BreakRule.Allowed(0) }
        }))
    }
    Paragraph {
        grid: DistanceGrid.MilliPoint, items: items, costModel: CostModel.FixedPoint,
        geometry: LineGeometry { bodyTarget: lineWidth, firstTarget: lineWidth, dropCapSpan: 0, dropCapInset: 0 },
        justification: JustificationPolicy {
            mode: JustificationMode.Unjustified, minGapCount: 0, minLineWidthRatioM: 0,
            minFinalLineWidthRatioM: None, maxAdjustment: 0, maxGapAdjustment: 0, maxGapRatioR: 0,
            latinSpacing: None, cjkStretchRatio: 0.0
        },
        knuthPlass: KnuthPlassPolicy {
            toleranceR: 0, linePenalty: 0, fitnessDemerit: 0, flaggedDemerit: 0, finalFlaggedDemerit: 0
        }
    }
}

// Join the clusters of each line and drop the space at the end of the line.
function linesOf(clusters: Array<BreakCluster>, plan: ParagraphPlan) -> string {
    plan.lines
        .map((line: PlanLine) => clusters.slice(line.start, line.trimmedEndExclusive).map((c: BreakCluster) => c.text).join(""))
        .join("\n")
}

let sentence = hazeInput.slice(1, hazeInput.length).join("\n")
let clusters = sentence.scalars().map(cluster)
let result = classifyBreaks(clusters, koreanRules(clusters))
    .flatMap((breaks: Array<BreakOpportunity>) => breakParagraph(paragraph(clusters, breaks)))
match result {
    case Ok(plans) => {
        // HAZE adapter. Return actual plan metrics and scalar boundaries.
        io.writeLine("optimized {plans.optimized.lines.length} {plans.optimized.totalCost}")
        for line in plans.optimized.lines {
            io.writeLine("{line.start} {line.trimmedEndExclusive} {line.trimmedTextWidth}")
        }
        io.writeLine("greedy {plans.greedy.lines.length} {plans.greedy.totalCost}")
        for line in plans.greedy.lines {
            io.writeLine("{line.start} {line.trimmedEndExclusive} {line.trimmedTextWidth}")
        }
    }
    case Err(error) => io.writeLine("{error}")
}
