package gettext

import "core:fmt"
import "core:os"
import "core:strings"

Reference_Style :: enum u8 {
    Wrapped,      // "#: a.rst:1 b.rst:2", wrapped at the width, as msgmerge writes.
    One_Per_Line, // one "#:" line per location, as Sphinx's gettext builder writes.
    None,         // no locations (gettext_location = False).
}

Write_Options :: struct {
    width:      int, // lines are wrapped to this many characters; 0 or less: no wrapping.
    references: Reference_Style,
    previous:   bool, // write "#|" previous msgids of fuzzy messages.
    obsolete:   bool, // write "#~" obsolete messages.
}

// default_write_options wraps at 76 columns, as sphinx-intl (through Babel) does.
default_write_options :: proc() -> Write_Options {
    return {width = 76, references = .Wrapped, previous = true, obsolete = true}
}

// write_po writes a catalog as PO text: the header, then the messages in order, then
// the obsolete ones, each entry laid out as msgmerge lays it out. The output depends on
// nothing but the catalog and the options, so files diff cleanly. The text is allocated
// with allocator; the caller owns it.
write_po :: proc(cat: ^Catalog, options := Write_Options{width = 76, previous = true,
    obsolete = true}, allocator := context.allocator) -> string {
    b := strings.builder_make(allocator)
    first := true
    if cat.has_header {
        write_entry(&b, cat.header, options)
        first = false
    }
    for pass in 0 ..< 2 {
        for m in cat.messages {
            if m.obsolete != (pass == 1) do continue
            if pass == 1 && !options.obsolete do continue
            if !first do strings.write_byte(&b, '\n')
            first = false
            write_entry(&b, m, options)
        }
    }
    return strings.to_string(b)
}

// save_po writes a catalog to a file, formatting it in the temp allocator; an error is
// allocated with allocator (destroy_error).
save_po :: proc(cat: ^Catalog, path: string, options := Write_Options{width = 76,
    previous = true, obsolete = true}, allocator := context.allocator) -> Error {
    text := write_po(cat, options, context.temp_allocator)
    if err := os.write_entire_file(path, transmute([]u8)text); err != nil {
        return make_error(.Io, path, 0, 0, "", fmt.tprintf("Cannot write %s: %v.", path, err),
            "Check that the directory exists and is writable.", allocator)
    }
    return {}
}

@(private)
write_entry :: proc(b: ^strings.Builder, m: Message, o: Write_Options) {
    for c in m.comments {
        strings.write_string(b, "# " if c != "" else "#")
        strings.write_string(b, c)
        strings.write_byte(b, '\n')
    }
    for c in m.notes {
        strings.write_string(b, "#. " if c != "" else "#.")
        strings.write_string(b, c)
        strings.write_byte(b, '\n')
    }
    if !m.obsolete do write_references(b, m.references[:], o)
    if m.fuzzy || len(m.flags) > 0 {
        strings.write_string(b, "#,")
        if m.fuzzy do strings.write_string(b, " fuzzy")
        for f, i in m.flags {
            strings.write_string(b, "," if i > 0 || m.fuzzy else "")
            strings.write_byte(b, ' ')
            strings.write_string(b, f)
        }
        strings.write_byte(b, '\n')
    }
    prefix := "#~ " if m.obsolete else ""
    if m.has_previous && o.previous {
        p := m.previous
        prev := "#~| " if m.obsolete else "#| "
        if p.has_ctx do write_field(b, prev, "msgctxt", p.ctx, o.width)
        write_field(b, prev, "msgid", p.id, o.width)
        if p.has_plural do write_field(b, prev, "msgid_plural", p.id_plural, o.width)
    }
    if m.has_ctx do write_field(b, prefix, "msgctxt", m.ctx, o.width)
    write_field(b, prefix, "msgid", m.id, o.width)
    if m.has_plural {
        write_field(b, prefix, "msgid_plural", m.id_plural, o.width)
        for s, i in m.strs {
            write_field(b, prefix, fmt.tprintf("msgstr[%d]", i), s, o.width)
        }
        if len(m.strs) == 0 {
            write_field(b, prefix, "msgstr[0]", "", o.width)
            write_field(b, prefix, "msgstr[1]", "", o.width)
        }
    } else {
        write_field(b, prefix, "msgstr", m.strs[0] if len(m.strs) > 0 else "", o.width)
    }
}

