//! A texture held by the backend.  Can be drawn with `dvui.renderTexture`.

ptr: *anyopaque,
width: u32,
height: u32,
format: TexturePixelFormat,
interpolation: TextureInterpolation,
wrap_u: dvui.enums.TextureWrap,
wrap_v: dvui.enums.TextureWrap,

const Texture = @This();

pub const CreateOptions = struct {
    width: u32,
    height: u32,
    format: TexturePixelFormat = .rgba_32,
    interpolation: TextureInterpolation = .linear,
    wrap_u: dvui.enums.TextureWrap = .clamp,
    wrap_v: dvui.enums.TextureWrap = .clamp,
};

/// A texture held by the backend that can be drawn onto.  See `dvui.Picture`.
pub const Target = struct {
    ptr: *anyopaque,
    width: u32,
    height: u32,
    format: TexturePixelFormat,
    interpolation: TextureInterpolation,
    wrap_u: dvui.enums.TextureWrap,
    wrap_v: dvui.enums.TextureWrap,

    /// Create a texture that can be rendered with `renderTexture` and drawn to
    /// with `renderTarget`.  Starts transparent (all zero).
    ///
    /// Remember to destroy the texture at some point, see `destroyLater`.
    ///
    /// Only valid between `Window.begin`and `Window.end`.
    pub fn create(options: CreateOptions) TextureError!Target {
        return try dvui.currentWindow().backend.textureCreateTarget(options);
    }

    /// Destroy target from `Target.create` at the end of the frame.
    ///
    /// While `Backend.textureDestroyTarget` immediately destroys the texture, this
    /// function deferres the destruction until the end of the frame, so it is safe
    /// to use even in a subwindow where rendering is deferred.
    ///
    /// Only valid between `Window.begin`and `Window.end`.
    pub fn destroyLater(self: Target) void {
        const cw = dvui.currentWindow();
        cw.texture_cache.trash_target.append(cw.gpa, self) catch |err| {
            dvui.log.err("Texture.Target.destroyLater got {any}\n", .{err});
        };
    }

    /// Clear to transparent (all zero).
    ///
    /// Only valid between `Window.begin`and `Window.end`.
    pub fn clear(self: Target) void {
        dvui.currentWindow().backend.textureClearTarget(self);
    }

    pub fn cast(t: Texture) Target {
        return .{ .ptr = t.ptr, .width = t.width, .height = t.height, .format = t.format, .interpolation = t.interpolation, .wrap_u = t.wrap_u, .wrap_v = t.wrap_v };
    }
};

