// Choosing which part of a picture becomes the profile picture.
//
// Without this, whatever was picked went to accountsservice whole, and a
// landscape photograph became a squashed thumbnail with the subject off the
// edge. The circle is drawn where the avatar is actually round elsewhere in the
// shell, so what is framed here is what appears there.
//
// The crop is reported in the picture's OWN pixels, not screen ones, so the
// result does not depend on the size this happened to be displayed at.

import QtQuick
import qs.config

Item {
    id: root

    // Absolute path of the picture being framed.
    property string source: ""

    // Side of the square viewport, in screen pixels.
    readonly property int viewport: 260

    // How far in the picture is zoomed, as a multiple of the smallest scale
    // that still covers the viewport. 1 means "just covers".
    property real zoom: 1
    readonly property real maxZoom: 4

    // Top-left of the drawn picture, relative to the viewport's top-left.
    property real offsetX: 0
    property real offsetY: 0

    readonly property bool ready: picture.status === Image.Ready
        && picture.sourceSize.width > 0 && picture.sourceSize.height > 0

    // The scale at which the shorter side exactly fills the viewport. Any
    // smaller and the square would include area the picture does not cover.
    readonly property real baseScale: root.ready
        ? root.viewport / Math.min(picture.sourceSize.width, picture.sourceSize.height)
        : 1
    readonly property real scale: root.baseScale * root.zoom

    readonly property real drawnWidth: root.ready ? picture.sourceSize.width * root.scale : 0
    readonly property real drawnHeight: root.ready ? picture.sourceSize.height * root.scale : 0

    // Emitted with a square in the picture's own pixel coordinates.
    signal cropped(int x, int y, int size)
    signal cancelled

    implicitWidth: parent ? parent.width : 620
    implicitHeight: column.implicitHeight

    // Keeps the viewport covered: the picture may never be dragged far enough
    // to expose an edge, so the crop is always entirely inside the image.
    function clamp(): void {
        root.offsetX = Math.min(0, Math.max(root.viewport - root.drawnWidth, root.offsetX));
        root.offsetY = Math.min(0, Math.max(root.viewport - root.drawnHeight, root.offsetY));
    }

    // Start centred, filling the viewport.
    function reset(): void {
        root.zoom = 1;
        root.offsetX = (root.viewport - root.drawnWidth) / 2;
        root.offsetY = (root.viewport - root.drawnHeight) / 2;
    }

    onReadyChanged: if (root.ready) root.reset()
    onZoomChanged: root.clamp()

    Column {
        id: column
        width: parent.width
        spacing: 14

        Row {
            spacing: 20

            Rectangle {
                id: frame
                width: root.viewport
                height: root.viewport
                radius: 12
                clip: true
                color: Theme.bgDark
                border.width: 1
                border.color: Theme.alpha(Theme.fg, 0.09)

                Image {
                    id: picture
                    source: root.source === "" ? "" : "file://" + root.source
                    x: root.offsetX
                    y: root.offsetY
                    width: root.drawnWidth
                    height: root.drawnHeight
                    fillMode: Image.Stretch
                    asynchronous: true
                    // The picture is drawn at whatever size the frame needs, so
                    // decoding it at full resolution wastes memory on a photo.
                    sourceSize.width: 1600
                    sourceSize.height: 1600
                    smooth: true
                }

                // The round mask: a dimming fill with the circle punched out of
                // it, then the ring drawn on top. Painted once per change, not
                // continuously.
                Canvas {
                    id: mask
                    anchors.fill: parent
                    onPaint: {
                        const context = mask.getContext("2d");
                        context.clearRect(0, 0, mask.width, mask.height);
                        context.fillStyle = Qt.rgba(0.12, 0.13, 0.19, 0.62);
                        context.fillRect(0, 0, mask.width, mask.height);
                        context.globalCompositeOperation = "destination-out";
                        context.beginPath();
                        context.arc(mask.width / 2, mask.height / 2,
                                    mask.width / 2 - 8, 0, Math.PI * 2);
                        context.fill();
                        context.globalCompositeOperation = "source-over";
                        context.strokeStyle = Qt.rgba(0.78, 0.83, 0.96, 0.85);
                        context.lineWidth = 2;
                        context.beginPath();
                        context.arc(mask.width / 2, mask.height / 2,
                                    mask.width / 2 - 8, 0, Math.PI * 2);
                        context.stroke();
                    }
                }

                // The offset is tracked from where the press started rather than
                // accumulated per frame, which would drift.
                MouseArea {
                    anchors.fill: parent
                    enabled: root.ready
                    cursorShape: Qt.OpenHandCursor
                    property real pressX: 0
                    property real pressY: 0
                    property real originX: 0
                    property real originY: 0

                    onPressed: mouse => {
                        pressX = mouse.x; pressY = mouse.y;
                        originX = root.offsetX; originY = root.offsetY;
                    }
                    onPositionChanged: mouse => {
                        if (!pressed)
                            return;
                        root.offsetX = originX + (mouse.x - pressX);
                        root.offsetY = originY + (mouse.y - pressY);
                        root.clamp();
                    }
                    onWheel: wheel => {
                        const before = root.scale;
                        root.zoom = Math.max(1, Math.min(root.maxZoom,
                            root.zoom * (wheel.angleDelta.y > 0 ? 1.1 : 1 / 1.1)));
                        // Keep the centre of the frame pointing at the same part
                        // of the picture, so zooming does not walk the subject
                        // out of the circle.
                        const ratio = root.scale / before;
                        root.offsetX = root.viewport / 2 - (root.viewport / 2 - root.offsetX) * ratio;
                        root.offsetY = root.viewport / 2 - (root.viewport / 2 - root.offsetY) * ratio;
                        root.clamp();
                    }
                }
            }

            Column {
                width: column.width - frame.width - 20
                spacing: 12

                Text {
                    width: parent.width
                    text: root.ready
                        ? "Drag to reposition, scroll to zoom."
                        : (root.source === "" ? "" : "Opening the picture…")
                    color: Theme.fgDim
                    font.family: Theme.fontFamily
                    font.pixelSize: Theme.fontSize
                    wrapMode: Text.WordWrap
                }

                Text {
                    width: parent.width
                    visible: root.ready
                    text: "The circle is what other people see. Written out at 512 × 512."
                    color: Theme.fgMuted
                    font.family: Theme.fontFamily
                    font.pixelSize: Theme.fontSizeSmall
                    wrapMode: Text.WordWrap
                }

                Text {
                    width: parent.width
                    visible: picture.status === Image.Error
                    text: "That file could not be opened as a picture."
                    color: Theme.danger
                    font.family: Theme.fontFamily
                    font.pixelSize: Theme.fontSizeSmall
                    wrapMode: Text.WordWrap
                }

                Row {
                    spacing: 8

                    SettingsButton {
                        text: "Set picture"
                        tone: "accent"
                        enabled: root.ready
                        onClicked: {
                            // Screen coordinates back into the picture's own.
                            const size = root.viewport / root.scale;
                            root.cropped(Math.round(-root.offsetX / root.scale),
                                         Math.round(-root.offsetY / root.scale),
                                         Math.round(size));
                        }
                    }

                    SettingsButton {
                        text: "Reset"
                        enabled: root.ready
                        onClicked: root.reset()
                    }

                    SettingsButton {
                        text: "Cancel"
                        onClicked: root.cancelled()
                    }
                }
            }
        }
    }
}
