sourceafExplorer::Explorer.fan

using afIoc
using afReflux
using gfx
using fwt

** (Service) - 
** The main service API for Explorer operations.
mixin Explorer {
    abstract Void rename(File file)
    abstract Void delete(File file)
    abstract Void cut(File file)
    abstract Void copy(File file)
    abstract Void paste(File destDir)
    
    ** Opens a dialogue for the file name before creating an empty file.
    ** File name defaults to 'NewFile.txt'. 
    abstract Void newFile(File containingFolder, Str? defFileName := null)
    
    ** Opens a dialogue for the folder name before creating an empty folder.
    ** Folder name defaults to 'NewFolder'.
    abstract Void newFolder(File containingFolder, Str? defFolderName := null)

    abstract Void openFileInSystem(File file)

    abstract Void compressToZip(File toCompress, File dst)
    
    abstract Image fileToIcon(File f)
    abstract Image urlToIcon(Uri url)

    abstract ExplorerPrefs preferences()

    @NoDoc  // a small hack until we make paste a global command
    abstract Bool pasteEnabled()
}
    
internal class ExplorerImpl : Explorer {
    @Inject private Registry    registry
    @Inject private RefluxIcons icons
    @Inject private Images      images
    @Inject private Preferences prefs
    @Inject private Reflux      reflux
    @Inject private Errors      errors
    @Inject private Dialogues   dialogues
                    Uri         fileIconsRoot   := `fan://afExplorer/res/icons-file/`

    internal File? copiedFile
    internal File? cutFile

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

    override Void rename(File file) {
        newName := dialogues.openPromptStr("Rename", file.name)
        if (newName != null) {
            file.rename(newName)
            if (file.parent != null)
                reflux.refresh(reflux.resolve(file.parent.uri.toStr))
        }
    }

    override Void delete(File file) {
        okay := dialogues.openQuestion("Delete ${file.name}?\n\n${file.osPath}", null, dialogues.yesNo)
        if (okay == dialogues.yes) {
            file.delete
            if (file.parent != null)
                reflux.refresh(reflux.resolve(file.parent.uri.toStr))
        }
    }

    override Void cut(File file) {
        cutFile     = file
        copiedFile  = null
    }
    
    override Void copy(File file) {
        cutFile     = null
        copiedFile  = file
    }

