/*
 * This file is part of muCommander, http://www.mucommander.com
 *
 * muCommander is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
 *
 * muCommander is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package com.mucommander.desktop.osx;

import com.mucommander.commons.file.AbstractFile;
import com.mucommander.commons.file.FileFactory;
import com.mucommander.commons.file.protocol.local.LocalFile;
import com.mucommander.desktop.QueuedTrash;
import com.mucommander.ui.macosx.AppleScript;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.io.OutputStreamWriter;
import java.util.List;

/**
 * OSXTrash provides access to the Mac OS X Finder's trash. Only local files (or locally mounted files) can be moved
 * to the trash.
 *
 * <p>
 *   <b>Implementation notes:</b><br/>
 *   <br/>
 *   This trash is implemented as a {@link com.mucommander.desktop.QueuedTrash} for several reasons:
 *   <ul>
 *    <li>the Finder plays a sound when it has been told to move a file to the trash and is done with it.
 *        Moving files to the trash repeatedly would play the sound as many times as the Finder has been told to move a
 *        file, which is obviously ugly.</li>
 *    <li>executing an AppleScript has a cost as it has to be compiled first. When files are moved repeatedly, it is
 *        more efficient to group files and execute only one AppleScript.</li>
 *   </ul>
 *   <br/>
 *   This class uses {@link com.mucommander.ui.macosx.AppleScript} to interact with the trash.
 * </p>
 *
 * @see OSXTrashProvider
 * @author Maxence Bernard
 */
public class OSXTrash extends QueuedTrash {
	private static final Logger LOGGER = LoggerFactory.getLogger(OSXTrash.class);
	
    /** AppleScript that reveals the trash in Finder */
    private final static String REVEAL_TRASH_APPLESCRIPT =
        "tell application \"Finder\" to open trash\n" +
        "activate application \"Finder\"\n";

    /** AppleScript that counts and returns the number of items in Trash */
    private final static String COUNT_TRASH_ITEMS_APPLESCRIPT =
            "tell application \"Finder\" to return count of items in trash";

    /** AppleScript that empties the trash */
    private final static String EMPTY_TRASH_APPLESCRIPT = "tell application \"Finder\" to empty trash";

    /**
     * AppleScript that moves files to the trash, for versions of AppleScript (1.10 or lower )that do not allow Unicode
     * in the script itself (only MacRoman). As a result, this script is more complicated as the only way to deal with
     * Unicode text is to read them from a file. See http://www.satimage.fr/software/en/unicode_and_applescript.html
     * for more info about this workaround.
     */
    private final static String MOVE_TO_TRASH_APPLESCRIPT_NO_UNICODE =
        // Loads the contents of the UTF8-encoded file which path is contained in the 'tmpFilePath' variable.
        // This variable must be set before the beginning of the script. This file contains the list of files to move
        // to the trash, separated by EOL characters. The file must NOT end with a trailing EOL.
        "set tmpFile to (open for access (POSIX file tmpFilePath))\n" +
        "set tmpFileContents to (read tmpFile for (get eof tmpFile) as «class utf8»)\n" +
        "close access tmpFile\n" +
        // Split the file contents into a list of lines, each line representing a POSIX file path to delete
        "set posixFileList to every paragraph of tmpFileContents\n" +
        // Convert the list of POSIX paths into a list of file objects. Note that internally AppleScript uses
        // a Mac-specific colon-separated path notation rather than the POSIX one.
        "set fileCount to the number of items in posixFileList\n" +
        "set fileList to {}\n" +
        "repeat with i from 1 to the fileCount\n" +
            "set posixFile to item i of posixFileList\n" +
            "copy POSIX file posixFile to the end of fileList\n" +
        "end repeat\n" +
        // Tell the Finder to move those files to the trash. Note that the file list must contain file objects and not
        // POSIX paths, hence the previous step. 
        "tell application \"Finder\" to move fileList to the trash";


    //////////////////////////////////
    // AbstractTrash implementation //
    //////////////////////////////////

    /**
     * Implementation notes: returns <code>true</code> only for local files that are not archive entries.
     */
    @Override
    public boolean canMoveToTrash(AbstractFile file) {
        return file.getTopAncestor() instanceof LocalFile;
    }

    /**
     * Implementation notes: always returns <code>true</code>.
     */
    @Override
    public boolean canEmpty() {
        return true;
    }

    @Override
    public boolean empty() {
        return AppleScript.execute(EMPTY_TRASH_APPLESCRIPT, null);
    }

