const std = @import("std");
const dvui = @import("dvui");
const basic_models = @import("../../models/basic_models.zig");
const Document = @import("../../models/Document.zig");
const Point2_f = basic_models.Point2_f;
const Point2_i = basic_models.Point2_i;
const Scale2_f = basic_models.Scale2_f;
const Rect_i = basic_models.Rect_i;
const Color = dvui.Color;

/// Трансформ объекта: позиция, угол, масштаб, непрозрачность.
pub const Transform = struct {
    position: Point2_f = .{},
    angle: f32 = 0,
    scale: Scale2_f = .{},
    opacity: f32 = 1.0,

    pub fn init(obj: *const Document.Object) Transform {
        const pos = obj.getProperty(.position) orelse Point2_f{ .x = 0, .y = 0 };
        const angle = obj.getProperty(.angle) orelse 0;
        const scale = obj.getProperty(.scale) orelse Scale2_f{ .scale_x = 1, .scale_y = 1 };
        const opacity = obj.getProperty(.opacity) orelse 1.0;
        return .{
            .position = pos,
            .angle = angle,
            .scale = scale,
            .opacity = opacity,
        };
    }

    /// Композиция: world = parent * local.
    pub fn compose(parent: Transform, local: Transform) Transform {
        const cos_a = std.math.cos(parent.angle);
        const sin_a = std.math.sin(parent.angle);
        const sx = parent.scale.scale_x * local.scale.scale_x;
        const sy = parent.scale.scale_y * local.scale.scale_y;
        const local_px = local.position.x * parent.scale.scale_x;
        const local_py = local.position.y * parent.scale.scale_y;
        const rx = cos_a * local_px - sin_a * local_py;
        const ry = sin_a * local_px + cos_a * local_py;
        return .{
            .position = .{
                .x = parent.position.x + rx,
                .y = parent.position.y + ry,
            },
            .angle = parent.angle + local.angle,
            .scale = .{ .scale_x = sx, .scale_y = sy },
            .opacity = parent.opacity * local.opacity,
        };
    }
};

/// Мировые -> локальные для заданного трансформа.
pub fn worldToLocalTransform(t: Transform, wx: f32, wy: f32) Point2_f {
    const dx = wx - t.position.x;
    const dy = wy - t.position.y;
    const cos_a = std.math.cos(-t.angle);
    const sin_a = std.math.sin(-t.angle);
    const sx = if (t.scale.scale_x != 0) t.scale.scale_x else 1.0;
    const sy = if (t.scale.scale_y != 0) t.scale.scale_y else 1.0;
    return .{
        .x = (dx * cos_a - dy * sin_a) / sx,
        .y = (dx * sin_a + dy * cos_a) / sy,
    };
}

