/* controls-container.vala
 *
 * Copyright 2020 James Westman <james@flyingpimonster.net>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * SPDX-License-Identifier: GPL-3.0-or-later
 */


/**
 * Handles the layout of a #CameraControls widget.
 *
 * This could be part of #CameraControls, but it's more organized to have the
 * layout code in one class and the widget's logic in another.
 */
public class Camera.ControlsContainer : Gtk.Container, Gtk.Buildable {
    public enum Mode {
        SIDEBAR,
        BOTTOM,
        TOP
    }
    private Mode _mode = SIDEBAR;
    public Mode mode {
        get {
            return _mode;
        }
        set {
            if (mode == value) {
                return;
            }

            _mode = value;
            queue_resize();
            notify_property("orientation");
        }
    }

    public Gtk.Orientation orientation {
        get {
            return mode == SIDEBAR ? Gtk.Orientation.VERTICAL : Gtk.Orientation.HORIZONTAL;
        }
    }

    private bool _narrow;
    public bool narrow {
        get {
            return _narrow;
        }
        private set {
            if (narrow == value) {
                return;
            }

            _narrow = value;

            tween[Tweens.TRANSITION] = narrow ? 1 : 0;
            queue_resize();
        }
    }


    private const int END_MARGIN = 18;
    private const int SIDE_MARGIN = 18;
    private const int SPACING = 12;


    private List<Gtk.Widget> start_widgets;
    private List<Gtk.Widget> end_widgets;
    private Gtk.Widget center_widget;

    private Tween tween;
    private enum Tweens {
        TRANSITION,
        LAST_TWEEN
    }


    construct {
        set_has_window(false);

        tween = new Tween(this, Tweens.LAST_TWEEN);
        tween.resize = true;
        tween.start(Tweens.TRANSITION, 0, 0);

        draw.connect(on_draw);
    }


    private int get_size_along_axis(Gtk.Widget widget) {
        if (widget == null) {
            return 0;
        }

        int nat;
        if (mode == SIDEBAR) {
            widget.get_preferred_height(null, out nat);
        } else {
            widget.get_preferred_width(null, out nat);
        }
        return nat;
    }


    private int get_size_across_axis(Gtk.Widget widget) {
        if (widget == null) {
            return 0;
        }

        int nat;
        if (orientation == VERTICAL) {
            widget.get_preferred_width(null, out nat);
        } else {
            widget.get_preferred_height(null, out nat);
        }
        return nat;
    }


    private int get_total_size_along_axis() {
        int start = 0, end = 0, center = 0;

        foreach (var widget in start_widgets) {
            start += get_size_along_axis(widget) + SPACING;
        }
        foreach (var widget in end_widgets) {
            end += get_size_along_axis(widget) + SPACING;
        }
        center = get_size_along_axis(center_widget);

        return int.max(start, end) * 2 + center + END_MARGIN * 2;
    }


    private int get_max_size_across_axis() {
        int size = 0;
        foreach (var widget in start_widgets) {
            size = int.max(size, get_size_across_axis(widget));
        }
        foreach (var widget in end_widgets) {
            size = int.max(size, get_size_across_axis(widget));
        }
        if (mode != TOP) {
            size = int.max(size, get_size_across_axis(center_widget));
        }

        return size + SIDE_MARGIN * 2;
    }


    protected override void get_preferred_width(out int min, out int nat) {
        switch (mode) {
            case SIDEBAR:
                nat = get_max_size_across_axis();
                break;
            case BOTTOM:
                nat = get_size_along_axis(center_widget) + END_MARGIN * 2;
                break;
            case TOP:
                int size = 0;
                foreach (var widget in start_widgets) {
                    size += get_size_along_axis(widget) + SPACING;
                }
                foreach (var widget in end_widgets) {
                    size += get_size_along_axis(widget) + SPACING;
                }
                nat = size - SPACING + END_MARGIN * 2;
                break;
            default:
                assert_not_reached();
        }

        min = nat;
    }


    protected override void get_preferred_height(out int min, out int nat) {
        if (orientation == VERTICAL) {
            nat = get_total_size_along_axis();
        } else {
            nat = get_max_size_across_axis();
        }

        min = nat;
    }


