const std = @import("std");
const Document = @import("../../models/Document.zig");
const pipeline = @import("pipeline.zig");
const DrawContext = pipeline.DrawContext;
const Color = @import("dvui").Color;
const base_models = @import("../../models/basic_models.zig");
const Point2_i = base_models.Point2_i;

const Object = Document.Object;
const default_stroke: Color.PMA = .{ .r = 0, .g = 0, .b = 0, .a = 255 };
const default_thickness: f32 = 2.0;

/// Линия от (0,0) до end_point.
pub fn draw(ctx: *DrawContext, obj: *const Object) void {
    const end_point = obj.getProperty(.end_point) orelse return;
    const end_x = end_point.x;
    const end_y = end_point.y;
    const stroke = if (obj.getProperty(.stroke_rgba)) |stroke_rgba| pipeline.rgbaToPma(stroke_rgba) else default_stroke;
    const thickness = obj.getProperty(.thickness) orelse default_thickness;
    drawLine(ctx, 0, 0, end_x, end_y, stroke, thickness, false);
}

/// Рисует отрезок по локальным концам (перевод в буфер внутри).
/// draw_when_outside: если true, участки линии за экраном тоже рисуются (толщиной 1 px); в буфере — обычная толщина.
pub fn drawLine(ctx: *DrawContext, x0: f32, y0: f32, x1: f32, y1: f32, color: Color.PMA, thickness: f32, draw_when_outside: bool) void {
    const w0 = ctx.localToWorld(x0, y0);
    const w1 = ctx.localToWorld(x1, y1);
    const b0 = ctx.worldToBuffer(w0.x, w0.y);
    const b1 = ctx.worldToBuffer(w1.x, w1.y);
    const t = &ctx.transform;
    const scale = @sqrt(t.scale.scale_x * ctx.scale_x * t.scale.scale_y * ctx.scale_y);
    const thickness_px: u32 = @as(u32, @intFromFloat(std.math.round(thickness * scale)));
    if (thickness_px > 0)
        drawLineInBuffer(ctx, b0.x, b0.y, b1.x, b1.y, color, thickness_px, draw_when_outside);
}

inline fn clip(p: f32, q: f32, t0: *f32, t1: *f32) bool {
    if (p == 0) {
        return q >= 0;
    }
    const r = q / p;
    if (p < 0) {
        if (r > t1.*) return false;
        if (r > t0.*) t0.* = r;
    } else {
        if (r < t0.*) return false;
        if (r < t1.*) t1.* = r;
    }
    return true;
}

/// Liang–Barsky отсечение отрезка (x0,y0)-(x1,y1) прямоугольником [left,right]x[top,bottom].
/// Координаты концов модифицируются по месту. Возвращает false, если отрезок целиком вне прямоугольника.
fn liangBarskyClip(
    x0: *i32,
    y0: *i32,
    x1: *i32,
    y1: *i32,
    left: i32,
    top: i32,
    right: i32,
    bottom: i32,
) bool {
    const fx0: f32 = @floatFromInt(x0.*);
    const fy0: f32 = @floatFromInt(y0.*);
    const fx1: f32 = @floatFromInt(x1.*);
    const fy1: f32 = @floatFromInt(y1.*);

    const dx = fx1 - fx0;
    const dy = fy1 - fy0;

    var t0: f32 = 0.0;
    var t1: f32 = 1.0;

    const fl: f32 = @floatFromInt(left);
    const ft: f32 = @floatFromInt(top);
    const fr: f32 = @floatFromInt(right);
    const fb: f32 = @floatFromInt(bottom);

    if (!clip(-dx, fx0 - fl, &t0, &t1)) return false; // x >= left
    if (!clip(dx, fr - fx0, &t0, &t1)) return false; // x <= right
    if (!clip(-dy, fy0 - ft, &t0, &t1)) return false; // y >= top
    if (!clip(dy, fb - fy0, &t0, &t1)) return false; // y <= bottom

    const nx0 = fx0 + dx * t0;
    const ny0 = fy0 + dy * t0;
    const nx1 = fx0 + dx * t1;
    const ny1 = fy0 + dy * t1;

    x0.* = @intFromFloat(std.math.round(nx0));
    y0.* = @intFromFloat(std.math.round(ny0));
    x1.* = @intFromFloat(std.math.round(nx1));
    y1.* = @intFromFloat(std.math.round(ny1));

    return true;
}

