/* Copyright 2018 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.
 */

public errordomain FaceShapeError {
    CANT_CREATE
}

public class FacesTool : EditingTools.EditingTool {
    protected const int CONTROL_SPACING = 8;
    protected const int FACE_LABEL_MAX_CHARS = 15;

    private enum EditingPhase {
        CLICK_TO_EDIT,
        NOT_EDITING,
        CREATING_DRAGGING,
        CREATING_EDITING,
        EDITING,
        DETECTING_FACES,
        DETECTING_FACES_FINISHED
    }

    public class FaceWidget : Gtk.Box {
        private static Pango.AttrList attrs_bold;
        private static Pango.AttrList attrs_normal;

        public signal void face_hidden();

        public Gtk.Button edit_button;
        public Gtk.Button delete_button;
        public Gtk.Label label;

        public weak FaceShape face_shape;

        static construct {
            attrs_bold = new Pango.AttrList();
            attrs_bold.insert(Pango.attr_weight_new(Pango.Weight.BOLD));
            attrs_normal = new Pango.AttrList();
            attrs_normal.insert(Pango.attr_weight_new(Pango.Weight.NORMAL));
        }

        public FaceWidget (FaceShape face_shape) {
            spacing = CONTROL_SPACING;

            edit_button = new Gtk.Button.with_label(Resources.EDIT_LABEL);
            edit_button.set_use_underline(true);
            delete_button = new Gtk.Button.with_label(Resources.DELETE_LABEL);
            delete_button.set_use_underline(true);

            label = new Gtk.Label(face_shape.get_name());
            label.halign = Gtk.Align.START;
            label.valign = Gtk.Align.CENTER;
            label.ellipsize = Pango.EllipsizeMode.END;
            label.width_chars = FACE_LABEL_MAX_CHARS;

            pack_start(label, true);
            pack_start(edit_button, false);
            pack_start(delete_button, false);

            this.face_shape = face_shape;
            face_shape.set_widget(this);
        }

        public bool on_enter_notify_event() {
            activate_label();

            if (face_shape.is_editable())
                return false;

            // This check is necessary to avoid painting the face twice --see
            // note in on_leave_notify_event.
            if (!face_shape.is_visible())
                face_shape.show();

            return true;
        }

        public bool on_leave_notify_event() {
            // This check is necessary because GTK+ will throw enter/leave_notify
            // events when the pointer passes though windows, even if one window
            // belongs to a widget that is a child of the widget that throws this
            // signal. So, this check is necessary to avoid "deactivation" of
            // the label if the pointer enters one of the buttons in this FaceWidget.
            if (!is_pointer_over(get_window())) {
                deactivate_label();

                if (face_shape.is_editable())
                    return false;

                face_shape.hide();
                face_hidden();
            }

            return true;
        }

        public void activate_label() {
            label.set_attributes(attrs_bold);
        }

        public void deactivate_label() {
            label.set_attributes(attrs_normal);
        }
    }

    private class FacesToolWindow : EditingTools.EditingToolWindow {
        public signal void face_hidden();
        public signal void face_edit_requested(string face_name);
        public signal void face_delete_requested(string face_name);
        public signal void detection_canceled();

        public Gtk.Button detection_button = new Gtk.Button.with_label(_("Detect faces…"));
        public Gtk.Button ok_button;
        public Gtk.Button cancel_button;
        public Gtk.Button cancel_detection_button;

        private EditingPhase editing_phase = EditingPhase.NOT_EDITING;
        private Gtk.Box help_layout = null;
        private Gtk.Box response_layout = null;
        private Gtk.HSeparator buttons_text_separator = null;
        private Gtk.Label help_text = null;
        private Gtk.Box face_widgets_layout = null;
        private Gtk.Box layout = null;