    private void allocate_widget(Gtk.Widget widget, Gtk.Allocation alloc, ref int pos, bool end) {
        int nat_h, nat_w;
        widget.get_preferred_width(null, out nat_w);
        widget.get_preferred_height(null, out nat_h);

        if (mode == SIDEBAR) {
            var size = Gtk.Allocation() {
                x = alloc.x + ((alloc.width - nat_w) / 2),
                y = alloc.y + (alloc.height - pos), // Sidebar is allocated bottom to top
                width = nat_w,
                height = nat_h
            };

            if (end) {
                pos -= nat_h + SPACING;
            } else {
                pos += nat_h + SPACING;
                size.y -= size.height;
            }

            widget.size_allocate(size);
        } else {
            var size = Gtk.Allocation() {
                x = alloc.x + pos,
                y = alloc.y + ((alloc.height - nat_h) / 2),
                width = nat_w,
                height = nat_h
            };

            if (end) {
                pos -= nat_w + SPACING;
                size.x -= size.width;
            } else {
                pos += nat_w + SPACING;
            }

            widget.size_allocate(size);
        }
    }


    public override void size_allocate(Gtk.Allocation alloc) {
        double transition = tween[Tweens.TRANSITION];

        int start = END_MARGIN;
        int length = (orientation == VERTICAL) ? alloc.height : alloc.width;
        int end = length - END_MARGIN;

        // Allocate the start and end widgets
        foreach (Gtk.Widget widget in start_widgets) {
            allocate_widget(widget, alloc, ref start, false);
            if (mode == BOTTOM) {
                // Fade in/out animation on the bottom bar
                widget.opacity = 1 - transition;
                widget.set_child_visible(transition != 1);
            }
        }
        foreach (Gtk.Widget widget in end_widgets) {
            allocate_widget(widget, alloc, ref end, true);
            if (mode == BOTTOM) {
                // Fade in/out animation on the bottom bar
                widget.opacity = 1 - transition;
                widget.set_child_visible(transition != 1);
            }
        }

        if (center_widget != null) {
            if (mode == TOP) {
                // Center child is never visible on the top bar
                center_widget.set_child_visible(false);
            } else {
                int center = length / 2;
                int center_size = get_size_along_axis(center_widget);
                int center_pos = center - (center_size / 2);

                // Determine whether the center child fits in the exact center
                // without overlapping the start or end widgets
                narrow = (start > center - center_size / 2) || (end < center + center_size / 2);

                allocate_widget(center_widget, alloc, ref center_pos, false);
            }
        }

        base.size_allocate(alloc);
    }


    protected override void forall_internal(bool include_internals, Gtk.Callback callback) {
        foreach_widget(start_widgets, callback);
        if (center_widget != null) {
            callback(center_widget);
        }
        foreach_widget(end_widgets, callback);
    }


    protected override void add(Gtk.Widget widget) {
        start_widgets.append(widget);
        widget.set_parent(this);
    }


    protected void add_child(Gtk.Builder builder, Object child, string? type)
            requires (type == null || type == "center" || type == "end") {

        var widget = child as Gtk.Widget;
        widget.set_parent(this);

        if (type == null) {
            start_widgets.append(widget);
        } else if (type == "center") {
            if (center_widget != null) {
                center_widget.unparent();
            }
            center_widget = widget;
        } else if (type == "end") {
            end_widgets.append(widget);
        }
    }

    protected override void remove(Gtk.Widget widget)
            requires (widget == center_widget
                      || start_widgets.find(widget) != null
                      || end_widgets.find(widget) != null) {

        widget.unparent();

        if (widget == center_widget) {
            center_widget = null;
        } else {
            start_widgets.remove(widget);
            end_widgets.remove(widget);
        }
    }

    private bool on_draw(Cairo.Context cr) {
        cr.set_source_rgba(0, 0, 0, 0.5);
        cr.rectangle(0, 0, get_allocated_width(), get_allocated_height());
        cr.fill();
        return false;
    }
}