/// Отсекает отрезок буфером ctx (0..buf_width-1, 0..buf_height-1).
fn clipLineToBuffer(ctx: *DrawContext, a: *Point2_i, b: *Point2_i, thickness: i32) bool {
    var x0 = a.x;
    var y0 = a.y;
    var x1 = b.x;
    var y1 = b.y;

    const left: i32 = -thickness;
    const top: i32 = -thickness;
    const right: i32 = @as(i32, @intCast(ctx.buf_width - 1)) + thickness;
    const bottom: i32 = @as(i32, @intCast(ctx.buf_height - 1)) + thickness;

    if (!liangBarskyClip(&x0, &y0, &x1, &y1, left, top, right, bottom)) {
        return false;
    }

    a.* = .{ .x = x0, .y = y0 };
    b.* = .{ .x = x1, .y = y1 };
    return true;
}

fn drawLineInBuffer(ctx: *DrawContext, bx0: i32, by0: i32, bx1: i32, by1: i32, color: Color.PMA, thickness_px: u32, draw_when_outside: bool) void {
    // Коррекция толщины в зависимости от угла линии.
    var thickness_corrected: u32 = thickness_px;
    var use_vertical: bool = undefined;
    const dx_f: f32 = @floatFromInt(bx1 - bx0);
    const dy_f: f32 = @floatFromInt(by1 - by0);
    const len: f32 = @sqrt(dx_f * dx_f + dy_f * dy_f);
    if (len > 0) {
        const cos_theta = @abs(dx_f) / len;
        const sin_theta = @abs(dy_f) / len;
        const desired: f32 = @floatFromInt(thickness_px);
        const eps: f32 = 1e-3;

        const vertical_based = desired / @max(sin_theta, eps);
        const horizontal_based = desired / @max(cos_theta, eps);

        use_vertical = sin_theta >= cos_theta;
        const corrected_f = if (use_vertical) vertical_based else horizontal_based;

        thickness_corrected = @max(@as(u32, 1), @as(u32, @intFromFloat(std.math.round(corrected_f))));
    }
    const half_thickness: i32 = @intCast(thickness_corrected / 2);
    const thickness_corrected_i: i32 = @as(i32, @intCast(thickness_corrected));

    var p0 = Point2_i{ .x = bx0, .y = by0 };
    var p1 = Point2_i{ .x = bx1, .y = by1 };

    // Отсечение только когда не рисуем вне viewport: иначе линия идёт целиком, толщина 1 px снаружи.
    if (!draw_when_outside) {
        if (!clipLineToBuffer(ctx, &p0, &p1, @as(i32, @intCast(thickness_corrected)))) return;
    }

    var x0 = p0.x;
    var y0 = p0.y;
    const ex = p1.x;
    const ey = p1.y;

    const dx: i32 = @intCast(@abs(ex - x0));
    const sx: i32 = if (x0 < ex) 1 else -1;
    const dy_abs: i32 = @intCast(@abs(ey - y0));
    const dy: i32 = -dy_abs;
    const sy: i32 = if (y0 < ey) 1 else -1;

    var err: i32 = dx + dy;
    const buf_w_i: i32 = @intCast(ctx.buf_width);
    const buf_h_i: i32 = @intCast(ctx.buf_height);

    while (true) {
        // Внутри viewport — полная толщина; снаружи при draw_when_outside — только 1 пиксель.
        const in_viewport = x0 >= -thickness_corrected_i and x0 < buf_w_i + thickness_corrected_i and y0 >= -thickness_corrected_i and y0 < buf_h_i + thickness_corrected_i;
        const effective_half: i32 = if (draw_when_outside and !in_viewport) 0 else half_thickness;

        var thick: i32 = -effective_half;
        while (thick <= effective_half) {
            const x = if (use_vertical) x0 + thick else x0;
            const y = if (use_vertical) y0 else y0 + thick;
            ctx.blendPixelAtBuffer(x, y, color);

            thick += 1;
        }

        if (x0 == ex and y0 == ey) break;

        const e2: i32 = 2 * err;
        if (e2 >= dy) {
            err += dy;
            x0 += sx;
        }
        if (e2 <= dx) {
            err += dx;
            y0 += sy;
        }
    }
}