        public FacesToolWindow(Gtk.Window container) {
            base(container);
            
            ok_button = new Gtk.Button.with_label(Resources.OK_LABEL);
            ok_button.set_use_underline(true);

            cancel_button = new Gtk.Button.with_label(Resources.CANCEL_LABEL);
            cancel_button.set_use_underline(true);

            cancel_detection_button = new Gtk.Button.with_label(Resources.CANCEL_LABEL);
            cancel_detection_button.set_use_underline(true);

            detection_button.set_tooltip_text(_("Detect faces on this photo"));

            cancel_detection_button.set_tooltip_text(_("Cancel face detection"));
            cancel_detection_button.set_image_position(Gtk.PositionType.LEFT);
            cancel_detection_button.clicked.connect(on_cancel_detection);

            cancel_button.set_tooltip_text(_("Close the Faces tool without saving changes"));
            cancel_button.set_image_position(Gtk.PositionType.LEFT);

            ok_button.set_image_position(Gtk.PositionType.LEFT);

            face_widgets_layout = new Gtk.Box(Gtk.Orientation.VERTICAL, CONTROL_SPACING);

            help_text = new Gtk.Label(_("Click and drag to tag a face"));
            help_layout = new Gtk.Box(Gtk.Orientation.HORIZONTAL, CONTROL_SPACING);
            help_layout.pack_start(help_text, true);

            response_layout = new Gtk.Box(Gtk.Orientation.HORIZONTAL, CONTROL_SPACING);
            #if ENABLE_FACE_DETECTION
            response_layout.add(detection_button);
            #endif
            response_layout.add(cancel_button);
            response_layout.add(ok_button);

            layout = new Gtk.Box(Gtk.Orientation.VERTICAL, CONTROL_SPACING);
            layout.pack_start(face_widgets_layout, false);
            layout.pack_start(help_layout, false);
            layout.pack_start(new Gtk.HSeparator(), false);
            layout.pack_start(response_layout, false);

            add(layout);
        }

        public void set_editing_phase(EditingPhase phase, FaceShape? face_shape = null) {
            if (editing_phase == EditingPhase.DETECTING_FACES &&
                phase != EditingPhase.DETECTING_FACES_FINISHED)
                return;

            switch (phase) {
                case EditingPhase.CLICK_TO_EDIT:
                    assert(face_shape != null);

                    help_text.set_markup(Markup.printf_escaped(_("Click to edit face <i>%s</i>"),
                        face_shape.get_name()));

                    break;
                case EditingPhase.NOT_EDITING:
                    help_text.set_text(_("Click and drag to tag a face"));

                    break;
                case EditingPhase.CREATING_DRAGGING:
                    help_text.set_text(_("Stop dragging to add your face and name it."));

                    break;
                case EditingPhase.CREATING_EDITING:
                    help_text.set_text(_("Type a name for this face, then press Enter"));

                    break;
                case EditingPhase.EDITING:
                    help_text.set_text(_("Move or modify the face shape or name and press Enter"));

                    break;
                case EditingPhase.DETECTING_FACES:
                    help_text.set_text(_("Detecting faces"));

                    if (cancel_detection_button.get_parent() == null)
                        help_layout.pack_start(cancel_detection_button, false);

                    detection_button.set_sensitive(false);
                    cancel_detection_button.set_sensitive(true);
                    cancel_detection_button.show();

                    break;
                case EditingPhase.DETECTING_FACES_FINISHED:
                    help_text.set_text(_("If you don’t set the name of unknown faces they won’t be saved."));

                    break;
                default:
                    assert_not_reached();
            }

            if (editing_phase == EditingPhase.DETECTING_FACES && editing_phase != phase) {
                cancel_detection_button.hide();
                detection_button.set_sensitive(true);
            }

            editing_phase = phase;
        }

        public EditingPhase get_editing_phase() {
            return editing_phase;
        }

        public void ok_button_set_sensitive(bool sensitive) {
            if (sensitive)
                ok_button.set_tooltip_text(_("Save changes and close the Faces tool"));
            else
                ok_button.set_tooltip_text(_("No changes to save"));

            ok_button.set_sensitive(sensitive);
        }

        public void add_face(FaceShape face_shape) {
            FaceWidget face_widget = new FaceWidget(face_shape);

            face_widget.face_hidden.connect(on_face_hidden);
            face_widget.edit_button.clicked.connect(edit_face);
            face_widget.delete_button.clicked.connect(delete_face);

            Gtk.EventBox event_box = new Gtk.EventBox();
            event_box.add(face_widget);
            event_box.add_events(Gdk.EventMask.ENTER_NOTIFY_MASK | Gdk.EventMask.LEAVE_NOTIFY_MASK);
            event_box.enter_notify_event.connect(face_widget.on_enter_notify_event);
            event_box.leave_notify_event.connect(face_widget.on_leave_notify_event);

            face_widgets_layout.pack_start(event_box, false);

            if (buttons_text_separator == null) {
                buttons_text_separator = new Gtk.HSeparator();
                face_widgets_layout.pack_end(buttons_text_separator, false);
            }

            face_widgets_layout.show_all();
        }