pub const Cache = struct {
    cache: Storage = .empty,
    /// Used to defer destroying textures until the next call to `reset` or `deinit`
    trash: Trash = .empty,
    trash_target: std.ArrayList(dvui.Texture.Target) = .empty,

    pub const Storage = dvui.TrackingAutoHashMap(Key, Texture, .get_and_put, dvui.data.Token);
    pub const Trash = std.ArrayList(dvui.Texture);

    pub const Key = u64;

    pub fn get(self: *Cache, key: Key) ?Texture {
        return self.cache.get(key);
    }

    pub fn remove(self: *Cache, key: Key) bool {
        return self.cache.remove(key);
    }

    /// Add a texture to the cache. This is useful if you want to load
    /// and image from disk, create a texture from it and then unload
    /// it from memory. The texture will remain in the cache as long
    /// as it's key is accessed at least once per call to `reset`.
    pub fn add(self: *Cache, gpa: std.mem.Allocator, key: Key, texture: Texture) std.mem.Allocator.Error!void {
        try self.trash.ensureUnusedCapacity(gpa, 1);
        const prev = try self.cache.fetchPut(gpa, key, texture);
        if (prev) |kv| {
            self.trash.appendAssumeCapacity(kv.value);
        }
    }

    pub fn retain(self: *Cache, gpa: std.mem.Allocator, key: Key, retain_token: ?dvui.data.Token) std.mem.Allocator.Error!void {
        try self.cache.retain(gpa, key, retain_token);
    }

    pub fn retainClear(self: *Cache, retain_token: dvui.data.Token) void {
        self.cache.retainClear(retain_token);
    }

    /// Remove a key from the cache. This can force the re-creation
    /// of a texture created by `ImageSource` for example.
    ///
    /// `gpa` is needed to store the texture for deferred destruction
    pub fn invalidate(self: *Cache, gpa: std.mem.Allocator, key: Key) std.mem.Allocator.Error!void {
        try self.trash.ensureUnusedCapacity(gpa, 1);
        const prev = self.cache.fetchRemove(key);
        if (prev) |kv| {
            self.trash.appendAssumeCapacity(kv.value);
        }
    }

    /// Destroys all unused and trashed textures since the last
    /// call to `reset`
    ///
    /// `allocator` is only used for the returned slice and can be
    /// different from the one used for calls to `add`
    pub fn reset(self: *Cache, backend: dvui.Backend) void {
        var it = self.cache.iterator();
        while (it.next_resetting()) |kv| {
            backend.textureDestroy(kv.value);
        }
        for (self.trash.items) |tex| {
            backend.textureDestroy(tex);
        }
        self.trash.clearRetainingCapacity();

        for (self.trash_target.items) |tex| {
            backend.textureDestroyTarget(tex);
        }
        self.trash_target.clearRetainingCapacity();
    }

    /// Deallocates and destroys all stored textures
    pub fn deinit(self: *Cache, gpa: std.mem.Allocator, backend: dvui.Backend) void {
        defer self.* = undefined;
        var it = self.cache.iterator();
        while (it.next()) |item| {
            backend.textureDestroy(item.value_ptr.*);
        }
        self.cache.deinit(gpa);
        for (self.trash.items) |tex| {
            backend.textureDestroy(tex);
        }
        self.trash.deinit(gpa);

        for (self.trash_target.items) |tex| {
            backend.textureDestroyTarget(tex);
        }
        self.trash_target.deinit(gpa);
    }
};

