const std = @import("std");

pub fn main(init: std.process.Init) !void {
var args_iter = std.process.Args.Iterator.init(init.minimal.args);
_ = args_iter.skip();

const decimals: usize = if (args_iter.next()) |arg_str|
std.fmt.parseInt(usize, arg_str, 10) catch 100
else
100;

const pi = try calculatePi(init.gpa, decimals);
defer init.gpa.free(pi);

try std.Io.File.writeStreamingAll(.stdout(), init.io, pi);
try std.Io.File.writeStreamingAll(.stdout(), init.io, "\n");
}

// Simple BigInt using array of u32 (base 10^9)
const BigInt = struct {
digits: []u32,
len: usize,
allocator: std.mem.Allocator,

fn init(allocator: std.mem.Allocator, size: usize) !BigInt {
const digits = try allocator.alloc(u32, size);
@memset(digits, 0);
return .{ .digits = digits, .len = 1, .allocator = allocator };
}

fn deinit(self: *BigInt) void {
self.allocator.free(self.digits);
}

fn set(self: *BigInt, value: u32) void {
@memset(self.digits, 0);
self.digits[0] = value;
self.len = 1;
}

fn copyFrom(self: *BigInt, other: *const BigInt) void {
@memcpy(self.digits[0..other.len], other.digits[0..other.len]);
self.len = other.len;
}

fn add(self: *BigInt, other: *const BigInt) void {
var carry: u32 = 0;
var i: usize = 0;
const max_len = @max(self.len, other.len);

while (i < max_len or carry > 0) : (i += 1) {
const a = if (i < self.len) self.digits[i] else 0;
const b = if (i < other.len) other.digits[i] else 0;
const sum = a + b + carry;
self.digits[i] = sum % 1000000000;
carry = sum / 1000000000;
}
self.len = i;
}

fn sub(self: *BigInt, other: *const BigInt) void {
var borrow: i32 = 0;
var i: usize = 0;

while (i < self.len) : (i += 1) {
const a: i64 = @as(i64, self.digits[i]);
const b: i64 = if (i < other.len) @as(i64, other.digits[i]) else 0;
var diff = a - b - borrow;
if (diff < 0) {
diff += 1000000000;
borrow = 1;
} else {
borrow = 0;
}
self.digits[i] = @as(u32, @intCast(diff));
}

while (self.len > 1 and self.digits[self.len - 1] == 0) {
self.len -= 1;
}
}

fn mulSmall(self: *BigInt, multiplier: u32) void {
var carry: u64 = 0;
var i: usize = 0;

while (i < self.len or carry > 0) : (i += 1) {
const prod = @as(u64, self.digits[i]) * @as(u64, multiplier) + carry;
self.digits[i] = @as(u32, @intCast(prod % 1000000000));
carry = prod / 1000000000;
}
self.len = i;
}

fn divSmall(self: *BigInt, divisor: u32) void {
var remainder: u64 = 0;
var i: usize = self.len;

while (i > 0) {
i -= 1;
const cur = remainder * 1000000000 + @as(u64, self.digits[i]);
self.digits[i] = @as(u32, @intCast(cur / @as(u64, divisor)));
remainder = cur % @as(u64, divisor);
}

while (self.len > 1 and self.digits[self.len - 1] == 0) {
self.len -= 1;
}
}

fn isZero(self: *const BigInt) bool {
return self.len == 1 and self.digits[0] == 0;
}
};

fn calculatePi(allocator: std.mem.Allocator, decimals: usize) ![]u8 {
const num_digits = (decimals + 10) / 9 + 10;

const atan1_5 = try arctanBigInt(allocator, 5, decimals, num_digits);
defer allocator.free(atan1_5);

const atan1_239 = try arctanBigInt(allocator, 239, decimals, num_digits);
defer allocator.free(atan1_239);

return formatPiBigInt(allocator, atan1_5, atan1_239, decimals, num_digits);
}

fn arctanBigInt(allocator: std.mem.Allocator, x: u32, decimals: usize, num_digits: usize) ![]u32 {
var term = try BigInt.init(allocator, num_digits);
defer term.deinit();

var result = try BigInt.init(allocator, num_digits);
defer result.deinit();

var temp = try BigInt.init(allocator, num_digits);
defer temp.deinit();

term.set(1);
var i: usize = 0;
while (i < decimals + 10) : (i += 9) {
term.mulSmall(1000000000);
}
term.divSmall(x);

var n: usize = 0;
const x_squared = x * x;

while (!term.isZero() and n < decimals * 2) : (n += 1) {
const divisor: u32 = @intCast(2 * n + 1);

temp.copyFrom(&term);
temp.divSmall(divisor);

if (n % 2 == 0) {
result.add(&temp);
} else {
result.sub(&temp);
}

term.divSmall(x_squared);
}

const output = try allocator.alloc(u32, result.len);
@memcpy(output, result.digits[0..result.len]);
return output;
}

fn formatPiBigInt(allocator: std.mem.Allocator, atan1_5: []const u32, atan1_239: []const u32, decimals: usize, num_digits: usize) ![]u8 {
var result_big = try BigInt.init(allocator, num_digits);
defer result_big.deinit();

var temp = try BigInt.init(allocator, num_digits);
defer temp.deinit();

@memcpy(result_big.digits[0..atan1_5.len], atan1_5[0..atan1_5.len]);
result_big.len = atan1_5.len;
result_big.mulSmall(16);

@memcpy(temp.digits[0..atan1_239.len], atan1_239[0..atan1_239.len]);
temp.len = atan1_239.len;
temp.mulSmall(4);

result_big.sub(&temp);

return formatBigInt(allocator, &result_big, decimals);
}

fn formatBigInt(allocator: std.mem.Allocator, num: *BigInt, decimals: usize) ![]u8 {
var result = try allocator.alloc(u8, decimals + 2);

const first_digit = num.digits[num.len - 1];
var temp_buf: [20]u8 = undefined;
const first_str = try std.fmt.bufPrint(&temp_buf, "{}", .{first_digit});

if (first_str.len > 0) {
result[0] = first_str[0];
} else {
result[0] = 48;
}

result[1] = 46;

var pos: usize = 2;
if (first_str.len > 1) {
var j: usize = 1;
while (j < first_str.len and pos < decimals + 2) : (j += 1) {
result[pos] = first_str[j];
pos += 1;
}
}

if (num.len > 1) {
var i: usize = num.len - 2;
while (true) : (i -= 1) {
const group = num.digits[i];
const group_str = try std.fmt.bufPrint(&temp_buf, "{d:09}", .{group});

var j: usize = 0;
while (j < group_str.len and pos < decimals + 2) : (j += 1) {
result[pos] = group_str[j];
pos += 1;
}

if (i == 0) break;
}
}

while (pos < decimals + 2) : (pos += 1) {
result[pos] = 48;
}

return result;
}