        private void edit_face(Gtk.Button button) {
            FaceWidget widget = (FaceWidget) button.get_parent();

            face_edit_requested(widget.label.get_text());
        }

        private void delete_face(Gtk.Button button) {
            FaceWidget widget = (FaceWidget) button.get_parent();

            face_delete_requested(widget.label.get_text());

            widget.get_parent().destroy();

            if (face_widgets_layout.get_children().length() == 1) {
                buttons_text_separator.destroy();
                buttons_text_separator = null;
            }
        }

        private void on_face_hidden() {
            face_hidden();
        }

        private void on_cancel_detection() {
            detection_canceled();
        }
    }

    public class EditingFaceToolWindow : EditingTools.EditingToolWindow {
        public signal bool key_pressed(Gdk.EventKey event);

        public Gtk.Entry entry;

        private Gtk.Box layout = null;

        public EditingFaceToolWindow(Gtk.Window container) {
            base(container);

            entry = new Gtk.Entry();

            layout = new Gtk.Box(Gtk.Orientation.HORIZONTAL, CONTROL_SPACING);
            layout.add(entry);

            add(layout);
        }

        public override bool key_press_event(Gdk.EventKey event) {
            return key_pressed(event) || base.key_press_event(event);
        }
    }

    private class FaceDetectionJob : BackgroundJob {
        private Gee.Queue<string> faces = null;
        private string image_path;
        private string output;
        public SpawnError? spawnError;

        public FaceDetectionJob(FacesToolWindow owner, string image_path,
            CompletionCallback completion_callback, Cancellable cancellable,
            CancellationCallback cancellation_callback) {
            base(owner, completion_callback, cancellable, cancellation_callback);

            this.image_path = image_path;
        }

        public override void execute() {
            try {
                string[] argv = {
                    AppDirs.get_facedetect_bin().get_path(),
                    "--cascade=" + AppDirs.get_haarcascade_file().get_path(),
                    "--scale=1.2",
                    image_path
                };
                Process.spawn_sync(null, argv, null, SpawnFlags.STDERR_TO_DEV_NULL, null, out output);

            } catch (SpawnError e) {
                spawnError = e;
                critical(e.message);

                return;
            }

            faces = new Gee.PriorityQueue<string>();
            string[] lines = output.split("\n");
            foreach (string line in lines) {
                if (line.length == 0)
                    continue;

                debug("shotwell-facedetect: %s", line);

                string[] type_and_serialized = line.split(";");
                if (type_and_serialized.length != 2) {
                    // Pass on external helper log output as our debug log
                    continue;
                }

                switch (type_and_serialized[0]) {
                    case "face":
                        StringBuilder serialized_geometry = new StringBuilder();
                        serialized_geometry.append(FaceRectangle.SHAPE_TYPE);
                        serialized_geometry.append(";");
                        serialized_geometry.append(parse_serialized_geometry(type_and_serialized[1]));

                        faces.add(serialized_geometry.str);
                        break;

                    case "warning":
                        warning("%s\n", type_and_serialized[1]);
                        break;

                    case "error":
                        critical("%s\n", type_and_serialized[1]);
                        assert_not_reached();

                    default:
                        break;
                }
            }
        }

        private string parse_serialized_geometry(string serialized_geometry) {
            string[] serialized_geometry_pieces = serialized_geometry.split("&");
            if (serialized_geometry_pieces.length != 4) {
                critical("Wrong serialized line in face detection program output.");
                assert_not_reached();
            }

            double x = 0;
            double y = 0;
            double width = 0;
            double height = 0;
            foreach (string piece in serialized_geometry_pieces) {

                string[] name_and_value = piece.split("=");
                if (name_and_value.length != 2) {
                    critical("Wrong serialized line in face detection program output.");
                    assert_not_reached();
                }

                switch (name_and_value[0]) {
                    case "x":
                        x = name_and_value[1].to_double();
                        break;

                    case "y":
                        y = name_and_value[1].to_double();
                        break;

                    case "width":
                        width = name_and_value[1].to_double();
                        break;

                    case "height":
                        height = name_and_value[1].to_double();
                        break;

                    default:
                        critical("Wrong serialized line in face detection program output.");
                        assert_not_reached();
                }
            }

            double half_width = width / 2;
            double half_height = height / 2;

            return "%s;%s;%s;%s".printf((x + half_width).to_string(), (y + half_height).to_string(),
                half_width.to_string(), half_height.to_string());
        }