/// Конвейер отрисовки: локальные координаты -> трансформ -> буфер.
pub const DrawContext = struct {
    pixels: []Color.PMA,
    buf_width: u32,
    buf_height: u32,
    visible_rect: Rect_i,
    scale_x: f32,
    scale_y: f32,
    transform: Transform = .{},
    /// Если true, blendPixelAtBuffer перезаписывает пиксель без бленда
    replace_mode: bool = false,
    _fill_canvas: ?*FillCanvas = null,

    pub fn setTransform(self: *DrawContext, t: Transform) void {
        self.transform = t;
    }

    /// Локальные -> мировые.
    pub fn localToWorld(self: *const DrawContext, local_x: f32, local_y: f32) Point2_f {
        const t = &self.transform;
        const cos_a = std.math.cos(t.angle);
        const sin_a = std.math.sin(t.angle);
        return .{
            .x = t.position.x + (local_x * t.scale.scale_x) * cos_a - (local_y * t.scale.scale_y) * sin_a,
            .y = t.position.y + (local_x * t.scale.scale_x) * sin_a + (local_y * t.scale.scale_y) * cos_a,
        };
    }

    /// Мировые -> буфер (float).
    pub fn worldToBufferF(self: *const DrawContext, wx: f32, wy: f32) Point2_f {
        const canvas_x = wx * self.scale_x;
        const canvas_y = wy * self.scale_y;
        const vx = @as(f32, @floatFromInt(self.visible_rect.x));
        const vy = @as(f32, @floatFromInt(self.visible_rect.y));
        return .{
            .x = canvas_x - vx,
            .y = canvas_y - vy,
        };
    }

    /// Мировые -> буфер (целые).
    pub fn worldToBuffer(self: *const DrawContext, wx: f32, wy: f32) Point2_i {
        const b = self.worldToBufferF(wx, wy);
        return .{
            .x = @intFromFloat(std.math.round(b.x)),
            .y = @intFromFloat(std.math.round(b.y)),
        };
    }

    /// Буфер -> мировые.
    pub fn bufferToWorld(self: *const DrawContext, buf_x: f32, buf_y: f32) Point2_f {
        const vx = @as(f32, @floatFromInt(self.visible_rect.x));
        const vy = @as(f32, @floatFromInt(self.visible_rect.y));
        const canvas_x = buf_x + vx;
        const canvas_y = buf_y + vy;
        const sx = if (self.scale_x != 0) self.scale_x else 1.0;
        const sy = if (self.scale_y != 0) self.scale_y else 1.0;
        return .{
            .x = canvas_x / sx,
            .y = canvas_y / sy,
        };
    }

    /// Мировые -> локальные.
    pub fn worldToLocal(self: *const DrawContext, wx: f32, wy: f32) Point2_f {
        return worldToLocalTransform(self.transform, wx, wy);
    }

    /// Смешивает цвет в пикселе буфера с учётом opacity трансформа. В replace_mode просто перезаписывает пиксель.
    /// Если активен fill canvas, каждый записанный пиксель помечается как граница для заливки.
    pub fn blendPixelAtBuffer(self: *DrawContext, bx_i32: i32, by_i32: i32, color: Color.PMA) void {
        if (self._fill_canvas) |fc| fc.setBorder(bx_i32, by_i32);

        if (bx_i32 < 0 or by_i32 < 0 or bx_i32 >= self.buf_width or by_i32 >= self.buf_height) return;
        const bx: u32 = @intCast(bx_i32);
        const by: u32 = @intCast(by_i32);
        const idx = by * self.buf_width + bx;
        const dst = &self.pixels[idx];
        if (self.replace_mode) {
            dst.* = color;
            return;
        }
        const t = &self.transform;
        const a = @as(f32, @floatFromInt(color.a)) / 255.0 * t.opacity;
        const src_r = @as(f32, @floatFromInt(color.r)) * t.opacity;
        const src_g = @as(f32, @floatFromInt(color.g)) * t.opacity;
        const src_b = @as(f32, @floatFromInt(color.b)) * t.opacity;
        const inv_a = 1.0 - a;
        dst.r = @intFromFloat(std.math.clamp(src_r + inv_a * @as(f32, @floatFromInt(dst.r)), 0, 255));
        dst.g = @intFromFloat(std.math.clamp(src_g + inv_a * @as(f32, @floatFromInt(dst.g)), 0, 255));
        dst.b = @intFromFloat(std.math.clamp(src_b + inv_a * @as(f32, @floatFromInt(dst.b)), 0, 255));
        dst.a = @intFromFloat(std.math.clamp(a * 255 + inv_a * @as(f32, @floatFromInt(dst.a)), 0, 255));
    }

    /// Накладывает буфер другого контекста на этот с заданной прозрачностью (один бленд на пиксель). Размеры буферов должны совпадать.
    pub fn compositeDrawerContext(self: *DrawContext, other: *const DrawContext, opacity: f32) void {
        if (self.buf_width != other.buf_width or self.buf_height != other.buf_height) return;
        const n = self.buf_width * self.buf_height;
        for (0..n) |i| {
            const src = other.pixels[i];
            if (src.a == 0) continue;
            const dst = &self.pixels[i];
            const a = @as(f32, @floatFromInt(src.a)) / 255.0 * opacity;
            const src_r = @as(f32, @floatFromInt(src.r)) * opacity;
            const src_g = @as(f32, @floatFromInt(src.g)) * opacity;
            const src_b = @as(f32, @floatFromInt(src.b)) * opacity;
            const inv_a = 1.0 - a;
            dst.r = @intFromFloat(std.math.clamp(src_r + inv_a * @as(f32, @floatFromInt(dst.r)), 0, 255));
            dst.g = @intFromFloat(std.math.clamp(src_g + inv_a * @as(f32, @floatFromInt(dst.g)), 0, 255));
            dst.b = @intFromFloat(std.math.clamp(src_b + inv_a * @as(f32, @floatFromInt(dst.b)), 0, 255));
            dst.a = @intFromFloat(std.math.clamp(a * 255 + inv_a * @as(f32, @floatFromInt(dst.a)), 0, 255));
        }
    }

    /// Пиксель в локальных координатах (трансформ + PMA).
    pub fn blendPixelLocal(self: *DrawContext, local_x: f32, local_y: f32, color: Color.PMA) void {
        const w = self.localToWorld(local_x, local_y);
        const b = self.worldToBufferF(w.x, w.y);
        const bx: i32 = @intFromFloat(b.x);
        const by: i32 = @intFromFloat(b.y);
        const vw = @as(i32, @intCast(self.visible_rect.w));
        const vh = @as(i32, @intCast(self.visible_rect.h));
        if (bx < 0 or bx >= vw or by < 0 or by >= vh) return;
        self.blendPixelAtBuffer(bx, by, color);
    }

    /// Начинает сбор границ для заливки: создаёт FillCanvas и при последующих вызовах blendPixelAtBuffer помечает пиксели как границу.
    pub fn startFill(self: *DrawContext, allocator: std.mem.Allocator) !void {
        const fc = try FillCanvas.init(allocator, self.buf_width, self.buf_height);
        const ptr = try allocator.create(FillCanvas);
        ptr.* = fc;
        self._fill_canvas = ptr;
    }

    /// Рисует заливку по собранным границам цветом color, освобождает FillCanvas и сбрасывает режим.
    pub fn stopFill(self: *DrawContext, allocator: std.mem.Allocator, color: Color.PMA) void {
        const fc = self._fill_canvas orelse return;
        self._fill_canvas = null;
        fc.fillColor(self, allocator, color);
        fc.deinit();
        allocator.destroy(fc);
    }
};