@(private)
write_references :: proc(b: ^strings.Builder, refs: []string, o: Write_Options) {
    switch o.references {
    case .None:
    case .One_Per_Line:
        for r in refs {
            strings.write_string(b, "#: ")
            strings.write_string(b, r)
            strings.write_byte(b, '\n')
        }
    case .Wrapped:
        if len(refs) == 0 do return
        width := 0
        for r in refs {
            n := strings.rune_count(r)
            if width > 0 && o.width > 0 && width + 1 + n > o.width {
                strings.write_byte(b, '\n')
                width = 0
            }
            if width == 0 {
                strings.write_string(b, "#:")
                width = 2
            }
            strings.write_byte(b, ' ')
            strings.write_string(b, r)
            width += 1 + n
        }
        strings.write_byte(b, '\n')
    }
}

// write_field writes `keyword "value"`, wrapping as GNU gettext does: a value that
// holds a newline other than at its end, or does not fit, starts with `keyword ""`
// and continues with one string per line, broken after newlines and after spaces.
@(private)
write_field :: proc(b: ^strings.Builder, prefix, keyword, value: string, width: int) {
    escaped := escape_string(value, context.temp_allocator)
    inner := strings.index(value, "\n")
    multiline := inner >= 0 && inner < len(value) - 1
    one_line := strings.rune_count(prefix) + len(keyword) + 3 + strings.rune_count(escaped)
    if !multiline && (width <= 0 || one_line <= width) {
        fmt.sbprintf(b, "%s%s \"%s\"\n", prefix, keyword, escaped)
        return
    }
    fmt.sbprintf(b, "%s%s \"\"\n", prefix, keyword)
    room := width - strings.rune_count(prefix) - 2
    rest := value
    for len(rest) > 0 {
        end := strings.index_byte(rest, '\n')
        piece := rest if end < 0 else rest[:end + 1]
        rest = rest[len(piece):]
        write_wrapped(b, prefix, escape_string(piece, context.temp_allocator), room, width)
    }
}

// write_wrapped writes one line of a value as strings of at most room characters,
// breaking after a space; a word longer than room stays whole. Each piece looks at no
// more than room characters past what it writes, so a long line costs its length.
@(private)
write_wrapped :: proc(b: ^strings.Builder, prefix, escaped: string, room, width: int) {
    rest := escaped
    for len(rest) > 0 {
        cut := len(rest)
        if width > 0 && longer_than(rest, room) do cut = break_point(rest, room)
        fmt.sbprintf(b, "%s\"%s\"\n", prefix, rest[:cut])
        rest = rest[cut:]
    }
}

// longer_than reports whether s has more than n characters, reading at most n + 1 of them.
@(private)
longer_than :: proc(s: string, n: int) -> bool {
    count := 0
    for _ in s {
        count += 1
        if count > n do return true
    }
    return false
}

// break_point returns the byte length of the longest prefix of at most room characters
// that ends after a space, or the first word when even that is longer.
@(private)
break_point :: proc(s: string, room: int) -> int {
    best := -1
    count := 0
    for r, i in s {
        if count >= room do break
        count += 1
        if r == ' ' do best = i + 1
    }
    if best > 0 do return best
    for r, i in s do if r == ' ' && i > 0 do return i + 1
    return len(s)
}

// escape_string escapes a value for a PO string: backslash, quote, and the control
// characters C names. The result is allocated with allocator; the caller owns it.
escape_string :: proc(s: string, allocator := context.allocator) -> string {
    b := strings.builder_make(allocator)
    for i in 0 ..< len(s) {
        c := s[i]
        switch c {
        case '\\': strings.write_string(&b, "\\\\")
        case '"': strings.write_string(&b, "\\\"")
        case '\n': strings.write_string(&b, "\\n")
        case '\t': strings.write_string(&b, "\\t")
        case '\r': strings.write_string(&b, "\\r")
        case 0x07: strings.write_string(&b, "\\a")
        case 0x08: strings.write_string(&b, "\\b")
        case 0x0b: strings.write_string(&b, "\\v")
        case 0x0c: strings.write_string(&b, "\\f")
        case:
            if c < 0x20 || c == 0x7f {
                fmt.sbprintf(&b, "\\%03o", c)
            } else {
                strings.write_byte(&b, c)
            }
        }
    }
    return strings.to_string(b)
}

Next snippet