pub const ImageSource = union(enum) {
    /// bytes of an supported image file (i.e. png, jpeg, gif, ...)
    imageFile: ImageFile,

    /// bytes of an premultiplied rgba u8 array in row major order
    pixelsPMA: struct {
        rgba: []Color.PMA,
        width: u32,
        height: u32,
        interpolation: TextureInterpolation = .linear,
        invalidation: InvalidationStrategy = .ptr,
    },

    /// bytes of a non premultiplied rgba u8 array in row major order, will
    /// be converted to premultiplied when making a texture
    pixels: struct {
        /// FIXME: This cannot use `[]const Color` because it's not marked `extern`
        ///        and doesn't have a stable memory layout
        rgba: []const u8,
        width: u32,
        height: u32,
        interpolation: TextureInterpolation = .linear,
        invalidation: InvalidationStrategy = .ptr,
    },

    /// When providing a texture directly, `hash` will return 0 and it will
    /// not be inserted into the texture cache.
    texture: Texture,

    pub const InvalidationStrategy = enum {
        /// The pointer will be used to determine if the source has changed.
        ///
        /// Changing the data behind the pointer will NOT invalidate the texture
        ptr,
        /// The bytes will be used to determine if the source has changed.
        ///
        /// Changing the data behind the pointer WILL invalidate the texture,
        /// but checking all the bytes every frame can be costly
        bytes,
        /// Do not cache the texture at all and generate a new texture each frame
        always,
    };

    pub const ImageFile = struct {
        bytes: []const u8,
        // Optional name/filename for debugging
        name: []const u8 = "imageFile",
        interpolation: TextureInterpolation = .linear,
        invalidation: InvalidationStrategy = .ptr,
    };

    /// Pass the return value of this to `dvui.textureInvalidate` to
    /// remove the texture from the cache.
    ///
    /// When providing a texture directly with `ImageSource.texture`,
    /// this function will always return 0 as it doesn't interact with
    /// the texture cache.
    pub fn hash(self: ImageSource) u64 {
        // short circuit for texture
        switch (self) {
            .texture => return 0,
            else => {},
        }

        var h = dvui.fnv.init();
        // .always hashes ptr (for uniqueness) and image dimensions so we can update the texture if dimensions stay the same
        const img_dimensions = self.size() catch Size{ .w = 0, .h = 0 };
        var dim: [2]u32 = .{ @trunc(img_dimensions.w), @trunc(img_dimensions.h) };
        const img_dim_bytes = std.mem.asBytes(&dim); // hashing u32 here instead of float because of unstable bit representation in floating point numbers

        switch (self) {
            .imageFile => |file| {
                switch (file.invalidation) {
                    .ptr => h.update(std.mem.asBytes(&file.bytes.ptr)),
                    .bytes => h.update(file.bytes),
                    .always => {
                        h.update(std.mem.asBytes(&file.bytes.ptr));
                        h.update(img_dim_bytes);
                    },
                }
                h.update(std.mem.asBytes(&@intFromEnum(file.interpolation)));
            },
            .pixelsPMA => |pixels| {
                switch (pixels.invalidation) {
                    .ptr, .always => h.update(std.mem.asBytes(&pixels.rgba.ptr)),
                    .bytes => h.update(@ptrCast(pixels.rgba)),
                }
                h.update(std.mem.asBytes(&@intFromEnum(pixels.interpolation)));
                h.update(img_dim_bytes);
            },
            .pixels => |pixels| {
                switch (pixels.invalidation) {
                    .ptr, .always => h.update(std.mem.asBytes(&pixels.rgba.ptr)),
                    .bytes => h.update(std.mem.sliceAsBytes(pixels.rgba)),
                }
                h.update(std.mem.asBytes(&@intFromEnum(pixels.interpolation)));
                h.update(img_dim_bytes);
            },
            .texture => unreachable,
        }
        return h.final();
    }

    /// Will get the texture from cache or create it if it doesn't already exist
    ///
    /// Only valid between `Window.begin` and `Window.end`
    pub fn getTexture(self: ImageSource) !Texture {
        const key = self.hash();
        const invalidate = switch (self) {
            .imageFile => |f| f.invalidation,
            .pixels => |px| px.invalidation,
            .pixelsPMA => |px| px.invalidation,
            // return texture directly
            .texture => |tex| return tex,
        };
        if (dvui.textureGetCached(key)) |cached_texture| {
            // if invalidate = always, we update the texture using updateImageSource for efficency, otherwise return the cached Texture
            if (invalidate == .always) {
                var tex_mut = cached_texture;
                try tex_mut.updateImageSource(self);
                if (cached_texture.ptr != tex_mut.ptr) {
                    // updateImageSource created a new texture, called
                    // destroyLater on old one, remove before adding new one
                    _ = dvui.currentWindow().texture_cache.remove(key);
                    dvui.textureAddToCache(key, tex_mut);
                }
                return tex_mut;
            } else return cached_texture;
        } else {
            // cache was empty we create a new Texture
            const new_texture = try Texture.fromImageSource(self);
            dvui.textureAddToCache(key, new_texture);
            return new_texture;
        }
    }

    /// Get the size of a raster image.  If source is .imageFile, this only decodes
    /// enough info to get the size.
    ///
    /// See `dvui.image`.
    ///
    /// Only valid between `Window.begin`and `Window.end`.
    pub fn size(source: ImageSource) !Size {
        switch (source) {
            .imageFile => |file| {
                var w: c_int = undefined;
                var h: c_int = undefined;
                var n: c_int = undefined;
                const ok = dvui.c.stbi_info_from_memory(file.bytes.ptr, @as(c_int, @intCast(file.bytes.len)), &w, &h, &n);
                if (ok == 1) {
                    return .{ .w = @floatFromInt(w), .h = @floatFromInt(h) };
                } else {
                    dvui.log.warn("imageSize stbi_info error on image \"{s}\": {s}\n", .{ file.name, dvui.c.stbi_failure_reason() });
                    return StbImageError.stbImageError;
                }
            },
            .pixelsPMA => |a| return .{ .w = @floatFromInt(a.width), .h = @floatFromInt(a.height) },
            .pixels => |a| return .{ .w = @floatFromInt(a.width), .h = @floatFromInt(a.height) },
            .texture => |tex| return .{ .w = @floatFromInt(tex.width), .h = @floatFromInt(tex.height) },
        }
    }
};

