const std = @import("std");
const expectEqualStrings = std.testing.expectEqualStrings;

pub fn fromPascalCase(allocator: *std.mem.Allocator, name: []const u8) ![]u8 {
    var utf8_name = (std.unicode.Utf8View.init(name) catch unreachable).iterator();
    var target_inx: u64 = 0;
    var previous_codepoint: ?u21 = null;
    var cp = utf8_name.nextCodepoint();
    if (cp == null) {
        return try allocator.dupeZ(u8, name);
    } // TODO: fix bug if single letter uppercase
    var codepoint = cp.?;
    const rc = try allocator.alloc(u8, name.len * 2); // This is overkill, but is > the maximum length possibly needed
    while (utf8_name.nextCodepoint()) |next_codepoint| {
        if (codepoint > 0xff) return error{UnicodeNotSupported}.UnicodeNotSupported;
        if (next_codepoint > 0xff) return error{UnicodeNotSupported}.UnicodeNotSupported;
        const ascii_char = @truncate(u8, codepoint);
        if (ascii_char >= 'A' and ascii_char < 'Z') {
            const lowercase_char = ascii_char + ('a' - 'A');
            if (previous_codepoint == null) {
                rc[target_inx] = lowercase_char;
                target_inx = target_inx + 1;
            } else {
                if (next_codepoint >= 'A' and next_codepoint <= 'Z' and previous_codepoint.? >= 'A' and previous_codepoint.? <= 'Z') {
                    //we are in an acronym - don't snake, just lower
                    rc[target_inx] = lowercase_char;
                    target_inx = target_inx + 1;
                } else {
                    rc[target_inx] = '_';
                    rc[target_inx + 1] = lowercase_char;
                    target_inx = target_inx + 2;
                }
            }
        } else {
            rc[target_inx] = ascii_char;
            target_inx = target_inx + 1;
        }
        previous_codepoint = codepoint;
        codepoint = next_codepoint;
    }
    // work in the last codepoint - force lowercase
    rc[target_inx] = @truncate(u8, codepoint);
    if (rc[target_inx] >= 'A' and rc[target_inx] <= 'Z') {
        const lowercase_char = rc[target_inx] + ('a' - 'A');
        rc[target_inx] = lowercase_char;
    }
    target_inx = target_inx + 1;

    rc[target_inx] = 0;
    return rc[0..target_inx];
}

test "converts from PascalCase to snake_case" {
    const allocator = std.testing.allocator;
    const snake_case = try fromPascalCase(allocator, "MyPascalCaseThing");
    defer allocator.free(snake_case);
    expectEqualStrings("my_pascal_case_thing", snake_case);
}
test "handles from PascalCase acronyms to snake_case" {
    const allocator = std.testing.allocator;
    const snake_case = try fromPascalCase(allocator, "SAMLMySAMLAcronymThing");
    defer allocator.free(snake_case);
    expectEqualStrings("saml_my_saml_acronym_thing", snake_case);
}
