1/*
2* Copyright (c) 2010-2013 Yorba Foundation
3*
4* This program is free software; you can redistribute it and/or
5* modify it under the terms of the GNU Lesser General Public
6* License as published by the Free Software Foundation; either
7* version 2.1 of the License, or (at your option) any later version.
8*
9* This program is distributed in the hope that it will be useful,
10* but WITHOUT ANY WARRANTY; without even the implied warranty of
11* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
12* General Public License for more details.
13*
14* You should have received a copy of the GNU General Public
15* License along with this program; if not, write to the
16* Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
17* Boston, MA 02110-1301 USA
18*/
19
20private class PhotoUpdates : MonitorableUpdates {
21    public LibraryPhoto photo;
22
23    public bool reimport_master = false;
24    public bool reimport_editable = false;
25    public bool reimport_raw_developments = false;
26    public File? editable_file = null;
27    public bool editable_file_info_altered = false;
28    public bool raw_developer_file_info_altered = false;
29    public FileInfo? editable_file_info = null;
30    public bool editable_in_alteration = false;
31    public bool raw_development_in_alteration = false;
32    public bool revert_to_master = false;
33    public Gee.Collection<File> developer_files = new Gee.ArrayList<File> ();
34
35    public PhotoUpdates (LibraryPhoto photo) {
36        base (photo);
37
38        this.photo = photo;
39    }
40
41    public override void mark_offline () {
42        base.mark_offline ();
43
44        reimport_master = false;
45        reimport_editable = false;
46        reimport_raw_developments = false;
47    }
48
49    public bool is_reimport_master () {
50        return reimport_master;
51    }
52
53    public bool is_reimport_editable () {
54        return reimport_editable;
55    }
56
57    public File? get_editable_file () {
58        return editable_file;
59    }
60
61    public FileInfo? get_editable_file_info () {
62        return editable_file_info;
63    }
64
65    public Gee.Collection<File> get_raw_developer_files () {
66        return developer_files;
67    }
68
69    public override bool is_in_alteration () {
70        return base.is_in_alteration () || editable_in_alteration;
71    }
72
73    public bool is_revert_to_master () {
74        return revert_to_master;
75    }
76
77    public virtual void set_editable_file (File? file) {
78        // if reverting, don't bother
79        if (file != null && revert_to_master)
80            return;
81
82        editable_file = file;
83    }
84
85    public virtual void set_editable_file_info (FileInfo? info) {
86        // if reverting, don't bother
87        if (info != null && revert_to_master)
88            return;
89
90        editable_file_info = info;
91        if (info == null)
92            editable_file_info_altered = false;
93    }
94
95    public virtual void set_editable_file_info_altered (bool altered) {
96        // if reverting, don't bother
97        if (altered && revert_to_master)
98            return;
99
100        editable_file_info_altered = altered;
101    }
102
103    public virtual void set_editable_in_alteration (bool in_alteration) {
104        editable_in_alteration = in_alteration;
105    }
106
107    public virtual void set_raw_development_in_alteration (bool in_alteration) {
108        raw_development_in_alteration = in_alteration;
109    }
110
111    public virtual void set_raw_developer_file_info_altered (bool altered) {
112        raw_developer_file_info_altered = altered;
113    }
114
115    public virtual void set_revert_to_master (bool revert) {
116        if (revert) {
117            // this means nothing any longer
118            reimport_editable = false;
119            editable_file = null;
120            editable_file_info = null;
121        }
122
123        revert_to_master = revert;
124    }
125
126    public virtual void add_raw_developer_file (File file) {
127        developer_files.add (file);
128    }
129
130    public virtual void clear_raw_developer_files () {
131        developer_files.clear ();
132    }
133
134    public virtual void set_reimport_master (bool reimport) {
135        reimport_master = reimport;
136
137        if (reimport)
138            mark_online ();
139    }
140
141    public virtual void set_reimport_editable (bool reimport) {
142        // if reverting or going offline, don't bother
143        if (reimport && (revert_to_master || is_set_offline ()))
144            return;
145
146        reimport_editable = reimport;
147    }
148
149    public virtual void set_reimport_raw_developments (bool reimport) {
150        reimport_raw_developments = reimport;
151
152        if (reimport)
153            mark_online ();
154    }
155
156    public override bool is_all_updated () {
157        return base.is_all_updated ()
158               && reimport_master == false
159               && reimport_editable == false
160               && editable_file == null
161               && editable_file_info_altered == false
162               && editable_file_info == null
163               && editable_in_alteration == false
164               && developer_files.size == 0
165               && raw_developer_file_info_altered == false
166               && revert_to_master == false;
167    }
168}
169
170private class PhotoMonitor : MediaMonitor {
171    private const int MAX_REIMPORT_JOBS_PER_CYCLE = 20;
172    private const int MAX_REVERTS_PER_CYCLE = 5;
173
174    private class ReimportMasterJob : BackgroundJob {
175        public LibraryPhoto photo;
176        public Photo.ReimportMasterState reimport_state = null;
177        public bool mark_online = false;
178        public Error err = null;
179
180        public ReimportMasterJob (PhotoMonitor owner, LibraryPhoto photo) {
181            base (owner, owner.on_master_reimported, new Cancellable (),
182                  owner.on_master_reimport_cancelled);
183
184            this.photo = photo;
185        }
186
187        public override void execute () {
188            try {
189                mark_online = photo.prepare_for_reimport_master (out reimport_state);
190            } catch (Error err) {
191                this.err = err;
192            }
193        }
194    }
195
196    private class ReimportEditableJob : BackgroundJob {
197        public LibraryPhoto photo;
198        public Photo.ReimportEditableState state = null;
199        public bool success = false;
200        public Error err = null;
201
202        public ReimportEditableJob (PhotoMonitor owner, LibraryPhoto photo) {
203            base (owner, owner.on_editable_reimported, new Cancellable (),
204                  owner.on_editable_reimport_cancelled);
205
206            this.photo = photo;
207        }
208
209        public override void execute () {
210            try {
211                success = photo.prepare_for_reimport_editable (out state);
212            } catch (Error err) {
213                this.err = err;
214            }
215        }
216    }
217
218    private class ReimportRawDevelopmentJob : BackgroundJob {
219        public LibraryPhoto photo;
220        public Photo.ReimportRawDevelopmentState state = null;
221        public bool success = false;
222        public Error err = null;
223
224        public ReimportRawDevelopmentJob (PhotoMonitor owner, LibraryPhoto photo) {
225            base (owner, owner.on_raw_development_reimported, new Cancellable (),
226                  owner.on_raw_development_reimport_cancelled);
227
228            this.photo = photo;
229        }
230
231        public override void execute () {
232            try {
233                success = photo.prepare_for_reimport_raw_development (out state);
234            } catch (Error err) {
235                this.err = err;
236            }
237        }
238    }
239
240    private Workers workers;
241    private Gee.ArrayList<LibraryPhoto> matched_editables = new Gee.ArrayList<LibraryPhoto> ();
242    private Gee.ArrayList<LibraryPhoto> matched_developments = new Gee.ArrayList<LibraryPhoto> ();
243    private Gee.HashMap<LibraryPhoto, ReimportMasterJob> master_reimport_pending = new Gee.HashMap <
244    LibraryPhoto, ReimportMasterJob > ();
245    private Gee.HashMap<LibraryPhoto, ReimportEditableJob> editable_reimport_pending =
246        new Gee.HashMap<LibraryPhoto, ReimportEditableJob> ();
247    private Gee.HashMap<LibraryPhoto, ReimportRawDevelopmentJob> raw_developments_reimport_pending =
248        new Gee.HashMap<LibraryPhoto, ReimportRawDevelopmentJob> ();
249
250    public PhotoMonitor (Workers workers, Cancellable cancellable) {
251        base (LibraryPhoto.global, cancellable);
252
253        this.workers = workers;
254    }
255
256    protected override MonitorableUpdates create_updates (Monitorable monitorable) {
257        assert (monitorable is LibraryPhoto);
258
259        return new PhotoUpdates ((LibraryPhoto) monitorable);
260    }
261
262    public override MediaSourceCollection get_media_source_collection () {
263        return LibraryPhoto.global;
264    }
265
266    public override bool is_file_represented (File file) {
267        LibraryPhotoSourceCollection.State state;
268        return get_photo_state_by_file (file, out state) != null;
269    }
270
271    public override void close () {
272        foreach (ReimportMasterJob job in master_reimport_pending.values)
273            job.cancel ();
274
275        foreach (ReimportEditableJob job in editable_reimport_pending.values)
276            job.cancel ();
277
278        foreach (ReimportRawDevelopmentJob job in raw_developments_reimport_pending.values)
279            job.cancel ();
280
281        base.close ();
282    }
283
284    private void cancel_reimports (LibraryPhoto photo) {
285        ReimportMasterJob? master_job = master_reimport_pending.get (photo);
286        if (master_job != null)
287            master_job.cancel ();
288
289        ReimportEditableJob? editable_job = editable_reimport_pending.get (photo);
290        if (editable_job != null)
291            editable_job.cancel ();
292    }
293
294    public override MediaMonitor.DiscoveredFile notify_file_discovered (File file, FileInfo info,
295            out Monitorable monitorable) {
296        LibraryPhotoSourceCollection.State state;
297        LibraryPhoto? photo = get_photo_state_by_file (file, out state);
298        if (photo == null) {
299            monitorable = null;
300
301            return MediaMonitor.DiscoveredFile.UNKNOWN;
302        }
303
304        switch (state) {
305        case LibraryPhotoSourceCollection.State.ONLINE:
306        case LibraryPhotoSourceCollection.State.OFFLINE:
307            monitorable = photo;
308
309            return MediaMonitor.DiscoveredFile.REPRESENTED;
310
311        case LibraryPhotoSourceCollection.State.TRASH:
312        case LibraryPhotoSourceCollection.State.EDITABLE:
313        case LibraryPhotoSourceCollection.State.DEVELOPER:
314        default:
315            // ignored ... trash always stays in trash, offline or not, and editables are
316            // simply attached to online/offline photos
317            monitorable = null;
318
319            return MediaMonitor.DiscoveredFile.IGNORE;
320        }
321    }
322
323    public override Gee.Collection<Monitorable>? candidates_for_unknown_file (File file, FileInfo info,
324            out MediaMonitor.DiscoveredFile result) {
325        // reset with each call
326        matched_editables.clear ();
327        matched_developments.clear ();
328
329        Gee.Collection<LibraryPhoto> matched_masters = new Gee.ArrayList<LibraryPhoto> ();
330        LibraryPhoto.global.fetch_by_matching_backing (info, matched_masters, matched_editables,
331                matched_developments);
332        if (matched_masters.size > 0) {
333            result = MediaMonitor.DiscoveredFile.UNKNOWN;
334
335            return matched_masters;
336        }
337
338        if (matched_editables.size == 0 && matched_developments.size == 0) {
339            result = MediaMonitor.DiscoveredFile.UNKNOWN;
340
341            return null;
342        }
343
344        // for editable files and raw developments, trust file characteristics alone
345        if (matched_editables.size > 0) {
346            LibraryPhoto match = matched_editables[0];
347            if (matched_editables.size > 1) {
348                warning ("Unknown file %s could be matched with %d photos; giving to %s, dropping others",
349                         file.get_path (), matched_editables.size, match.to_string ());
350                for (int ctr = 1; ctr < matched_editables.size; ctr++) {
351                    if (!matched_editables[ctr].does_editable_exist ())
352                        matched_editables[ctr].revert_to_master ();
353                }
354            }
355
356            update_editable_file (match, file);
357        }
358
359        if (matched_developments.size > 0) {
360            LibraryPhoto match_raw = matched_developments[0];
361            if (matched_developments.size > 1) {
362                warning ("Unknown file %s could be matched with %d photos; giving to %s, dropping others",
363                         file.get_path (), matched_developments.size, match_raw.to_string ());
364            }
365
366            update_raw_development_file (match_raw, file);
367        }
368
369        result = MediaMonitor.DiscoveredFile.IGNORE;
370
371        return null;
372    }
373
374    public override File[]? get_auxilliary_backing_files (Monitorable monitorable) {
375        LibraryPhoto photo = (LibraryPhoto) monitorable;
376        File[] files = new File[0];
377
378        // Editable.
379        if (photo.has_editable ())
380            files += photo.get_editable_file ();
381
382        // Raw developments.
383        Gee.Collection<File>? raw_files = photo.get_raw_developer_files ();
384        if (raw_files != null)
385            foreach (File f in raw_files)
386                files += f;
387
388        // Return null if no files.
389        return files.length > 0 ? files : null;
390    }
391
392    public override void update_backing_file_info (Monitorable monitorable, File file, FileInfo? info) {
393        LibraryPhoto photo = (LibraryPhoto) monitorable;
394
395        if (get_master_file (photo).equal (file))
396            check_for_master_changes (photo, info);
397        else if (get_editable_file (photo) != null && get_editable_file (photo).equal (file))
398            check_for_editable_changes (photo, info);
399        else if (get_raw_development_files (photo) != null) {
400            foreach (File f in get_raw_development_files (photo)) {
401                if (f.equal (file))
402                    check_for_raw_development_changes (photo, info);
403            }
404        }
405    }
406
407    public override void notify_discovery_completing () {
408        matched_editables.clear ();
409    }
410
411    // If filesize has changed, treat that as a full-blown modification
412    // and reimport ... this is problematic if only the metadata has changed, but so be it.
413    //
414    // TODO: We could do an MD5 check for more accuracy.
415    private void check_for_master_changes (LibraryPhoto photo, FileInfo? info) {
416        // if not present, offline state is already taken care of by LibraryMonitor
417        if (info == null)
418            return;
419
420        BackingPhotoRow state = photo.get_master_photo_row ();
421        if (state.matches_file_info (info))
422            return;
423
424        if (state.is_touched (info)) {
425            update_master_file_info_altered (photo);
426            update_master_file_alterations_completed (photo, info);
427        } else {
428            update_reimport_master (photo);
429        }
430    }
431
432    private void check_for_editable_changes (LibraryPhoto photo, FileInfo? info) {
433        if (info == null) {
434            update_revert_to_master (photo);
435
436            return;
437        }
438
439        // If state matches, done -- editables have no bearing on a photo's offline status.
440        BackingPhotoRow? state = photo.get_editable_photo_row ();
441        if (state == null || state.matches_file_info (info))
442            return;
443
444        if (state.is_touched (info)) {
445            update_editable_file_info_altered (photo);
446            update_editable_file_alterations_completed (photo, info);
447        } else {
448            update_reimport_editable (photo);
449        }
450    }
451
452    private void check_for_raw_development_changes (LibraryPhoto photo, FileInfo? info) {
453        if (info == null) {
454            // Switch back to default for safety.
455            photo.set_raw_developer (RawDeveloper.SHOTWELL);
456
457            return;
458        }
459
460        Gee.Collection<BackingPhotoRow>? rows = photo.get_raw_development_photo_rows ();
461        if (rows == null)
462            return;
463
464        // Look through all possible rows, if we find a file with a matching name or info,
465        // assume we found our man.
466        foreach (BackingPhotoRow row in rows) {
467            if (row.matches_file_info (info))
468                return;
469            if (info.get_name () == row.filepath) {
470                if (row.is_touched (info)) {
471                    update_raw_development_file_info_altered (photo);
472                    update_raw_development_file_alterations_completed (photo);
473                } else {
474                    update_reimport_raw_developments (photo);
475                }
476
477                break;
478            }
479        }
480    }
481
482    public override bool notify_file_created (File file, FileInfo info) {
483        LibraryPhotoSourceCollection.State state;
484        LibraryPhoto? photo = get_photo_state_by_file (file, out state);
485        if (photo == null)
486            return false;
487
488        switch (state) {
489        case LibraryPhotoSourceCollection.State.ONLINE:
490        case LibraryPhotoSourceCollection.State.TRASH:
491        case LibraryPhotoSourceCollection.State.EDITABLE:
492        case LibraryPhotoSourceCollection.State.DEVELOPER:
493            // do nothing, although this is unexpected
494            warning ("File %s created in %s state", file.get_path (), state.to_string ());
495            break;
496
497        case LibraryPhotoSourceCollection.State.OFFLINE:
498            mdbg ("Will mark %s online".printf (photo.to_string ()));
499            update_online (photo);
500            break;
501
502        default:
503            error ("Unknown LibraryPhoto collection state %s", state.to_string ());
504        }
505
506        return true;
507    }
508
509    public override bool notify_file_moved (File old_file, File new_file, FileInfo info) {
510        LibraryPhotoSourceCollection.State old_state;
511        LibraryPhoto? old_photo = get_photo_state_by_file (old_file, out old_state);
512
513        LibraryPhotoSourceCollection.State new_state;
514        LibraryPhoto? new_photo = get_photo_state_by_file (new_file, out new_state);
515
516        // Four possibilities:
517        //
518        // 1. Moving an existing photo file to a location where no photo is represented
519        //    Operation: have the Photo object move with the file.
520        // 2. Moving a file with no representative photo to a location where a photo is represented
521        //    (i.e. is offline).  Operation: Update the photo (backing has changed).
522        // 3. Moving a file with no representative photo to a location with no representative
523        //    photo.  Operation: Enqueue for import (if appropriate).
524        // 4. Move a file with a representative photo to a location where a photo is represented
525        //    Operation: Mark the old photo as offline (or drop editable) and update new photo
526        //    (the backing has changed).
527
528        if (old_photo != null && new_photo == null) {
529            // 1.
530            switch (old_state) {
531            case LibraryPhotoSourceCollection.State.ONLINE:
532            case LibraryPhotoSourceCollection.State.TRASH:
533            case LibraryPhotoSourceCollection.State.OFFLINE:
534                mdbg ("Will set new master file for %s to %s".printf (old_photo.to_string (),
535                        new_file.get_path ()));
536                update_master_file (old_photo, new_file);
537                break;
538
539            case LibraryPhotoSourceCollection.State.EDITABLE:
540                mdbg ("Will set new editable file for %s to %s".printf (old_photo.to_string (),
541                        new_file.get_path ()));
542                update_editable_file (old_photo, new_file);
543                break;
544
545            case LibraryPhotoSourceCollection.State.DEVELOPER:
546                mdbg ("Will set new raw development file for %s to %s".printf (old_photo.to_string (),
547                        new_file.get_path ()));
548                update_raw_development_file (old_photo, new_file);
549                break;
550
551            default:
552                error ("Unknown LibraryPhoto collection state %s", old_state.to_string ());
553            }
554        } else if (old_photo == null && new_photo != null) {
555            // 2.
556            switch (new_state) {
557            case LibraryPhotoSourceCollection.State.ONLINE:
558            case LibraryPhotoSourceCollection.State.TRASH:
559            case LibraryPhotoSourceCollection.State.OFFLINE:
560                mdbg ("Will reimport master file for %s".printf (new_photo.to_string ()));
561                update_reimport_master (new_photo);
562                break;
563
564            case LibraryPhotoSourceCollection.State.EDITABLE:
565                mdbg ("Will reimport editable file for %s".printf (new_photo.to_string ()));
566                update_reimport_editable (new_photo);
567                break;
568
569            case LibraryPhotoSourceCollection.State.DEVELOPER:
570                mdbg ("Will reimport raw development file for %s".printf (new_photo.to_string ()));
571                update_reimport_raw_developments (new_photo);
572                break;
573
574            default:
575                error ("Unknown LibraryPhoto collection state %s", new_state.to_string ());
576            }
577        } else if (old_photo == null && new_photo == null) {
578            // 3.
579            return false;
580        } else {
581            assert (old_photo != null && new_photo != null);
582            // 4.
583            switch (old_state) {
584            case LibraryPhotoSourceCollection.State.ONLINE:
585                mdbg ("Will mark offline %s".printf (old_photo.to_string ()));
586                update_offline (old_photo);
587                break;
588
589            case LibraryPhotoSourceCollection.State.TRASH:
590            case LibraryPhotoSourceCollection.State.OFFLINE:
591                // do nothing
592                break;
593
594            case LibraryPhotoSourceCollection.State.EDITABLE:
595                mdbg ("Will revert %s to master".printf (old_photo.to_string ()));
596                update_revert_to_master (old_photo);
597                break;
598
599            case LibraryPhotoSourceCollection.State.DEVELOPER:
600                // do nothing
601                break;
602
603            default:
604                error ("Unknown LibraryPhoto collection state %s", old_state.to_string ());
605            }
606
607            switch (new_state) {
608            case LibraryPhotoSourceCollection.State.ONLINE:
609            case LibraryPhotoSourceCollection.State.TRASH:
610            case LibraryPhotoSourceCollection.State.OFFLINE:
611                mdbg ("Will reimport master file for %s".printf (new_photo.to_string ()));
612                update_reimport_master (new_photo);
613                break;
614
615            case LibraryPhotoSourceCollection.State.EDITABLE:
616                mdbg ("Will reimport editable file for %s".printf (new_photo.to_string ()));
617                update_reimport_editable (new_photo);
618                break;
619
620            case LibraryPhotoSourceCollection.State.DEVELOPER:
621                mdbg ("Will reimport raw development file for %s".printf (new_photo.to_string ()));
622                update_reimport_raw_developments (new_photo);
623                break;
624
625            default:
626                error ("Unknown LibraryPhoto collection state %s", new_state.to_string ());
627            }
628        }
629
630        return true;
631    }
632
633    public override bool notify_file_altered (File file) {
634        LibraryPhotoSourceCollection.State state;
635        LibraryPhoto? photo = get_photo_state_by_file (file, out state);
636        if (photo == null)
637            return false;
638
639        switch (state) {
640        case LibraryPhotoSourceCollection.State.ONLINE:
641        case LibraryPhotoSourceCollection.State.OFFLINE:
642        case LibraryPhotoSourceCollection.State.TRASH:
643            mdbg ("Will reimport master for %s".printf (photo.to_string ()));
644            update_reimport_master (photo);
645            update_master_file_in_alteration (photo, true);
646            break;
647
648        case LibraryPhotoSourceCollection.State.EDITABLE:
649            mdbg ("Will reimport editable for %s".printf (photo.to_string ()));
650            update_reimport_editable (photo);
651            update_editable_file_in_alteration (photo, true);
652            break;
653
654        case LibraryPhotoSourceCollection.State.DEVELOPER:
655            mdbg ("Will reimport raw development for %s".printf (photo.to_string ()));
656            update_reimport_raw_developments (photo);
657            update_raw_development_file_in_alteration (photo, true);
658            break;
659
660        default:
661            error ("Unknown LibraryPhoto collection state %s", state.to_string ());
662        }
663
664        return true;
665    }
666
667    public override bool notify_file_attributes_altered (File file) {
668        LibraryPhotoSourceCollection.State state;
669        LibraryPhoto? photo = get_photo_state_by_file (file, out state);
670        if (photo == null)
671            return false;
672
673        switch (state) {
674        case LibraryPhotoSourceCollection.State.ONLINE:
675        case LibraryPhotoSourceCollection.State.TRASH:
676            mdbg ("Will update master file info for %s".printf (photo.to_string ()));
677            update_master_file_info_altered (photo);
678            update_master_file_in_alteration (photo, true);
679            break;
680
681        case LibraryPhotoSourceCollection.State.OFFLINE:
682            // do nothing, but unexpected
683            warning ("File %s attributes altered in %s state", file.get_path (),
684                     state.to_string ());
685            update_master_file_in_alteration (photo, true);
686            break;
687
688        case LibraryPhotoSourceCollection.State.EDITABLE:
689            mdbg ("Will update editable file info for %s".printf (photo.to_string ()));
690            update_editable_file_info_altered (photo);
691            update_editable_file_in_alteration (photo, true);
692            break;
693
694        case LibraryPhotoSourceCollection.State.DEVELOPER:
695            mdbg ("Will update raw development file info for %s".printf (photo.to_string ()));
696            update_raw_development_file_info_altered (photo);
697            update_raw_development_file_in_alteration (photo, true);
698            break;
699
700        default:
701            error ("Unknown LibraryPhoto collection state %s", state.to_string ());
702        }
703
704        return true;
705    }
706
707    public override bool notify_file_alteration_completed (File file, FileInfo info) {
708        LibraryPhotoSourceCollection.State state;
709        LibraryPhoto? photo = get_photo_state_by_file (file, out state);
710        if (photo == null)
711            return false;
712
713        switch (state) {
714        case LibraryPhotoSourceCollection.State.ONLINE:
715        case LibraryPhotoSourceCollection.State.TRASH:
716        case LibraryPhotoSourceCollection.State.OFFLINE:
717            update_master_file_alterations_completed (photo, info);
718            break;
719
720        case LibraryPhotoSourceCollection.State.EDITABLE:
721            update_editable_file_alterations_completed (photo, info);
722            break;
723
724        case LibraryPhotoSourceCollection.State.DEVELOPER:
725            update_raw_development_file_alterations_completed (photo);
726            break;
727
728        default:
729            error ("Unknown LibraryPhoto collection state %s", state.to_string ());
730        }
731
732        return true;
733    }
734
735    public override bool notify_file_deleted (File file) {
736        LibraryPhotoSourceCollection.State state;
737        LibraryPhoto? photo = get_photo_state_by_file (file, out state);
738        if (photo == null)
739            return false;
740
741        switch (state) {
742        case LibraryPhotoSourceCollection.State.ONLINE:
743            mdbg ("Will mark %s offline".printf (photo.to_string ()));
744            update_offline (photo);
745            update_master_file_in_alteration (photo, false);
746            break;
747
748        case LibraryPhotoSourceCollection.State.TRASH:
749        case LibraryPhotoSourceCollection.State.OFFLINE:
750            // do nothing / already knew this
751            update_master_file_in_alteration (photo, false);
752            break;
753
754        case LibraryPhotoSourceCollection.State.EDITABLE:
755            mdbg ("Will revert %s to master".printf (photo.to_string ()));
756            update_revert_to_master (photo);
757            update_editable_file_in_alteration (photo, false);
758            break;
759
760        case LibraryPhotoSourceCollection.State.DEVELOPER:
761            mdbg ("Will revert %s to master".printf (photo.to_string ()));
762            update_revert_to_master (photo);
763            update_editable_file_in_alteration (photo, false);
764            update_raw_development_file_in_alteration (photo, false);
765            break;
766
767        default:
768            error ("Unknown LibraryPhoto collection state %s", state.to_string ());
769        }
770
771        return true;
772    }
773
774    protected override void on_media_source_destroyed (DataSource source) {
775        base.on_media_source_destroyed (source);
776
777        cancel_reimports ((LibraryPhoto) source);
778    }
779
780    private LibraryPhoto? get_photo_state_by_file (File file, out LibraryPhotoSourceCollection.State state) {
781        File? real_file = null;
782        if (has_pending_updates ()) {
783            foreach (Monitorable monitorable in get_monitorables ()) {
784                LibraryPhoto photo = (LibraryPhoto) monitorable;
785
786                PhotoUpdates? updates = get_existing_photo_updates (photo);
787                if (updates == null)
788                    continue;
789
790                if (updates.get_master_file () != null && updates.get_master_file ().equal (file)) {
791                    real_file = photo.get_master_file ();
792
793                    break;
794                }
795
796                if (updates.get_editable_file () != null && updates.get_editable_file ().equal (file)) {
797                    real_file = photo.get_editable_file ();
798
799                    // if the photo's "real" editable file is null, then this file hasn't been
800                    // associated with it (yet) so fake the call
801                    if (real_file == null) {
802                        state = LibraryPhotoSourceCollection.State.EDITABLE;
803
804                        return photo;
805                    }
806
807                    break;
808                }
809
810                if (updates.get_raw_developer_files () != null) {
811                    bool found = false;
812                    foreach (File raw in updates.get_raw_developer_files ()) {
813                        if (raw.equal (file)) {
814                            found = true;
815
816                            break;
817                        }
818                    }
819
820                    if (found) {
821                        Gee.Collection<File>? developed = photo.get_raw_developer_files ();
822                        if (developed != null) {
823                            foreach (File f in developed) {
824                                if (f.equal (file)) {
825                                    real_file = f;
826                                    state = LibraryPhotoSourceCollection.State.DEVELOPER;
827
828                                    break;
829                                }
830                            }
831
832                        }
833
834                        break;
835                    }
836                }
837            }
838        }
839
840        return LibraryPhoto.global.get_state_by_file (real_file ?? file, out state);
841    }
842
843    public PhotoUpdates fetch_photo_updates (LibraryPhoto photo) {
844        return (PhotoUpdates) fetch_updates (photo);
845    }
846
847    public PhotoUpdates? get_existing_photo_updates (LibraryPhoto photo) {
848        return get_existing_updates (photo) as PhotoUpdates;
849    }
850
851    public void update_reimport_master (LibraryPhoto photo) {
852        fetch_photo_updates (photo).set_reimport_master (true);
853
854        // cancel outstanding reimport
855        if (master_reimport_pending.has_key (photo))
856            master_reimport_pending.get (photo).cancel ();
857    }
858
859    public void update_reimport_editable (LibraryPhoto photo) {
860        fetch_photo_updates (photo).set_reimport_editable (true);
861
862        // cancel outstanding reimport
863        if (editable_reimport_pending.has_key (photo))
864            editable_reimport_pending.get (photo).cancel ();
865    }
866
867    public void update_reimport_raw_developments (LibraryPhoto photo) {
868        fetch_photo_updates (photo).set_reimport_raw_developments (true);
869
870        // cancel outstanding reimport
871        if (raw_developments_reimport_pending.has_key (photo))
872            raw_developments_reimport_pending.get (photo).cancel ();
873    }
874
875    public File? get_editable_file (LibraryPhoto photo) {
876        PhotoUpdates? updates = get_existing_photo_updates (photo);
877
878        return (updates != null && updates.get_editable_file () != null) ? updates.get_editable_file ()
879               : photo.get_editable_file ();
880    }
881
882    public Gee.Collection<File>? get_raw_development_files (LibraryPhoto photo) {
883        PhotoUpdates? updates = get_existing_photo_updates (photo);
884
885        return (updates != null && updates.get_raw_developer_files () != null) ?
886               updates.get_raw_developer_files () : photo.get_raw_developer_files ();
887    }
888
889    public void update_editable_file (LibraryPhoto photo, File file) {
890        fetch_photo_updates (photo).set_editable_file (file);
891    }
892
893    public void update_editable_file_info_altered (LibraryPhoto photo) {
894        fetch_photo_updates (photo).set_editable_file_info_altered (true);
895    }
896
897    public void update_raw_development_file (LibraryPhoto photo, File file) {
898        fetch_photo_updates (photo).add_raw_developer_file (file);
899    }
900
901    public void update_raw_development_file_info_altered (LibraryPhoto photo) {
902        fetch_photo_updates (photo).set_raw_developer_file_info_altered (true);
903    }
904
905    public void update_editable_file_in_alteration (LibraryPhoto photo, bool in_alteration) {
906        fetch_photo_updates (photo).set_editable_in_alteration (in_alteration);
907    }
908
909    public void update_editable_file_alterations_completed (LibraryPhoto photo, FileInfo info) {
910        fetch_photo_updates (photo).set_editable_file_info (info);
911        fetch_photo_updates (photo).set_editable_in_alteration (false);
912    }
913
914    public void update_raw_development_file_in_alteration (LibraryPhoto photo, bool in_alteration) {
915        fetch_photo_updates (photo).set_raw_development_in_alteration (in_alteration);
916    }
917
918    public void update_raw_development_file_alterations_completed (LibraryPhoto photo) {
919        fetch_photo_updates (photo).set_raw_development_in_alteration (false);
920    }
921
922    public void update_revert_to_master (LibraryPhoto photo) {
923        fetch_photo_updates (photo).set_revert_to_master (true);
924    }
925
926    protected override void process_updates (Gee.Collection<MonitorableUpdates> all_updates,
927            TransactionController controller, ref int op_count) throws Error {
928        base.process_updates (all_updates, controller, ref op_count);
929
930        Gee.Map<LibraryPhoto, File> set_editable_file = null;
931        Gee.Map<LibraryPhoto, FileInfo> set_editable_file_info = null;
932        Gee.Map<LibraryPhoto, Gee.Collection<File>> set_raw_developer_files = null;
933        Gee.ArrayList<LibraryPhoto> revert_to_master = null;
934        Gee.ArrayList<LibraryPhoto> reimport_master = null;
935        Gee.ArrayList<LibraryPhoto> reimport_editable = null;
936        Gee.ArrayList<LibraryPhoto> reimport_raw_developments = null;
937        int reimport_job_count = 0;
938
939        foreach (MonitorableUpdates monitorable_updates in all_updates) {
940            if (op_count >= MAX_OPERATIONS_PER_CYCLE)
941                break;
942
943            PhotoUpdates? updates = monitorable_updates as PhotoUpdates;
944            if (updates == null)
945                continue;
946
947            if (updates.get_editable_file () != null) {
948                if (set_editable_file == null)
949                    set_editable_file = new Gee.HashMap<LibraryPhoto, File> ();
950
951                set_editable_file.set (updates.photo, updates.get_editable_file ());
952                updates.set_editable_file (null);
953                op_count++;
954            }
955
956            if (updates.get_editable_file_info () != null) {
957                if (set_editable_file_info == null)
958                    set_editable_file_info = new Gee.HashMap<LibraryPhoto, FileInfo> ();
959
960                set_editable_file_info.set (updates.photo, updates.get_editable_file_info ());
961                updates.set_editable_file_info (null);
962                op_count++;
963            }
964
965            if (updates.get_raw_developer_files () != null) {
966                if (set_raw_developer_files == null)
967                    set_raw_developer_files = new Gee.HashMap<LibraryPhoto, Gee.Collection<File>> ();
968
969                set_raw_developer_files.set (updates.photo, updates.get_raw_developer_files ());
970                updates.clear_raw_developer_files ();
971                op_count++;
972            }
973
974            if (updates.is_revert_to_master ()) {
975                if (revert_to_master == null)
976                    revert_to_master = new Gee.ArrayList<LibraryPhoto> ();
977
978                if (revert_to_master.size < MAX_REVERTS_PER_CYCLE) {
979                    revert_to_master.add (updates.photo);
980                    updates.set_revert_to_master (false);
981                }
982                op_count++;
983            }
984
985            if (updates.is_reimport_master () && reimport_job_count < MAX_REIMPORT_JOBS_PER_CYCLE) {
986                if (reimport_master == null)
987                    reimport_master = new Gee.ArrayList<LibraryPhoto> ();
988
989                reimport_master.add (updates.photo);
990                updates.set_reimport_master (false);
991                reimport_job_count++;
992                op_count++;
993            }
994
995            if (updates.is_reimport_editable () && reimport_job_count < MAX_REIMPORT_JOBS_PER_CYCLE) {
996                if (reimport_editable == null)
997                    reimport_editable = new Gee.ArrayList<LibraryPhoto> ();
998
999                reimport_editable.add (updates.photo);
1000                updates.set_reimport_editable (false);
1001                reimport_job_count++;
1002                op_count++;
1003            }
1004        }
1005
1006        if (set_editable_file != null) {
1007            mdbg ("Changing editable file of %d photos".printf (set_editable_file.size));
1008
1009            try {
1010                Photo.set_many_editable_file (set_editable_file);
1011            } catch (DatabaseError err) {
1012                AppWindow.database_error (err);
1013            }
1014        }
1015
1016        if (set_editable_file_info != null) {
1017            mdbg ("Updating %d editable files timestamps".printf (set_editable_file_info.size));
1018
1019            try {
1020                Photo.update_many_editable_timestamps (set_editable_file_info);
1021            } catch (DatabaseError err) {
1022                AppWindow.database_error (err);
1023            }
1024        }
1025
1026        if (revert_to_master != null) {
1027            mdbg ("Reverting %d photos to master".printf (revert_to_master.size));
1028
1029            foreach (LibraryPhoto photo in revert_to_master)
1030                photo.revert_to_master ();
1031        }
1032
1033        //
1034        // Now that the metadata has been updated, deal with imports and reimports
1035        //
1036
1037        if (reimport_master != null) {
1038            mdbg ("Reimporting %d masters".printf (reimport_master.size));
1039
1040            foreach (LibraryPhoto photo in reimport_master) {
1041                assert (!master_reimport_pending.has_key (photo));
1042
1043                ReimportMasterJob job = new ReimportMasterJob (this, photo);
1044                master_reimport_pending.set (photo, job);
1045                workers.enqueue (job);
1046            }
1047        }
1048
1049        if (reimport_editable != null) {
1050            mdbg ("Reimporting %d editables".printf (reimport_editable.size));
1051
1052            foreach (LibraryPhoto photo in reimport_editable) {
1053                assert (!editable_reimport_pending.has_key (photo));
1054
1055                ReimportEditableJob job = new ReimportEditableJob (this, photo);
1056                editable_reimport_pending.set (photo, job);
1057                workers.enqueue (job);
1058            }
1059        }
1060
1061        if (reimport_raw_developments != null) {
1062            mdbg ("Reimporting %d raw developments".printf (reimport_raw_developments.size));
1063
1064            foreach (LibraryPhoto photo in reimport_raw_developments) {
1065                assert (!raw_developments_reimport_pending.has_key (photo));
1066
1067                ReimportRawDevelopmentJob job = new ReimportRawDevelopmentJob (this, photo);
1068                raw_developments_reimport_pending.set (photo, job);
1069                workers.enqueue (job);
1070            }
1071        }
1072    }
1073
1074    private void on_master_reimported (BackgroundJob j) {
1075        ReimportMasterJob job = (ReimportMasterJob) j;
1076
1077        // no longer pending
1078        bool removed = master_reimport_pending.unset (job.photo);
1079        assert (removed);
1080
1081        if (job.err != null) {
1082            critical ("Unable to reimport %s due to master file changing: %s", job.photo.to_string (),
1083                      job.err.message);
1084
1085            update_offline (job.photo);
1086
1087            return;
1088        }
1089
1090        if (!job.mark_online) {
1091            // the prepare_for_reimport_master failed, photo is now considered offline
1092            update_offline (job.photo);
1093
1094            return;
1095        }
1096
1097        try {
1098            job.photo.finish_reimport_master (job.reimport_state);
1099        } catch (DatabaseError err) {
1100            AppWindow.database_error (err);
1101        }
1102
1103        // now considered online
1104        if (job.photo.is_offline ())
1105            update_online (job.photo);
1106
1107        mdbg ("Reimported master for %s".printf (job.photo.to_string ()));
1108    }
1109
1110    private void on_master_reimport_cancelled (BackgroundJob j) {
1111        bool removed = master_reimport_pending.unset (((ReimportMasterJob) j).photo);
1112        assert (removed);
1113    }
1114
1115    private void on_editable_reimported (BackgroundJob j) {
1116        ReimportEditableJob job = (ReimportEditableJob) j;
1117
1118        // no longer pending
1119        bool removed = editable_reimport_pending.unset (job.photo);
1120        assert (removed);
1121
1122        if (job.err != null) {
1123            critical ("Unable to reimport editable %s: %s", job.photo.to_string (), job.err.message);
1124
1125            return;
1126        }
1127
1128        try {
1129            job.photo.finish_reimport_editable (job.state);
1130        } catch (DatabaseError err) {
1131            AppWindow.database_error (err);
1132        }
1133
1134        mdbg ("Reimported editable for %s".printf (job.photo.to_string ()));
1135    }
1136
1137    private void on_editable_reimport_cancelled (BackgroundJob j) {
1138        bool removed = editable_reimport_pending.unset (((ReimportEditableJob) j).photo);
1139        assert (removed);
1140    }
1141
1142    private void on_raw_development_reimported (BackgroundJob j) {
1143        ReimportRawDevelopmentJob job = (ReimportRawDevelopmentJob) j;
1144
1145        // no longer pending
1146        bool removed = raw_developments_reimport_pending.unset (job.photo);
1147        assert (removed);
1148
1149        if (job.err != null) {
1150            critical ("Unable to reimport raw development %s: %s", job.photo.to_string (), job.err.message);
1151
1152            return;
1153        }
1154
1155        try {
1156            job.photo.finish_reimport_raw_development (job.state);
1157        } catch (DatabaseError err) {
1158            AppWindow.database_error (err);
1159        }
1160
1161        mdbg ("Reimported raw development for %s".printf (job.photo.to_string ()));
1162    }
1163
1164    private void on_raw_development_reimport_cancelled (BackgroundJob j) {
1165        bool removed = raw_developments_reimport_pending.unset (((ReimportRawDevelopmentJob) j).photo);
1166        assert (removed);
1167    }
1168}
1169