        public string? get_next() {
            if (faces == null)
                return null;

            return faces.poll();
        }

        public void reset() {
            faces = null;
        }
    }

    private Cairo.Surface image_surface = null;
    private Gee.HashMap<string, FaceShape> face_shapes;
    private Gee.HashMap<string, string> original_face_locations;
    private Cancellable face_detection_cancellable;
    private FaceDetectionJob face_detection;
    private Workers workers;
    private FaceShape editing_face_shape = null;
    private FacesToolWindow faces_tool_window = null;

    private FacesTool() {
        base("FacesTool");
    }

    public static FacesTool factory() {
        return new FacesTool();
    }

    public override void activate(EditingTools.PhotoCanvas canvas) {
        face_shapes = new Gee.HashMap<string, FaceShape>();
        original_face_locations = new Gee.HashMap<string, string>();

        bind_canvas_handlers(canvas);

        if (image_surface != null)
            image_surface = null;

        Gdk.Rectangle scaled_pixbuf_position = canvas.get_scaled_pixbuf_position();
        image_surface = new Cairo.ImageSurface(Cairo.Format.ARGB32,
            scaled_pixbuf_position.width,
            scaled_pixbuf_position.height);

        faces_tool_window = new FacesToolWindow(canvas.get_container());

        Gee.Map<FaceID?, FaceLocation>? face_locations =
            FaceLocation.get_locations_by_photo(canvas.get_photo());
        if (face_locations != null)
            foreach (Gee.Map.Entry<FaceID?, FaceLocation> entry in face_locations.entries) {
                FaceShape new_face_shape;
                string serialized_geometry = entry.value.get_serialized_geometry();
                try {
                    new_face_shape = FaceShape.from_serialized(canvas, serialized_geometry);
                } catch (FaceShapeError e) {
                    if (e is FaceShapeError.CANT_CREATE)
                        continue;

                    assert_not_reached();
                }
                Face? face = Face.global.fetch(entry.key);
                assert(face != null);
                string face_name = face.get_name();
                new_face_shape.set_name(face_name);

                add_face(new_face_shape);
                original_face_locations.set(face_name, serialized_geometry);
            }

        set_ok_button_sensitivity();

        face_detection_cancellable = new Cancellable();
        workers = new Workers(1, false);
        face_detection = new FaceDetectionJob(faces_tool_window,
            canvas.get_photo().get_file().get_path(), on_faces_detected,
            face_detection_cancellable, on_detection_cancelled);

        bind_window_handlers();

        base.activate(canvas);
    }

    public override void deactivate() {
        if (canvas != null)
            unbind_canvas_handlers(canvas);

        if (faces_tool_window != null) {
            unbind_window_handlers();
            faces_tool_window.hide();
            faces_tool_window.destroy();
            faces_tool_window = null;
        }

        base.deactivate();
    }

    private void bind_canvas_handlers(EditingTools.PhotoCanvas canvas) {
        canvas.new_surface.connect(prepare_ctx);
        canvas.resized_scaled_pixbuf.connect(on_resized_pixbuf);
    }

    private void unbind_canvas_handlers(EditingTools.PhotoCanvas canvas) {
        canvas.new_surface.disconnect(prepare_ctx);
        canvas.resized_scaled_pixbuf.disconnect(on_resized_pixbuf);
    }

    private void bind_window_handlers() {
        faces_tool_window.key_press_event.connect(on_keypress);
        faces_tool_window.ok_button.clicked.connect(on_faces_ok);
        faces_tool_window.cancel_button.clicked.connect(notify_cancel);
        faces_tool_window.detection_button.clicked.connect(detect_faces);
        faces_tool_window.face_hidden.connect(on_face_hidden);
        faces_tool_window.face_edit_requested.connect(edit_face);
        faces_tool_window.face_delete_requested.connect(delete_face);
        faces_tool_window.detection_canceled.connect(cancel_face_detection);
    }