    override Void paste(File destDir) {
        // TODO: dialog for copy overwrite options
        if (cutFile != null) {
            cutFile.moveInto(destDir)
            cutFile = null
        }
        if (copiedFile != null) {
            if (!copiedFile.isDir) {
                // handle name conflicts when duplicating (copy->paste) files
                fileIndex := 0
                destName := copiedFile.name.toUri
                destFile := destDir + destName
                while (destFile.exists) {
                    fileIndex++
                    if (copiedFile.ext == null)
                        destName = `${copiedFile.name}($fileIndex)`
                    else
                        destName = `${copiedFile.name[0..<-copiedFile.ext.size-1]}($fileIndex).${copiedFile.ext}`
                    destFile = destDir + destName
                }
                copiedFile.copyTo(destFile)
                
            } else
                copiedFile.copyInto(destDir)
            
            // once copied, allow multiple pastes
//          copiedFile = null
        }
        reflux := (Reflux) registry.serviceById(Reflux#.qname)
        reflux.refresh(reflux.resolve(destDir.uri.toStr))
    }
    
    override Void newFile(File containingFolder, Str? defFileName := null) {
        fileName := dialogues.openPromptStr("New File", defFileName ?: "NewFile.txt")
        if (fileName != null) {
            containingFolder.createFile(fileName)
            reflux.refresh(reflux.resolve(containingFolder.uri.toStr))
        }
    }

    override Void newFolder(File containingFolder, Str? defFolderName := null) {
        dirName := dialogues.openPromptStr("New Folder", defFolderName ?: "NewFolder")
        if (dirName != null) {
            containingFolder.createDir(dirName)
            reflux.refresh(reflux.resolve(containingFolder.uri.toStr))
        }
    }
    
    override Void openFileInSystem(File file) {
        Desktop.launchProgram(file.uri)
    }
    
    override Void compressToZip(File toCompress, File dst) {
        if (dst.isDir || dst.exists)
            throw ArgErr("Cannot write to $dst")
        
        try {
            // TODO: Pop up progress monitor for long running zip tasks
            parentUri := toCompress.parent.uri
            zip := Zip.write(dst.out)
            try {
                toCompress.walk |src| {
                    if (src.isDir) return

                    path := src.uri.relTo(parentUri)
                    out := zip.writeNext(path)
                    try {
                        src.in(16 * 1024).pipe(out)
                    } finally
                        out.close
                }
            } finally
                zip.close

        } catch (Err err)
            errors.add(err)

        reflux.refresh
    }
    
    override Image fileToIcon(File f) {
        hidden := preferences.isHidden(f)

        if (f.isDir) {
            // can't cache osRoots 'cos it changes with flash drives et al
            osRoots := File.osRoots.map { it.normalize }        
            name := osRoots.contains(f) ? "icoFolderRoot" : "icoFolder"
            return hidden ? icons.getFaded(name) : icons.get(name)
        }
        
        // if the image is small enough ~5k, return a thumbnail as the icon
        // .svg files and the like cause ugly stack traces as FWT logs the Err before returning null... Grrr!!
        if ("bmp jpg jpeg gif png".split.contains(f.ext ?: "") && f.size < (5 * 1024)) {
            if (images.contains(f.uri))
                return hidden ? images.getFaded(f.uri) : images.get(f.uri)

            icon := images.load(f.uri, false)
            if (icon != null) {
                if (icon.size == Size(16, 16)) {
                    images[f.uri] = icon
                    return icon
                }

                // note we have to return a 16x16 image else SWT scales it for us
                if (icon.size.w <= 16 && icon.size.h <= 16) {
                    newIcon := Image(Size(16, 16)) |Graphics g| {
                        g.drawImage(icon, (16 - icon.size.w) / 2, (16 - icon.size.h) / 2)
                    }
                    images[f.uri] = newIcon
                    return newIcon
                }

                if (icon.size.w >= icon.size.h) {
                    newH := icon.size.h * 16 / icon.size.w
                    newIcon := icon.resize(Size(16, newH))
                    if (newH < 16) {
                        newIcon = Image(Size(16, 16)) |Graphics g| {
                            g.drawImage(newIcon, 0, (16 - newH) / 2)
                        }
                    }
                    images[f.uri] = newIcon
                    return newIcon
                }

                if (icon.size.w <= icon.size.h) {
                    newW := icon.size.w * 16 / icon.size.h
                    newIcon := icon.resize(Size(newW, 16))
                    if (newW < 16) {
                        newIcon = Image(Size(16, 16)) |Graphics g| {
                            g.drawImage(newIcon, (16 - newW) / 2, 0)
                        }
                    }
                    images[f.uri] = newIcon
                    return newIcon
                }               
            }
        }
        
        // look for explicit match based off ext
        if (f.ext != null) {
            icon := fileIcon("file${f.ext.capitalize}.png", hidden)
            if (icon != null) return icon
        }
        
        mimeType := f.mimeType?.noParams
        if (mimeType != null) {
            mime := mimeType.mediaType.fromDisplayName.capitalize + mimeType.subType.fromDisplayName.capitalize
            icon := fileIcon("file${mime}.png", hidden)
            if (icon != null) return icon

            mime = mimeType.mediaType.fromDisplayName.capitalize
            icon = fileIcon("file${mime}.png", hidden)
            if (icon != null) return icon
        }

        return fileIcon("file.png", hidden)
    }
    
    override Image urlToIcon(Uri url) {
        // look for explicit match based off ext
        if (url.ext != null) {
            icon := fileIcon("file${url.ext.capitalize}.png", false)
            if (icon != null) return icon
        }
        
        mimeType := url.mimeType?.noParams
        if (mimeType != null) {
            mime := mimeType.mediaType.fromDisplayName.capitalize + mimeType.subType.fromDisplayName.capitalize
            icon := fileIcon("file${mime}.png", false)
            if (icon != null) return icon

            mime = mimeType.mediaType.fromDisplayName.capitalize
            icon = fileIcon("file${mime}.png", false)
            if (icon != null) return icon
        }

        return fileIcon("fileTextHtml.png", false)
    }

    override once ExplorerPrefs preferences() {
        prefs.loadPrefs(ExplorerPrefs#, "afExplorer.fog")
    }

    private Image? fileIcon(Str fileName, Bool hidden) {
        uri := fileIconsRoot.plusName(fileName)
        return hidden ? images.getFaded(uri, false) : images.get(uri, false)
    }
    
    override Bool pasteEnabled() {
        copiedFile != null || cutFile != null
    }
}