    @Override
    public boolean isTrashFile(AbstractFile file) {
        return (file.getTopAncestor() instanceof LocalFile)
            && (file.getAbsolutePath(true).indexOf("/.Trash/") != -1);
    }

    /**
     * Implementation notes: this method is implemented and returns <code>-1</code> only if an error ocurred while
     * retrieving the trash item count.
     */
    @Override
    public int getItemCount() {
        StringBuilder output = new StringBuilder();
        if(!AppleScript.execute(COUNT_TRASH_ITEMS_APPLESCRIPT, output))
            return -1;

        try {
            return Integer.parseInt(output.toString().trim());
        }
        catch(NumberFormatException e) {
            LOGGER.debug("Caught an exception", e);
            return -1;
        }
    }

    @Override
    public void open() {
        AppleScript.execute(REVEAL_TRASH_APPLESCRIPT, null);
    }

    /**
     * Implementation notes: always returns <code>true</code>.
     */
    @Override
    public boolean canOpen() {
        return true;
    }


    ////////////////////////////////
    // QueuedTrash implementation //
    ////////////////////////////////

    /**
     * Performs the actual job of moving files to the trash using AppleScript.
     *
     * <p>The thread starts by waiting {@link OSXTrash#QUEUE_PERIOD} milliseconds before moving them to give additional
     * files a chance to be queued and regrouped as a single AppleScript call. If some files were queued during
     * that period, the thread will wait an additional {@link OSXTrash#QUEUE_PERIOD}, and so on.<p>
     *
     * <p>There are several reasons for doing that instead of executing an AppleScript synchronously for each file
     * passed to {@link OSXTrash#moveToTrash(com.mucommander.commons.file.AbstractFile)} :
     * <ul>
     *  <li>the Finder plays a sound when it has been told to move a file to the trash and is done with it. Calling
     * moveToTrash repeatedly would play the sound as many times as the method has been called (believe me it's ugly
     * and a show-stopper!)
     *  <li>executing an AppleScript has a cost as it has to be compiled first. If moveToTrash is called repeatedly, it
     * is more efficient to regroup files to be moved and execute only one AppleScript.
     * </ul>
     */
    @Override
    protected boolean moveToTrash(List<AbstractFile> queuedFiles) {
        String appleScript;

        // Simple script for AppleScript versions with Unicode support, i.e. that allows Unicode characters in the
        // script (AppleScript 2.0 / Mac OS X 10.5 or higher).
        if(AppleScript.getScriptEncoding().equals(AppleScript.UTF8)) {
            int nbFiles = queuedFiles.size();
            appleScript = "tell application \"Finder\" to move {";
            for(int i=0; i<nbFiles; i++) {
                appleScript += "posix file \""+queuedFiles.get(i).getAbsolutePath()+"\"";
                if(i<nbFiles-1)
                    appleScript += ", ";
            }
            appleScript += "} to the trash";

            return AppleScript.execute(appleScript, null);
        }
        // Script for AppleScript versions without Unicode support (AppleScript 1.10 / Mac OS X 10.4 or lower)
        else {
            AbstractFile tmpFile = null;
            OutputStreamWriter tmpOut = null;

            try {
                // Create the temporary file that contains the list of files to move, encoded as UTF-8 and separated by
                // EOL characters. The file must NOT end with a trailing EOL.
                int nbFiles = queuedFiles.size();
                tmpFile = FileFactory.getTemporaryFile("trash_files.muco", false);
                tmpOut = new OutputStreamWriter(tmpFile.getOutputStream(), "utf-8");

                for(int i=0; i<nbFiles; i++) {
                    tmpOut.write(queuedFiles.get(i).getAbsolutePath());
                    if(i<nbFiles-1)
                        tmpOut.write("\n");
                }

                tmpOut.close();

                // Set the 'tmpFilePath' variable to the path of the temporary file we just created
                appleScript = "set tmpFilePath to \""+tmpFile.getAbsolutePath()+"\"\n";
                appleScript += MOVE_TO_TRASH_APPLESCRIPT_NO_UNICODE;

                boolean success = AppleScript.execute(appleScript, null);

                // AppleScript has been executed, we can now safely close and delete the temporary file
                tmpFile.delete();

                return success;
            }
            catch(IOException e) {
                LOGGER.debug("Caught IOException", e);

                if(tmpOut!=null) {
                    try { tmpOut.close(); }
                    catch(IOException e1) {
                        // There's not much we can do about it
                    }
                }

                if(tmpFile!=null) {
                    try { tmpFile.delete(); }
                    catch(IOException e2) {
                        // There's not much we can do about it
                    }
                }

                return false;
            }
        }

    }
}
