package org.limewire.core.impl.itunes;

import java.io.File;
import java.io.IOException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.TimeUnit;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.limewire.concurrent.ExecutorsHelper;
import org.limewire.core.settings.iTunesSettings;
import org.limewire.util.FileUtils;
import org.limewire.util.OSUtils;

import com.google.inject.Singleton;
import com.jacob.activeX.ActiveXComponent;
import com.jacob.com.ComFailException;
import com.jacob.com.ComThread;
import com.jacob.com.Dispatch;
import com.jacob.com.Variant;

/**
 * Handles sending completed downloads into iTunes.
 */
@Singleton
public final class ItunesMediatorImpl implements ItunesMediator {

    private static final Log LOG = LogFactory.getLog(ItunesMediatorImpl.class);

    /**
     * The queue that will process the tunes to add.
     */
    private final ExecutorService QUEUE = ExecutorsHelper.newProcessingQueue("iTunesAdderThread");

    @Override
    public void addSong(File file) {

        // If not on Windows or OSX don't do anything.
        if (!OSUtils.isWindows() && !OSUtils.isMacOSX())
            return;

        // Make sure we convert any uppercase to lowercase or vice versa.
        try {
            file = FileUtils.getCanonicalFile(file);
        } catch (IOException ignored) {
        }

        // Verify that we're adding a real file.
        if (!file.exists()) {
            if (LOG.isDebugEnabled())
                LOG.debug("File: '" + file + "' does not exist");
            return;
        }

        File[] files;
        if (file.isDirectory()) {
        	files = FileUtils.getFilesRecursive(file, 
        			iTunesSettings.ITUNES_SUPPORTED_FILE_TYPES.get());
        } else if (file.isFile() && isSupported(FileUtils.getFileExtension(file)))
            files = new File[] { file };
        else
            return;

        for (File toAdd : files) {
            if (LOG.isTraceEnabled())
                LOG.trace("Will add '" + toAdd + "' to Playlist");

            if (OSUtils.isWindows())
                QUEUE.execute(new AddFileToWindowsITunes(toAdd));
            else if (OSUtils.isMacOSX())
                QUEUE.execute(new ExecOSAScriptCommand(toAdd));
        }
    }

    /**
     * Returns true if the extension of name is a supported file type.
     */
    private static boolean isSupported(String extension) {
        if (extension.isEmpty())
            return false;
        String[] types = iTunesSettings.ITUNES_SUPPORTED_FILE_TYPES.get();
        for (int i = 0; i < types.length; i++)
            if (extension.equalsIgnoreCase(types[i]))
                return true;
        return false;
    }

    /** Uses JACOB and the iTunes COM SDK to add files to iTunes. */
    private static class AddFileToWindowsITunes implements Runnable {
        private static final String ITUNES_ACTIVEX_NAME = "iTunes.Application";

        /**
         * Set to a value the first time this class executes an add operation.
         * If false, all subsequent adds will be ignored.
         */
        private static Boolean ready = null;

        private final File file;

        /**
         * Constructs a new AddFileToWindowsITunes for the specified file.
         */
        public AddFileToWindowsITunes(File file) {
            this.file = file;
        }

        public void run() {
            try {
                addFileToLibrary(file);
            } catch (com.jacob.com.ComFailException e) {
                LOG.warn("Error adding file to itunes library", e);
            } catch (IllegalStateException e) {
                LOG.warn("Error adding file to itunes library", e);
            }
        }

        /**
         * @return true if:
         *         <li>The JACOB library is correctly linked
         *         <li>iTunes seems to be responsive
         */
        public synchronized boolean isReady() {
            if (ready != null)
                return ready;
            // Check for JACOB & iTunes
            try {
                ComThread.InitMTA();
                try {
                    // Check for iTunes
                    new ActiveXComponent(ITUNES_ACTIVEX_NAME);
                } finally {
                    ComThread.Release();
                }
            } catch (UnsatisfiedLinkError ex) {
                // JACOB is not in the java.library.path
                LOG.error("JACOB is not in the java.library.path.", ex);
                ready = Boolean.FALSE;
                return false;
            } catch (ComFailException ex) {
                // iTunes ActiveX wasn't available...
                LOG.error("iTunes ActiveX component not found.", ex);
                ready = Boolean.FALSE;
                return false;
            }

            ready = Boolean.TRUE;
            return true;
        }

        /**
         * Does the heavy lifting, activating COM and inserting the file into
         * the iTunes DB. Does not add duplicates.
         */
        private boolean addFileToLibrary(File newFile) {
            if (!isReady())
                return false;
            ComThread.InitMTA();
            try {
                ActiveXComponent iTunesCom = new ActiveXComponent(ITUNES_ACTIVEX_NAME);
                Dispatch iTunesController = iTunesCom.getObject();
                Dispatch libraryPlaylist = null;
                Dispatch addStatus = null;
                try {
                    libraryPlaylist = Dispatch.get(iTunesController, "LibraryPlaylist").toDispatch();
                    Variant addStatusVariant = Dispatch.call(libraryPlaylist, "AddFile", newFile.getAbsolutePath());
                    if (addStatusVariant == null || addStatusVariant.isNull()) {
                        return false;
                    }

                    addStatus = addStatusVariant.toDispatch();
                    while (Dispatch.get(addStatus, "InProgress").getBoolean()) {
                        // Waiting for add operation to complete (This is usually
                        // instantaneous)...
                        try {
                            TimeUnit.MILLISECONDS.sleep(10);
                        } catch (InterruptedException ignored) {
                            break;
                        }
                    }
                } finally {
                    if (addStatus != null) {
                        addStatus.safeRelease();
                    }
                    if (libraryPlaylist != null) {
                        libraryPlaylist.safeRelease();
                    }
                    iTunesController.safeRelease();
                    iTunesCom.safeRelease();
                }
            } finally {
                ComThread.Release();
            }
            return true;
        }
    }

    /**
     * Executes the osascript CLI command (Mac OSX)
     */
    private static class ExecOSAScriptCommand implements Runnable {
        /**
         * The file to add.
         */
        private final File file;

        /**
         * Constructs a new ExecOSAScriptCommand for the specified file.
         */
        public ExecOSAScriptCommand(File file) {
            this.file = file;
        }

        /**
         * Constructs and returns a osascript command.
         */
        private String[] createOSAScriptCommand(File file) {
            String path = file.getAbsolutePath();
            String playlist = iTunesSettings.ITUNES_PLAYLIST.get();

            String[] command = new String[] { "osascript", "-e", "tell application \"Finder\"",
                    "-e",
                    "set hfsFile to (POSIX file \"" + path + "\")",
                    "-e",
                    "set thePlaylist to \"" + playlist + "\"",
                    "-e",
                    "tell application \"iTunes\"",
                    // "-e", "activate", // launch and bring to front
                    "-e",
                    "launch", // launch in background
                    "-e", "if not (exists playlist thePlaylist) then", "-e",
                    "set thisPlaylist to make new playlist", "-e",
                    "set name of thisPlaylist to thePlaylist", "-e", "end if", "-e",
                    "add hfsFile to playlist thePlaylist", "-e", "end tell", "-e", "end tell" };

            return command;
        }

        /**
         * Runs the osascript command
         */
        public void run() {
            try {
                Runtime.getRuntime().exec(createOSAScriptCommand(file));
            } catch (IOException err) {
                LOG.debug(err);
            }
        }
    }
}
