sourceafReflux::Images.fan

using afIoc
using gfx::Image
using concurrent::Actor

** (Service) -
** Maintains a cache of Images, ensuring they are disposed of properly.
** 
** A common usage is to create an 'Icons' class in your project that keeps tabs on the file mappings:
** 
** pre>
** syntax: fantom
** 
** using afIoc::Inject
** using afReflux::Images
** using gfx::Image
** 
** class Icons {
**     @Inject private Images images
**     
**     private new make(|This| in) { in(this) }
**     
**     Image icoImage1() { get("image1.png") }
**     Image icoImage2() { get("image2.png") }
**     Image icoImage3() { get("image3.png") }
** 
**     Image get(Str name) {
**         images[`fan://${typeof.pod.name}/res/icons/${name}`]
**     }
** }
** <pre
** 
** Then the icons may be referenced with:
** 
**   syntax: fantom
** 
**   icon := icons.icoImage1
** 
** Don't forget to add the image directory to 'resDirs' in the 'build.fan':
** 
**   syntax: fantom
** 
**   resDirs = [`res/icons/`]
** 
@Js
mixin Images {

    ** Returns (and caches) the image at the given URI.
    @Operator
    abstract Image? get(Uri uri, Bool checked := true)

    ** Stashes the image under the given URI.
    ** If another image existed under the same URI, it is disposed of.
    @Operator
    abstract Void set(Uri uri, Image image)

    ** Returns true if an image is mapped to the given URI.
    abstract Bool contains(Uri uri)

    ** Returns (and caches) a faded version of the image at the given URI.
    ** Useful for generating *disabled* icons.
    abstract Image? getFaded(Uri uri, Bool checked := true)

    ** Returns (and does not cache) the image at the given URI ensuring that it is fully loaded and that
    ** its 'size()' is available.
    abstract Image? load(Uri uri, Bool checked := true)

    ** Disposes of all the images. This is called on registry shutdown.
    ** The 'AppModule' config key is 'afReflux.disposeOfImages'.
    abstract Void disposeAll()
}

@NoDoc  @Js // so maxLoadTime may be overridden
class ImagesImpl : Images {
    private Uri:Image       images      := Uri:Image[:]
    private Uri:Image       fadedImages := Uri:Image[:]
    private Image[]         extra       := Image[,]
    private Uri:DateTime    fileCache   := Uri:DateTime[:]

            Duration maxLoadTime    := 200ms

    new make(|This| in) { in(this) }

    override Image? get(Uri uri, Bool checked := true) {
        if (images.containsKey(uri))
            return images[uri]

        image := makeImage(uri, checked)

        if (image != null)
            images[uri] = image

        return image
    }

    override Void set(Uri uri, Image image) {
        if (image == images[uri])
            return
        if (images.containsKey(uri))
            images[uri].dispose
        images[uri] = image
    }

    override Bool contains(Uri uri) {
        images.containsKey(uri)
    }

    override Image? getFaded(Uri uri, Bool checked := true) {
        if (fadedImages.containsKey(uri))
            return fadedImages[uri]

        image := load(uri, checked)
        if (image == null)
            return null

        faded := Image.makePainted(image.size) |gfx| {
            gfx.alpha = 128
            gfx.antialias = false
            gfx.drawImage(image, 0, 0)
        }

        fadedImages[uri] = faded
        return faded
    }

    override Image? load(Uri uri, Bool checked := true) {
        // we may cache an image produced from load, but don't bother caching it itself
        image := makeImage(uri, checked)

        if (image == null)
            return null

        try {
            napTime := 0sec
            while (napTime < maxLoadTime && (image.size.w == 0 || image.size.h == 0)) {
                napTime += 20ms
                Actor.sleep(20ms)
            }
            if (image.size.w == 0 || image.size.h == 0)
                throw Err("Loading image `${uri}` took too long... (>${maxLoadTime.toLocale}) - w=${image.size.w} h=${image.size.h}")

        } catch (Err err) {
            // beware: org.eclipse.swt.SWTException: Unsupported or unrecognized format
            if (!checked)
                return null
            throw err
        }

        return image
    }
    
    ** Fantom has some sort of internal Image caching going on - so we cache bust the URI if needed
    private Image? makeImage(Uri uri, Bool checked := true) {
        if (Env.cur.runtime == "js")
            return Image(uri, checked)

        try {
            f := (File?) uri.get
            if (f == null || !f.exists) throw UnresolvedErr("file does not exist: ${f?.normalize ?: uri}")
            
            lastUpdated := fileCache.getOrAdd(uri) { f.modified }
            if (f.modified > lastUpdated) {
                fileCache[uri] = f.modified
                uri = uri.plusQuery(["bustCache":Int.random(0..9999).toStr])
            }
            
            return Image(uri, checked)

        } catch (Err e) {
            if (checked) throw e
            return null
        }       
    }

    override Void disposeAll() {
        images.vals.each { it.dispose }
        images.clear
        extra.each { it.dispose }
        extra.clear
    }
}