    private void unbind_window_handlers() {
        faces_tool_window.key_press_event.disconnect(on_keypress);
        faces_tool_window.ok_button.clicked.disconnect(on_faces_ok);
        faces_tool_window.cancel_button.clicked.disconnect(notify_cancel);
        faces_tool_window.detection_button.clicked.disconnect(detect_faces);
        faces_tool_window.face_hidden.disconnect(on_face_hidden);
        faces_tool_window.face_edit_requested.disconnect(edit_face);
        faces_tool_window.face_delete_requested.disconnect(delete_face);
        faces_tool_window.detection_canceled.disconnect(cancel_face_detection);
    }

    private void prepare_ctx(Cairo.Context ctx, Dimensions dim) {
        if (editing_face_shape != null)
            editing_face_shape.prepare_ctx(ctx, dim);
    }

    private void on_resized_pixbuf(Dimensions old_dim, Gdk.Pixbuf scaled, Gdk.Rectangle scaled_position) {
        if (image_surface != null)
            image_surface = null;

        image_surface = new Cairo.ImageSurface(Cairo.Format.ARGB32, scaled.width, scaled.height);
        Cairo.Context ctx = new Cairo.Context(image_surface);
        ctx.set_source_rgba(255.0, 255.0, 255.0, 0.0);
        ctx.paint();

        if (editing_face_shape != null)
            editing_face_shape.on_resized_pixbuf(old_dim, scaled);

        if (face_shapes != null)
            foreach (FaceShape face_shape in face_shapes.values)
                face_shape.on_resized_pixbuf(old_dim, scaled);
    }

    public override bool on_keypress(Gdk.EventKey event) {
        string event_keyval = Gdk.keyval_name(event.keyval);

        if (event_keyval == "Return" || event_keyval == "KP_Enter") {
            on_faces_ok();
            return true;
        }

        return base.on_keypress(event);
    }

    public override void on_left_click(int x, int y) {
        if (editing_face_shape != null && editing_face_shape.on_left_click(x, y))
            return;

        foreach (FaceShape face_shape in face_shapes.values) {
            if (face_shape.is_visible() && face_shape.cursor_is_over(x, y)) {
                edit_face_shape(face_shape);
                face_shape.set_editable(true);

                return;
            }
        }

        new_face_shape(x, y);
    }

    public override void on_left_released(int x, int y) {
        if (editing_face_shape != null) {
            editing_face_shape.on_left_released(x, y);

            if (faces_tool_window.get_editing_phase() == EditingPhase.CREATING_DRAGGING)
                faces_tool_window.set_editing_phase(EditingPhase.CREATING_EDITING);
        }
    }

    public override void on_motion(int x, int y, Gdk.ModifierType mask) {
        if (editing_face_shape == null) {
            FaceShape to_show = null;
            double distance = 0;
            double new_distance;

            foreach (FaceShape face_shape in face_shapes.values) {
                bool cursor_is_over = face_shape.cursor_is_over(x, y);

                // The FaceShape that will be shown needs to be repainted
                // even if it is already visible, since it could be erased by
                // another hiding FaceShape -and for the same
                // reason it needs to be painted after all
                // hiding faces are already erased.
                // Also, we paint the FaceShape whose center is closer
                // to the pointer.
                if (cursor_is_over) {
                    face_shape.hide();
                    face_shape.get_widget().deactivate_label();

                    if (to_show == null) {
                        to_show = face_shape;
                        distance = face_shape.get_distance(x, y);
                    } else {
                        new_distance = face_shape.get_distance(x, y);

                        if (new_distance < distance) {
                            to_show = face_shape;
                            distance = new_distance;
                        }
                    }
                } else if (!cursor_is_over && face_shape.is_visible()) {
                    face_shape.hide();
                    face_shape.get_widget().deactivate_label();
                }
            }

            if (to_show == null) {
                faces_tool_window.set_editing_phase(EditingPhase.NOT_EDITING);
            } else {
                faces_tool_window.set_editing_phase(EditingPhase.CLICK_TO_EDIT, to_show);

                to_show.show();
                to_show.get_widget().activate_label();
            }
        } else editing_face_shape.on_motion(x, y, mask);
    }