/// Конвертирует u32 0xRRGGBBAA в Color.PMA.
pub fn rgbaToPma(rgba: u32) Color.PMA {
    const r: u8 = @intCast((rgba >> 24) & 0xFF);
    const g: u8 = @intCast((rgba >> 16) & 0xFF);
    const b: u8 = @intCast((rgba >> 8) & 0xFF);
    const a: u8 = @intCast((rgba >> 0) & 0xFF);
    if (a == 0) return .{ .r = 0, .g = 0, .b = 0, .a = 0 };
    const af: f32 = @as(f32, @floatFromInt(a)) / 255.0;
    return .{
        .r = @intFromFloat(@as(f32, @floatFromInt(r)) * af),
        .g = @intFromFloat(@as(f32, @floatFromInt(g)) * af),
        .b = @intFromFloat(@as(f32, @floatFromInt(b)) * af),
        .a = a,
    };
}

/// Контекст для заполнения фигур цветом. Границы хранятся в set — по x и y можно добавлять произвольные точки.
const FillCanvas = struct {
    /// Множество пикселей границы (x, y) — без ограничения по размеру буфера.
    border_set: std.AutoHashMap(Point2_i, void),
    buf_width: u32,
    buf_height: u32,

    pub fn init(allocator: std.mem.Allocator, width: u32, height: u32) !FillCanvas {
        const border_set = std.AutoHashMap(Point2_i, void).init(allocator);
        return .{
            .border_set = border_set,
            .buf_width = width,
            .buf_height = height,
        };
    }

    pub fn deinit(self: *FillCanvas) void {
        self.border_set.deinit();
    }

    /// Добавляет точку границы; координаты x, y могут быть любыми (условно бесконечное поле).
    pub fn setBorder(self: *FillCanvas, x: i32, y: i32) void {
        self.border_set.put(.{ .x = x, .y = y }, {}) catch {};
    }

    /// Заливка четырёхсвязным стековым алгоритмом от первой найденной внутренней точки.
    pub fn fillColor(self: *FillCanvas, draw_ctx: *DrawContext, allocator: std.mem.Allocator, color: Color.PMA) void {
        const n = self.border_set.count();
        if (n == 0) return;

        const buf_w_i: i32 = @intCast(self.buf_width);
        const buf_h_i: i32 = @intCast(self.buf_height);

        // Ключи один раз по (y, x) — по строкам x уже будут отсортированы.
        var keys_buf = std.ArrayList(Point2_i).empty;
        defer keys_buf.deinit(allocator);
        keys_buf.ensureTotalCapacity(allocator, n) catch return;
        var iter = self.border_set.keyIterator();
        while (iter.next()) |k| {
            keys_buf.appendAssumeCapacity(k.*);
        }
        std.mem.sort(Point2_i, keys_buf.items, {}, struct {
            fn lessThan(_: void, a: Point2_i, b: Point2_i) bool {
                if (a.y != b.y) return a.y < b.y;
                return a.x < b.x;
            }
        }.lessThan);

        // Семена: по строкам находим сегменты (пары x), пересекаем с окном буфера, берём середину сегмента.
        var seeds = findFillSeeds(keys_buf.items, buf_w_i, buf_h_i, allocator) catch return;
        defer seeds.deinit(allocator);

        var stack = std.ArrayList(Point2_i).empty;
        defer stack.deinit(allocator);
        var filled = std.AutoHashMap(Point2_i, void).init(allocator);
        defer filled.deinit();

        for (seeds.items) |s| {
            if (self.border_set.contains(s)) continue;
            if (filled.contains(s)) continue;
            stack.clearRetainingCapacity();
            stack.append(allocator, s) catch return;
            while (stack.pop()) |cell| {
                if (self.border_set.contains(cell)) continue;
                const gop = filled.getOrPut(cell) catch return;
                if (gop.found_existing) continue;

                if (cell.x >= 0 and cell.x < buf_w_i and cell.y >= 0 and cell.y < buf_h_i) {
                    draw_ctx.blendPixelAtBuffer(cell.x, cell.y, color);
                }
                if (cell.x > 0) stack.append(allocator, .{ .x = cell.x - 1, .y = cell.y }) catch return;
                if (cell.x < buf_w_i - 1) stack.append(allocator, .{ .x = cell.x + 1, .y = cell.y }) catch return;
                if (cell.y > 0) stack.append(allocator, .{ .x = cell.x, .y = cell.y - 1 }) catch return;
                if (cell.y < buf_h_i - 1) stack.append(allocator, .{ .x = cell.x, .y = cell.y + 1 }) catch return;
            }
        }
    }

    /// По строкам: рёбра (подряд идущие x) → сегменты между ними. Семена — середины чётных сегментов (при чётном числе границ).
    fn findFillSeeds(
        keys: []const Point2_i,
        buf_w_i: i32,
        buf_h_i: i32,
        allocator: std.mem.Allocator,
    ) !std.ArrayList(Point2_i) {
        var list = std.ArrayList(Point2_i).empty;
        errdefer list.deinit(allocator);
        var segments = std.ArrayList(struct { left: i32, right: i32 }).empty;
        defer segments.deinit(allocator);

        var i: usize = 0;
        while (i < keys.len) {
            const y = keys[i].y;
            const row_start = i;
            while (i < keys.len and keys[i].y == y) : (i += 1) {}
            const row = keys[row_start..i];
            if (row.len < 2 or y < 0 or y >= buf_h_i) continue;

            segments.clearRetainingCapacity();
            var run_end_x: i32 = row[0].x;
            for (row[1..]) |p| {
                if (p.x != run_end_x + 1) {
                    try segments.append(allocator, .{ .left = run_end_x + 1, .right = p.x - 1 });
                    run_end_x = p.x;
                } else {
                    run_end_x = p.x;
                }
            }
            // Семена только при чётном числе границ
            if ((segments.items.len + 1) % 2 != 0) continue;

            for (segments.items, 0..) |seg, gi| {
                if (gi % 2 != 0 or seg.left > seg.right) continue;
                const left = @max(seg.left, 0);
                const right = @min(seg.right, buf_w_i - 1);
                if (left <= right) {
                    try list.append(allocator, .{ .x = left + @divTrunc(right - left, 2), .y = y });
                }
            }
        }
        return list;
    }
};
