package co.arcs.groove.basking.task;

import com.beust.jcommander.internal.Maps;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableList.Builder;
import com.google.common.collect.Lists;
import com.google.common.eventbus.EventBus;
import com.google.common.io.CharStreams;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.annotation.Nullable;

import co.arcs.groove.basking.SyncService;
import co.arcs.groove.basking.Utils;
import co.arcs.groove.basking.event.Events.BuildSyncPlanFinishedEvent;
import co.arcs.groove.basking.event.Events.BuildSyncPlanProgressChangedEvent;
import co.arcs.groove.basking.event.Events.BuildSyncPlanStartedEvent;
import co.arcs.groove.basking.task.BuildSyncPlanTask.SyncPlan;
import co.arcs.groove.basking.task.BuildSyncPlanTask.SyncPlan.Item;
import co.arcs.groove.basking.task.BuildSyncPlanTask.SyncPlan.Item.Action;
import co.arcs.groove.thresher.Song;

/**
 * Builds a plan of what needs to be done to synchronise the contents of the sync directory with a
 * collection of songs.
 */
public class BuildSyncPlanTask implements Task<SyncPlan> {

    public static class SyncPlan {

        private final ImmutableList<Item> items;
        private final int download;
        private final int delete;
        private final int leave;

        public SyncPlan(List<Item> items) {
            this.items = ImmutableList.copyOf(items);
            int delete = 0;
            int download = 0;
            int leave = 0;
            for (Item item : items) {
                if (item.action == Action.DELETE) {
                    delete++;
                } else if (item.action == Action.DOWNLOAD) {
                    download++;
                } else if (item.action == Action.LEAVE) {
                    leave++;
                }
            }
            this.download = download;
            this.delete = delete;
            this.leave = leave;
        }

        public ImmutableList<Item> getItems() {
            return items;
        }

        public int getToDownload() {
            return download;
        }

        public int getToDelete() {
            return delete;
        }

        public int getToLeave() {
            return leave;
        }

        public static class Item {

            public static enum Action {
                DOWNLOAD, DELETE, LEAVE
            }

            private final File file;
            private final Action action;
            private final Song song;

            public Item(File file, Action action, @Nullable Song songToSync) {
                this.file = file;
                this.action = action;
                this.song = songToSync;
            }

            public File getFile() {
                return file;
            }

            public Action getAction() {
                return action;
            }

            public Song getSong() {
                return song;
            }
        }
    }

    public static final String CACHE_FILENAME = WriteCacheFileTask.CACHE_FILENAME;
    private final EventBus bus;
    private final File syncDir;
    private final Set<Song> songs;

    /**
     * Creates a new sync plan task.
     *
     * @param bus
     *         The bus on which to post events.
     * @param syncDir
     *         The directory to be synchronised.
     * @param songs
     *         The songs that {@code syncDir} should contain.
     */
    public BuildSyncPlanTask(EventBus bus, File syncDir, Set<Song> songs) {
        this.bus = bus;
        this.syncDir = syncDir;
        this.songs = songs;
    }

    @Override
    public SyncPlan call() throws Exception {

        bus.post(new BuildSyncPlanStartedEvent(this));

        SyncPlan syncPlan = buildSyncPlanUsingCache(songs);

        if (syncPlan == null) {
            syncPlan = buildSyncPlanByAnalysingFiles(songs);
        }

        bus.post(new BuildSyncPlanFinishedEvent(this,
                syncPlan.download,
                syncPlan.delete,
                syncPlan.leave));

        return syncPlan;
    }

    private SyncPlan buildSyncPlanByAnalysingFiles(Set<Song> songs) {

        // Get a list of all non-hidden mp3 files
        List<File> files = Lists.newArrayList(syncDir.listFiles(new FilenameFilter() {

            @Override
            public boolean accept(File arg0, String arg1) {
                return !arg1.startsWith(".") && arg1.endsWith(SyncService.FINISHED_FILE_EXTENSION);
            }
        }));

        int progress = 0;
        bus.post(new BuildSyncPlanProgressChangedEvent(this, progress++, files.size()));

        Map<Integer, File> existingSongs = Maps.newHashMap();
        for (File f : files) {
            // This operation takes far longer than everything else, so is used as the measure of progress
            Integer id = Utils.decodeId(f);
            if (id != Utils.ID_NONE) {
                existingSongs.put(id, f);
            }
            bus.post(new BuildSyncPlanProgressChangedEvent(this, progress++, files.size()));
        }

        Map<Integer, Song> wantedSongs = Maps.newHashMap();
        for (Song s : songs) {
            wantedSongs.put(s.getId(), s);
        }

        Builder<Item> syncPlanItems = ImmutableList.builder();

        for (Integer id : wantedSongs.keySet()) {
            if (existingSongs.containsKey(id)) {
                // Wanted song already exists, so leave as is
                syncPlanItems.add(new Item(existingSongs.get(id),
                        Action.LEAVE,
                        wantedSongs.get(id)));
                existingSongs.remove(id);
            } else {
                // Wanted song is absent, so download
                syncPlanItems.add(new Item(new File(syncDir,
                        Utils.getDiskName(wantedSongs.get(id))),
                        Action.DOWNLOAD,
                        wantedSongs.get(id)
                ));
            }
        }

        // Remaining songs are present but unwanted, so delete
        for (Integer id : existingSongs.keySet()) {
            syncPlanItems.add(new Item(existingSongs.get(id), Action.DELETE, null));
        }
        return new SyncPlan(syncPlanItems.build());
    }

    /**
     * Reads the cache file and returns a sync plan based on its contents.
     *
     * @return A sync plan, or null if the cache file does not exist.
     */
    @Nullable
    private SyncPlan buildSyncPlanUsingCache(Collection<Song> songs) throws IOException {
        File cacheFile = new File(syncDir, CACHE_FILENAME);
        if (cacheFile.exists()) {

            ImmutableList.Builder<SyncPlan.Item> items = ImmutableList.builder();

            InputStream is = new BufferedInputStream(new FileInputStream(cacheFile));
            Map<Integer, String> cacheMap = CharStreams.readLines(new InputStreamReader(is),
                    WriteCacheFileTask.newCacheFileLineProcessor());
            is.close();

            int progress = 0;

            for (Song song : songs) {
                if (cacheMap.containsKey(song.getId())) {
                    // Wanted song is in cache, so leave as is
                    items.add(new Item(new File(syncDir, cacheMap.get(song.getId())),
                            Action.LEAVE,
                            song));
                    cacheMap.remove(song.getId());
                } else {
                    // Wanted song is absent, so download
                    items.add(new Item(new File(syncDir, Utils.getDiskName(song)),
                            Action.DOWNLOAD,
                            song));
                }
                bus.post(new BuildSyncPlanProgressChangedEvent(this, progress++, songs.size()));
            }

            // Unwanted stuff that's in the cache should be removed
            for (String fileName : cacheMap.values()) {
                items.add(new Item(new File(syncDir, fileName), Action.DELETE, null));
            }

            return new SyncPlan(items.build());
        } else {
            return null;
        }
    }
}

@@ -7213,16 +7213,100 @@
 ess = 0;
+%0A            bus.post(new BuildSyncPlanProgressChangedEvent(this, 0, songs.size()));
 %0A%0A      
@@ -8003,32 +8003,34 @@
 Event(this, 
+++
 progress
 ++, songs.si
@@ -8017,18 +8017,16 @@
 progress
-++
 , songs.

Fix task not reporting full range of progress.