const std = @import("std");
const Span = @import("ast.zig").Span;

pub const Level = enum {
    err,
    warn,
    note,
};

pub const SourceLoc = struct {
    line: u32,
    col: u32,

    pub fn compute(source: []const u8, byte_offset: u32) SourceLoc {
        var line: u32 = 1;
        var col: u32 = 1;
        const end = @min(byte_offset, @as(u32, @intCast(source.len)));
        for (source[0..end]) |c| {
            if (c == '\n') {
                line += 1;
                col = 1;
            } else {
                col += 1;
            }
        }
        return .{ .line = line, .col = col };
    }
};

pub const Diagnostic = struct {
    level: Level,
    message: []const u8,
    span: ?Span,
    source_file: ?[]const u8 = null,
};

pub const DiagnosticList = struct {
    items: std.ArrayList(Diagnostic) = .empty,
    allocator: std.mem.Allocator,
    source: []const u8,
    file_name: []const u8,
    current_source_file: ?[]const u8 = null,
    import_sources: ?*const std.StringHashMap([:0]const u8) = null,

    pub fn init(allocator: std.mem.Allocator, source: []const u8, file_name: []const u8) DiagnosticList {
        return .{
            .allocator = allocator,
            .source = source,
            .file_name = file_name,
        };
    }

    pub fn deinit(self: *DiagnosticList) void {
        self.items.deinit(self.allocator);
    }

    pub fn add(self: *DiagnosticList, level: Level, message: []const u8, span: ?Span) void {
        // Deduplicate: skip if same level+span+message already exists
        for (self.items.items) |d| {
            if (d.level == level and std.mem.eql(u8, d.message, message)) {
                const a = d.span orelse continue;
                const b = span orelse continue;
                if (a.start == b.start and a.end == b.end) return;
            }
        }
        self.items.append(self.allocator, .{
            .level = level,
            .message = message,
            .span = span,
            .source_file = self.current_source_file,
        }) catch {};
    }

    pub fn addFmt(self: *DiagnosticList, level: Level, span: ?Span, comptime fmt: []const u8, args: anytype) void {
        const message = std.fmt.allocPrint(self.allocator, fmt, args) catch "diagnostic format error";
        self.add(level, message, span);
    }

    pub fn hasErrors(self: *const DiagnosticList) bool {
        for (self.items.items) |d| {
            if (d.level == .err) return true;
        }
        return false;
    }

    fn resolveSourceAndFile(self: *const DiagnosticList, d: Diagnostic) struct { source: []const u8, file_name: []const u8 } {
        if (d.source_file) |sf| {
            if (self.import_sources) |is| {
                if (is.get(sf)) |src| {
                    return .{ .source = src, .file_name = sf };
                }
            }
        }
        return .{ .source = self.source, .file_name = self.file_name };
    }

    pub fn render(self: *const DiagnosticList, writer: anytype) !void {
        for (self.items.items) |d| {
            const level_str = switch (d.level) {
                .err => "error",
                .warn => "warning",
                .note => "note",
            };
            if (d.span) |span| {
                const resolved = self.resolveSourceAndFile(d);
                const loc = SourceLoc.compute(resolved.source, span.start);
                try writer.print("{s}:{d}:{d}: {s}: {s}\n", .{ resolved.file_name, loc.line, loc.col, level_str, d.message });
            } else {
                try writer.print("{s}: {s}: {s}\n", .{ self.file_name, level_str, d.message });
            }
        }
    }

    pub fn renderDebug(self: *const DiagnosticList) void {
        for (self.items.items) |d| {
            const level_str = switch (d.level) {
                .err => "error",
                .warn => "warning",
                .note => "note",
            };
            if (d.span) |span| {
                const resolved = self.resolveSourceAndFile(d);
                const loc = SourceLoc.compute(resolved.source, span.start);
                std.debug.print("{s}:{d}:{d}: {s}: {s}\n", .{ resolved.file_name, loc.line, loc.col, level_str, d.message });
            } else {
                std.debug.print("{s}: {s}: {s}\n", .{ self.file_name, level_str, d.message });
            }
        }
    }
};