/// Update a texture that was created with `textureCreate`. or fromImageSource
///
/// The dimensions of the image must match the initial dimensions!
/// Only valid to call while the underlying Texture is not destroyed!
///
/// Only valid between `Window.begin` and `Window.end`.
pub fn updateImageSource(self: *Texture, src: ImageSource) !void {
    switch (src) {
        .imageFile => |f| {
            const img = try Color.PMAImage.fromImageFile(f.name, dvui.currentWindow().arena(), f.bytes);
            defer dvui.currentWindow().arena().free(img.pma);
            try update(self, img.pma);
        },
        .pixels => |px| {
            const copy = try dvui.currentWindow().arena().dupe(u8, px.rgba);
            defer dvui.currentWindow().arena().free(copy);
            const pma = Color.PMA.sliceFromRGBA(copy);
            try update(self, pma);
        },
        .pixelsPMA => |px| {
            try update(self, px.rgba);
        },
        .texture => @panic("this is not supported currently"),
    }
}

/// creates a new Texture from an ImageSource
///
/// Only valid between `Window.begin` and `Window.end`.
pub fn fromImageSource(source: ImageSource) !Texture {
    return switch (source) {
        .imageFile => |f| try Texture.fromImageFile(f.name, f.bytes, f.interpolation),
        .pixelsPMA => |px| try Texture.fromPixelsPMA(px.rgba, px.width, px.height, px.interpolation),
        .pixels => |px| blk: {
            // Using arena here instead of lifo as this buffer is likely to be large and we
            // prefer that lifo doesn't reallocate as often. Arena is intended for larger,
            // one of allocations and we can still free the buffer here
            const copy = try dvui.currentWindow().arena().dupe(u8, px.rgba);
            defer dvui.currentWindow().arena().free(copy);
            break :blk try Texture.fromPixelsPMA(Color.PMA.sliceFromRGBA(copy), px.width, px.height, px.interpolation);
        },
        .texture => |t| t,
    };
}

pub fn fromImageFile(name: []const u8, image_bytes: []const u8, interpolation: TextureInterpolation) (TextureError || StbImageError)!Texture {
    const img = Color.PMAImage.fromImageFile(name, dvui.currentWindow().arena(), image_bytes) catch return StbImageError.stbImageError;
    defer dvui.currentWindow().arena().free(img.pma);
    return try create(img.pma, .{ .width = img.width, .height = img.height, .interpolation = interpolation });
}

pub fn fromPixelsPMA(pma: []const Color.PMA, width: u32, height: u32, interpolation: TextureInterpolation) TextureError!Texture {
    return try dvui.textureCreate(pma, .{ .width = width, .height = height, .interpolation = interpolation });
}

/// Create a texture that can be rendered with `renderTexture`.
///
/// Remember to destroy the texture at some point, see `destroyLater`.
///
/// Only valid between `Window.begin` and `Window.end`.
pub fn create(pixels: []const Color.PMA, options: CreateOptions) TextureError!Texture {
    if (pixels.len != options.width * options.height) {
        dvui.log.err("Texture was created with an incorrect amount of pixels, expected {d} but got {d} (w: {d}, h: {d})", .{ pixels.len, options.width * options.height, options.width, options.height });
    }
    dvui.currentWindow().render_stats.textures_created += 1;
    return dvui.currentWindow().backend.textureCreate(@ptrCast(pixels.ptr), options);
}

pub fn cast(t: Target) Texture {
    return .{ .ptr = t.ptr, .width = t.width, .height = t.height, .format = t.format, .interpolation = t.interpolation, .wrap_u = t.wrap_u, .wrap_v = t.wrap_v };
}

