/* Copyright 2016 Software Freedom Conservancy Inc.
 *
 * This software is licensed under the GNU Lesser General Public License
 * (version 2.1 or later).  See the COPYING file in this distribution.
 */

private class VideoUpdates : MonitorableUpdates {
    public Video video;
    
    private bool check_interpretable = false;
    
    public VideoUpdates(Video video) {
        base (video);
        
        this.video = video;
    }
    
    public virtual void set_check_interpretable(bool check) {
        check_interpretable = check;
    }
    
    public override void mark_online() {
        base.mark_online();
        
        set_check_interpretable(true);
    }
    
    public bool is_check_interpretable() {
        return check_interpretable;
    }
    
    public override bool is_all_updated() {
        return (check_interpretable == false) && base.is_all_updated();
    }
}

private class VideoMonitor : MediaMonitor {
    private const int MAX_INTERPRETABLE_CHECKS_PER_CYCLE = 5;
    
    // Performs interpretable check on video. In a background job because
    // this will create a new thumbnail for the video.
    private class VideoInterpretableCheckJob : BackgroundJob {
        // IN
        public Video video;
        
        // OUT
        public Video.InterpretableResults? results = null;
        
        public VideoInterpretableCheckJob(Video video, CompletionCallback? callback = null) {
            base (video, callback);
            this.video = video;
        }
        
        public override void execute() {
            results = video.check_is_interpretable();
        }
    }
    
    // Work queue for video thumbnailing.
    // Note: only using 1 thread. If we want to change this to use multiple
    // threads, we need to put a lock around background_jobs wherever it's modified.
    private Workers workers = new Workers(1, false);
    private uint64 background_jobs = 0;
    
    public VideoMonitor(Cancellable cancellable) {
        base (Video.global, cancellable);
                
        foreach (DataObject obj in Video.global.get_all()) {
            Video video = obj as Video;
            assert (video != null);
            if (!video.get_is_interpretable())
                set_check_interpretable(video, true);
        }
    }
    
    protected override MonitorableUpdates create_updates(Monitorable monitorable) {
        assert(monitorable is Video);
        
        return new VideoUpdates((Video) monitorable);
    }
    
    public override MediaSourceCollection get_media_source_collection() {
        return Video.global;
    }
    
    public override bool is_file_represented(File file) {
        VideoSourceCollection.State state;
        return get_state(file, out state) != null;
    }
    
    public override MediaMonitor.DiscoveredFile notify_file_discovered(File file, FileInfo info,
        out Monitorable monitorable) {
        VideoSourceCollection.State state;
        Video? video = get_state(file, out state);
        if (video == null) {
            monitorable = null;
            
            return MediaMonitor.DiscoveredFile.UNKNOWN;
        }
        
        switch (state) {
            case VideoSourceCollection.State.ONLINE:
            case VideoSourceCollection.State.OFFLINE:
                monitorable = video;
                
                return MediaMonitor.DiscoveredFile.REPRESENTED;
            
            case VideoSourceCollection.State.TRASH:
            default:
                // ignored ... trash always stays in trash
                monitorable = null;
                
                return MediaMonitor.DiscoveredFile.IGNORE;
        }
    }
    
    public override Gee.Collection<Monitorable>? candidates_for_unknown_file(File file, FileInfo info,
        out MediaMonitor.DiscoveredFile result) {
        Gee.Collection<Video> matched = new Gee.ArrayList<Video>();
        Video.global.fetch_by_matching_backing(info, matched);
        
        result = MediaMonitor.DiscoveredFile.UNKNOWN;
        
        return matched;
    }
    
    public override bool notify_file_created(File file, FileInfo info) {
        VideoSourceCollection.State state;
        Video? video = get_state(file, out state);
        if (video == null)
            return false;
        
        update_online(video);
        
        return true;
    }
    
