#!/usr/bin/env python3
"""quill — normalize and re-wrap plain text (paragraphs are where blank lines are)."""

import argparse
import sys
import unicodedata

VERSION = "quill 1.4.2"
CJK_RANGES = (
    (0x2E80, 0x9FFF), (0x3400, 0x4DBF), (0xF900, 0xFAFF),
    (0xFF00, 0xFF60), (0x3040, 0x30FF), (0xAC00, 0xD7AF),
)

def is_cjk(ch):
    cp = ord(ch)
    return any(lo <= cp <= hi for lo, hi in CJK_RANGES)

def atoms(text):
    """Split text into unbreakable atoms: space-run separated words, or
    single CJK characters (the 1.4.2 fix: CJK wraps without spaces)."""
    out = []
    word = ""
    for ch in text:
        if is_cjk(ch):
            if word:
                out.append(word)
                word = ""
            out.append(ch)
        elif ch == " ":
            if word:
                out.append(word)
                word = ""
            out.append(" ")
        else:
            word += ch
    if word:
        out.append(word)
    return out

def wrap_text(text, width):
    """Greedy wrap over atoms; width >= 20 enforced by the parser."""
    lines, cur, cur_len = [], [], 0
    for a in atoms(text):
        alen = len(a)
        if a == " ":
            if cur:
                cur.append(" ")
                cur_len += 1
            continue
        if cur and cur_len + alen > width:
            while cur and cur[-1] == " ":
                cur.pop()
            lines.append("".join(cur))
            cur, cur_len = [], 0
        if alen > width:
            # hard-split an unbreakable atom (long URL) at the width edge
            while alen > width:
                take = width - cur_len if cur else width
                cur.append(a[:take])
                lines.append("".join(cur))
                a = a[take:]
                alen -= take
                cur, cur_len = [], 0
        cur.append(a)
        cur_len += alen
    while cur and cur[-1] == " ":
        cur.pop()
    if cur:
        lines.append("".join(cur))
    return lines

def paragraphs(lines, join_blank):
    para, paras = [], []
    for ln in lines:
        if ln.strip() == "":
            if para:
                paras.append(para)
                para = []
        else:
            para.append(ln)
    if para:
        paras.append(para)
    if join_blank and paras:
        return [sum(paras, [])]
    return paras

def run(lines, width, join_blank, reflow):
    out = []
    for para in paragraphs(lines, join_blank):
        if reflow:
            text = " ".join(" ".join(s.split()) for s in para)
            out.extend(wrap_text(text, width))
        else:
            for s in para:
                out.append(" ".join(s.split()))
        out.append("")
    while out and out[-1] == "":
        out.pop()
    return "\n".join(out) + ("\n" if out else "")

def main(argv=None):
    ap = argparse.ArgumentParser(prog="quill", add_help=True,
        description="Normalize and re-wrap plain text.")
    ap.add_argument("-w", "--width", type=int, default=72)
    ap.add_argument("--join-blank", action="store_true")
    ap.add_argument("--no-reflow", action="store_true")
    ap.add_argument("--version", action="version", version=VERSION)
    ap.add_argument("files", nargs="*")
    ns = ap.parse_args(argv)
    if ns.width < 20:
        print("quill: width must be >= 20", file=sys.stderr)
        return 2
    if ns.files:
        try:
            text = "".join(open(f, encoding="utf-8", errors="replace").read() for f in ns.files)
        except OSError as e:
            print(f"quill: {e}", file=sys.stderr)
            return 1
    else:
        text = sys.stdin.read()
    sys.stdout.write(run(text.splitlines(), ns.width, ns.join_blank, not ns.no_reflow))
    return 0

if __name__ == "__main__":
    sys.exit(main())
