const std = @import("std");
const basic_models = @import("basic_models.zig");
const Size_f = basic_models.Size_f;
const Document = @This();

pub const Object = @import("Object.zig");
const shape = @import("shape/shape.zig");

size: Size_f,
objects: std.ArrayList(Object),
next_object_id: u64,

pub fn init(size: Size_f) Document {
    return .{
        .size = size,
        .objects = std.ArrayList(Object).empty,
        .next_object_id = 1,
    };
}

pub fn deinit(self: *Document, allocator: std.mem.Allocator) void {
    for (self.objects.items) |*obj| obj.deinit(allocator);
    self.objects.deinit(allocator);
}

pub fn clone(self: *const Document, allocator: std.mem.Allocator) !Document {
    var objects_list = std.ArrayList(Object).empty;
    errdefer {
        for (objects_list.items) |*obj| obj.deinit(allocator);
        objects_list.deinit(allocator);
    }
    var next_id = self.next_object_id;
    for (self.objects.items) |obj| {
        try objects_list.append(allocator, try obj.clone(allocator, &next_id));
    }
    return .{
        .size = self.size,
        .objects = objects_list,
        .next_object_id = next_id,
    };
}

pub fn addObject(self: *Document, allocator: std.mem.Allocator, template: Object) !void {
    const obj = try template.clone(allocator, &self.next_object_id);
    try self.objects.append(allocator, obj);
}

/// Добавляет объект в документ: как ребёнка родителя (если id найден), иначе в корень.
pub fn addObjectUnderParentId(self: *Document, allocator: std.mem.Allocator, parent_id: ?u64, template: Object) !void {
    if (parent_id) |id| {
        if (self.findObjectById(id)) |parent| {
            try parent.addChild(allocator, template, &self.next_object_id);
            return;
        }
    }
    try self.addObject(allocator, template);
}

pub fn addShape(self: *Document, allocator: std.mem.Allocator, parent: ?*Object, shape_kind: Object.ShapeKind) !void {
    const obj = try shape.createObject(allocator, shape_kind);
    if (parent) |p| {
        try p.addChild(allocator, obj, &self.next_object_id);
    } else {
        try self.addObject(allocator, obj);
    }
}

/// Удаляет объект из документа (из корня или из детей родителя). Возвращает true, если объект был найден и удалён.
pub fn removeObject(self: *Document, allocator: std.mem.Allocator, obj: *Object) bool {
    for (self.objects.items, 0..) |*item, i| {
        if (item == obj) {
            var removed = self.objects.orderedRemove(i);
            removed.deinit(allocator);
            return true;
        }
        if (removeFromChildren(allocator, &item.children, obj)) return true;
    }
    return false;
}

/// Удаляет объект по id. Возвращает true, если объект был найден и удалён.
pub fn removeObjectById(self: *Document, allocator: std.mem.Allocator, obj_id: u64) bool {
    for (self.objects.items, 0..) |*item, i| {
        if (item.id == obj_id) {
            var removed = self.objects.orderedRemove(i);
            removed.deinit(allocator);
            return true;
        }
        if (removeFromChildrenById(allocator, &item.children, obj_id)) return true;
    }
    return false;
}

pub fn findObjectById(self: *Document, obj_id: u64) ?*Object {
    for (self.objects.items) |*item| {
        if (item.id == obj_id) return item;
        if (findInChildrenById(&item.children, obj_id)) |found| return found;
    }
    return null;
}

fn removeFromChildren(allocator: std.mem.Allocator, children: *std.ArrayList(Object), obj: *Object) bool {
    for (children.items, 0..) |*item, i| {
        if (item == obj) {
            var removed = children.orderedRemove(i);
            removed.deinit(allocator);
            return true;
        }
        if (removeFromChildren(allocator, &item.children, obj)) return true;
    }
    return false;
}

fn removeFromChildrenById(allocator: std.mem.Allocator, children: *std.ArrayList(Object), obj_id: u64) bool {
    for (children.items, 0..) |*item, i| {
        if (item.id == obj_id) {
            var removed = children.orderedRemove(i);
            removed.deinit(allocator);
            return true;
        }
        if (removeFromChildrenById(allocator, &item.children, obj_id)) return true;
    }
    return false;
}

fn findInChildrenById(children: *std.ArrayList(Object), obj_id: u64) ?*Object {
    for (children.items) |*item| {
        if (item.id == obj_id) return item;
        if (findInChildrenById(&item.children, obj_id)) |found| return found;
    }
    return null;
}