/// Update a texture that was created with `textureCreate`.
///
/// If the backend does not support updating textures, it will be destroyed and
/// recreated, changing the pointer inside tex.
///
/// Only valid between `Window.begin` and `Window.end`.
pub fn update(tex: *Texture, pma: []const Color.PMA) !void {
    if (pma.len != tex.width * tex.height) @panic("Texture size and supplied Content did not match");
    dvui.currentWindow().backend.textureUpdate(tex.*, @ptrCast(pma.ptr)) catch |err| {
        // texture update not supported by backend, destroy and create texture
        if (err == TextureError.NotImplemented) {
            const new_tex = try create(pma, .{ .width = tex.width, .height = tex.height, .interpolation = tex.interpolation, .format = tex.format, .wrap_u = tex.wrap_u, .wrap_v = tex.wrap_v });
            destroyLater(tex.*);
            tex.* = new_tex;
        } else {
            return err;
        }
    };
}

/// Update a sub-rectangle of a texture from raw PMA pixel data.
/// The full pixel buffer must be passed (same as for `update`); only the
/// rows/columns within the given rect will be uploaded.
///
/// Falls back to a full `update` if the backend does not support sub-rect
/// updates.
///
/// Only valid between `Window.begin` and `Window.end`.
pub fn updateSubRect(tex: *Texture, pma: [*]const u8, x: u32, y: u32, w: u32, h: u32) !void {
    dvui.currentWindow().backend.textureUpdateSubRect(tex.*, pma, x, y, w, h) catch |err| {
        if (err == TextureError.NotImplemented) {
            const full_len = tex.width * tex.height;
            const full: []const Color.PMA = @as([*]const Color.PMA, @ptrCast(@alignCast(pma)))[0..full_len];
            try update(tex, full);
        } else {
            return err;
        }
    };
}

/// Read pixels from texture created with `textureCreateTarget`.
///
/// Returns pixels allocated by arena.
///
/// Only valid between `Window.begin`and `Window.end`.
pub fn readTarget(arena: std.mem.Allocator, texture: Target) TextureError![]Color.PMA {
    const size: usize = texture.width * texture.height * @sizeOf(Color.PMA);
    const pixels = try arena.alloc(u8, size);
    errdefer arena.free(pixels);

    try dvui.currentWindow().backend.textureReadTarget(texture, pixels.ptr);

    return @ptrCast(pixels);
}

/// Convert a target texture to a normal texture.  target is destroyed.  See
/// `fromTargetTemp`.
///
/// Only valid between `Window.begin`and `Window.end`.
pub fn fromTarget(target: Target) TextureError!Texture {
    return dvui.currentWindow().backend.textureFromTarget(target);
}

/// Get a temporary drawable Texture from a Target, that will be automatically
/// cleaned up at the end of the frame.  target is not destroyed.  See
/// `fromTarget`.
///
/// Only valid between `Window.begin`and `Window.end`.
pub fn fromTargetTemp(target: Target) TextureError!Texture {
    return dvui.currentWindow().backend.textureFromTargetTemp(target);
}

/// Destroy a texture created with `textureCreate` at the end of the frame.
///
/// While `Backend.textureDestroy` immediately destroys the texture, this
/// function deferres the destruction until the end of the frame, so it is safe
/// to use even in a subwindow where rendering is deferred.
///
/// Only valid between `Window.begin`and `Window.end`.
pub fn destroyLater(texture: Texture) void {
    const cw = dvui.currentWindow();
    cw.texture_cache.trash.append(cw.gpa, texture) catch |err| {
        dvui.log.err("Texture destroyLater got {any}\n", .{err});
    };
}

const std = @import("std");
const dvui = @import("dvui.zig");

const Size = dvui.Size;
const Color = dvui.Color;
const TextureInterpolation = dvui.enums.TextureInterpolation;
const TexturePixelFormat = dvui.enums.TexturePixelFormat;

const TextureError = dvui.Backend.TextureError;
const StbImageError = dvui.StbImageError;

test {
    @import("std").testing.refAllDecls(@This());
}