    public override bool notify_file_moved(File old_file, File new_file, FileInfo new_file_info) {
        VideoSourceCollection.State old_state;
        Video? old_video = get_state(old_file, out old_state);
        
        VideoSourceCollection.State new_state;
        Video? new_video = get_state(new_file, out new_state);
        
        // Four possibilities:
        //
        // 1. Moving an existing photo file to a location where no photo is represented
        //    Operation: have the Photo object move with the file.
        // 2. Moving a file with no representative photo to a location where a photo is represented
        //    (i.e. is offline).  Operation: Update the photo (backing has changed).
        // 3. Moving a file with no representative photo to a location with no representative
        //    photo.  Operation: Enqueue for import (if appropriate).
        // 4. Move a file with a representative photo to a location where a photo is represented
        //    Operation: Mark the old photo as offline (or drop editable) and update new photo
        //    (the backing has changed).
        
        if (old_video != null && new_video == null) {
            // 1.
            update_master_file(old_video, new_file);
        } else if (old_video == null && new_video != null) {
            // 2.
            set_check_interpretable(new_video, true);
        } else if (old_video == null && new_video == null) {
            // 3.
            return false;
        } else {
            assert(old_video != null && new_video != null);
            
            // 4.
            update_offline(old_video);
            set_check_interpretable(new_video, true);
        }
        
        return true;
    }
    
    public override bool notify_file_altered(File file) {
        VideoSourceCollection.State state;
        return get_state(file, out state) != null;
    }
    
    public override bool notify_file_attributes_altered(File file) {
        VideoSourceCollection.State state;
        Video? video = get_state(file, out state);
        if (video == null)
            return false;
        
        update_master_file_info_altered(video);
        update_master_file_in_alteration(video, true);
        
        return true;
    }
    
    public override bool notify_file_alteration_completed(File file, FileInfo info) {
        VideoSourceCollection.State state;
        Video? video = get_state(file, out state);
        if (video == null)
            return false;
        
        update_master_file_alterations_completed(video, info);
        
        return true;
    }
    
    public override bool notify_file_deleted(File file) {
        VideoSourceCollection.State state;
        Video? video = get_state(file, out state);
        if (video == null)
            return false;
        
        update_master_file_in_alteration(video, false);
        update_offline(video);
        
        return true;
    }
    
    private Video? get_state(File file, out VideoSourceCollection.State state) {
        File? real_file = null;
        foreach (Monitorable monitorable in get_monitorables()) {
            Video video = (Video) monitorable;
            
            VideoUpdates? updates = get_existing_video_updates(video);
            if (updates == null)
                continue;
            
            if (updates.get_master_file() != null && updates.get_master_file().equal(file)) {
                real_file = video.get_master_file();
                
                break;
            }
        }
        
        return Video.global.get_state_by_file(real_file ?? file, out state);
    }
    
    public VideoUpdates fetch_video_updates(Video video) {
        VideoUpdates? updates = fetch_updates(video) as VideoUpdates;
        assert(updates != null);
        
        return updates;
    }
    
    public VideoUpdates? get_existing_video_updates(Video video) {
        return get_existing_updates(video) as VideoUpdates;
    }
    
    public void set_check_interpretable(Video video, bool check) {
        fetch_video_updates(video).set_check_interpretable(check);
    }
    
    protected override void process_updates(Gee.Collection<MonitorableUpdates> all_updates,
        TransactionController controller, ref int op_count) throws Error {
        base.process_updates(all_updates, controller, ref op_count);
        
        Gee.ArrayList<Video>? check = null;
        
        foreach (MonitorableUpdates monitorable_updates in all_updates) {
            if (op_count >= MAX_OPERATIONS_PER_CYCLE)
                break;
            
            // use a separate limit on interpretable checks because they're more expensive than
            // simple database commands
            if (check != null && check.size >= MAX_INTERPRETABLE_CHECKS_PER_CYCLE)
                break;
            
            VideoUpdates? updates = monitorable_updates as VideoUpdates;
            if (updates == null)
                continue;
            
            if (updates.is_check_interpretable()) {
                if (check == null)
                    check = new Gee.ArrayList<Video>();
                
                check.add(updates.video);
                updates.set_check_interpretable(false);
                op_count++;
            }
        }
        
        if (check != null) {
            mdbg("Checking interpretable for %d videos".printf(check.size));
            
            Video.notify_offline_thumbs_regenerated();
            
            background_jobs += check.size;
            foreach (Video video in check)
                workers.enqueue(new VideoInterpretableCheckJob(video, on_interpretable_check_complete));
        }
    }
    
    void on_interpretable_check_complete(BackgroundJob j) {
        VideoInterpretableCheckJob job = (VideoInterpretableCheckJob) j;
        
        job.results.foreground_finish();
        
        --background_jobs;
        if (background_jobs <= 0)
            Video.notify_normal_thumbs_regenerated();
    }
}