    public override bool on_leave_notify_event() {
        // This check is a workaround for bug #3896.
        if (is_pointer_over(canvas.get_drawing_window()) &&
            !is_pointer_over(faces_tool_window.get_window()))
            return false;

        if (editing_face_shape != null)
            return base.on_leave_notify_event();

        foreach (FaceShape face_shape in face_shapes.values) {
            if (face_shape.is_editable())
                return base.on_leave_notify_event();

            if (face_shape.is_visible()) {
                face_shape.hide();
                face_shape.get_widget().deactivate_label();

                break;
            }
        }

        faces_tool_window.set_editing_phase(EditingPhase.NOT_EDITING);

        return base.on_leave_notify_event();
    }

    public override EditingTools.EditingToolWindow? get_tool_window() {
        return faces_tool_window;
    }

    public override void paint(Cairo.Context default_ctx) {
        // fill region behind the image surface with neutral color
        int w = canvas.get_drawing_window().get_width();
        int h = canvas.get_drawing_window().get_height();

        default_ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0);
        default_ctx.rectangle(0, 0, w, h);
        default_ctx.fill();
        default_ctx.paint();

        Cairo.Context ctx = new Cairo.Context(image_surface);
        ctx.set_operator(Cairo.Operator.SOURCE);
        ctx.set_source_rgba(0.0, 0.0, 0.0, 0.0);
        ctx.paint();

        canvas.paint_surface(image_surface, true);

        // paint face shape last
        if (editing_face_shape != null)
            editing_face_shape.show();
    }

    private void new_face_shape(int x, int y) {
        edit_face_shape(new FaceRectangle(canvas, x, y), true);
    }

    private void edit_face_shape(FaceShape face_shape, bool creating = false) {
        hide_visible_face();

        if (editing_face_shape != null) {
            // We need to do this because it could be one of the already
            // created faces being edited, and if that is the case it
            // will not be destroyed.
            editing_face_shape.hide();
            editing_face_shape.set_editable(false);

            // This is to allow the user to edit a FaceShape's shape
            // without pressing the Enter button.
            if (face_shapes.values.contains(editing_face_shape))
                set_ok_button_sensitivity();

            editing_face_shape = null;
        }

        if (creating) {
            faces_tool_window.set_editing_phase(EditingPhase.CREATING_DRAGGING);
        } else {
            face_shape.show();

            faces_tool_window.set_editing_phase(EditingPhase.EDITING);
        }

        editing_face_shape = face_shape;
        editing_face_shape.add_me_requested.connect(add_face);
        editing_face_shape.delete_me_requested.connect(release_face_shape);
    }

    private void release_face_shape() {
        if (editing_face_shape == null)
            return;

        // We need to do this because it could be one of the already
        // created faces being edited, and if that is the case it
        // will not be destroyed.
        if (editing_face_shape in face_shapes.values) {
            editing_face_shape.hide();
            editing_face_shape.set_editable(false);

            editing_face_shape.get_widget().deactivate_label();
        }

        editing_face_shape = null;

        faces_tool_window.set_editing_phase(EditingPhase.NOT_EDITING);
        faces_tool_window.present();
    }

    private void hide_visible_face() {
        foreach (FaceShape face_shape in face_shapes.values) {
            if (face_shape.is_visible()) {
                face_shape.hide();

                break;
            }
        }
    }

    private void on_faces_ok() {
        if (face_shapes == null)
            return;

        Gee.Map<Face, string> new_faces = new Gee.HashMap<Face, string>();
        foreach (FaceShape face_shape in face_shapes.values) {
            if (!face_shape.get_known())
                continue;

            Face new_face = Face.for_name(face_shape.get_name());

            new_faces.set(new_face, face_shape.serialize());
        }

        ModifyFacesCommand command = new ModifyFacesCommand(canvas.get_photo(), new_faces);
        applied(command, null, canvas.get_photo().get_dimensions(), false);
    }

    private void on_face_hidden() {
        if (editing_face_shape != null)
            editing_face_shape.show();
    }

    private void add_face(FaceShape face_shape) {
        string? prepared_face_name = Face.prep_face_name(face_shape.get_name());

        if (prepared_face_name != null) {
            face_shape.set_name(prepared_face_name);

            if (face_shapes.values.contains(face_shape)) {
                foreach (Gee.Map.Entry<string, FaceShape> entry in face_shapes.entries) {
                    if (entry.value == face_shape) {
                        if (entry.key == prepared_face_name)
                            break;

                        face_shapes.unset(entry.key);
                        face_shapes.set(prepared_face_name, face_shape);

                        face_shape.set_known(true);
                        face_shape.get_widget().label.set_text(face_shape.get_name());

                        break;
                    }
                }
            } else if (!face_shapes.has_key(prepared_face_name)) {
                faces_tool_window.add_face(face_shape);
                face_shapes.set(prepared_face_name, face_shape);
            } else return;

            face_shape.hide();
            face_shape.set_editable(false);

            set_ok_button_sensitivity();
            release_face_shape();
        }
    }

    private void edit_face(string face_name) {
        FaceShape face_shape = face_shapes.get(face_name);
        assert(face_shape != null);

        face_shape.set_editable(true);
        edit_face_shape(face_shape);
    }

    private void delete_face(string face_name) {
        face_shapes.unset(face_name);

        // It is posible to have two visible faces at the same time, this happens
        // if you are editing one face and you move the pointer around the
        // FaceWidgets area in FacesToolWindow. And you can delete one of that
        // faces, so the other visible face must be repainted.
        foreach (FaceShape face_shape in face_shapes.values) {
            if (face_shape.is_visible()) {
                face_shape.hide();
                face_shape.show();

                break;
            }
        }

        set_ok_button_sensitivity();
    }

    private void set_ok_button_sensitivity() {
        Gee.Map<string, FaceShape> known_face_shapes = new Gee.HashMap<string, FaceShape>();
        foreach (Gee.Map.Entry<string, FaceShape> face_shape in face_shapes.entries) {
            if (face_shape.value.get_known()) {
                known_face_shapes.set(face_shape.key, face_shape.value);
            }
        }

        if (original_face_locations.size != known_face_shapes.size) {
            faces_tool_window.ok_button_set_sensitive(true);

            return;
        }

        foreach (Gee.Map.Entry<string, FaceShape> face_shape in known_face_shapes.entries) {
            bool found = false;

            foreach (Gee.Map.Entry<string, string> face_location in original_face_locations.entries) {
                if (face_location.key == face_shape.key) {
                    if (face_location.value == face_shape.value.serialize()) {
                        found = true;

                        break;
                    } else {
                        faces_tool_window.ok_button_set_sensitive(true);

                        return;
                    }
                }
            }

            if (!found) {
                faces_tool_window.ok_button_set_sensitive(true);

                return;
            }
        }

        faces_tool_window.ok_button_set_sensitive(false);
    }

    private void detect_faces() {
        faces_tool_window.detection_button.set_sensitive(false);
        faces_tool_window.set_editing_phase(EditingPhase.DETECTING_FACES);

        workers.enqueue(face_detection);
    }

    private void pick_faces_from_autodetected() {
        int c = 0;
        while (true) {
            string? serialized_geometry = face_detection.get_next();
            if (serialized_geometry == null) {
                faces_tool_window.set_editing_phase(EditingPhase.DETECTING_FACES_FINISHED);

                return;
            }

            FaceShape face_shape;
            try {
                face_shape = FaceShape.from_serialized(canvas, serialized_geometry);
            } catch (FaceShapeError e) {
                if (e is FaceShapeError.CANT_CREATE)
                    continue;

                assert_not_reached();
            }

            bool found = false;
            foreach (FaceShape existing_face_shape in face_shapes.values) {
                if (existing_face_shape.equals(face_shape)) {
                    found = true;

                    break;
                }
            }

            if (found)
                continue;

            c++;

            face_shape.set_name("Unknown face #%d".printf(c));
            face_shape.set_known(false);
            add_face(face_shape);
        }
    }

    private void on_faces_detected() {
        face_detection_cancellable.reset();
        
        if (face_detection.spawnError != null){
            string spawnErrorMessage = _("Error trying to spawn face detection program:\n");
            AppWindow.error_message(spawnErrorMessage + face_detection.spawnError.message + "\n");
            faces_tool_window.set_editing_phase(EditingPhase.DETECTING_FACES_FINISHED);
        } else
            pick_faces_from_autodetected();
    }

    private void on_detection_cancelled(BackgroundJob job) {
        ((FaceDetectionJob) job).reset();
        face_detection_cancellable.reset();

        faces_tool_window.set_editing_phase(EditingPhase.DETECTING_FACES_FINISHED);
    }

    private void cancel_face_detection() {
        faces_tool_window.cancel_detection_button.set_sensitive(false);

        face_detection.cancel();
    }
}