// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
/* exported ShellMagnifier */

const Gio = imports.gi.Gio;
const Main = imports.ui.main;

const { loadInterfaceXML } = imports.misc.fileUtils;

const MAG_SERVICE_PATH = '/io/github/scarecrow_de/Magnifier';
const ZOOM_SERVICE_PATH = '/io/github/scarecrow_de/Magnifier/ZoomRegion';

// Subset of gnome-mag's Magnifier dbus interface -- to be expanded.  See:
// http://git.gnome.org/browse/gnome-mag/tree/xml/...Magnifier.xml
const MagnifierIface = loadInterfaceXML('io.github.scarecrow_de.Magnifier');

// Subset of gnome-mag's ZoomRegion dbus interface -- to be expanded.  See:
// http://git.gnome.org/browse/gnome-mag/tree/xml/...ZoomRegion.xml
const ZoomRegionIface = loadInterfaceXML('io.github.scarecrow_de.Magnifier.ZoomRegion');

// For making unique ZoomRegion DBus proxy object paths of the form:
// '/io/github/scarecrow_de/Magnifier/ZoomRegion/zoomer0',
// '/io/github/scarecrow_de/Magnifier/ZoomRegion/zoomer1', etc.
let _zoomRegionInstanceCount = 0;

var ShellMagnifier = class ShellMagnifier {
    constructor() {
        this._zoomers = {};

        this._dbusImpl = Gio.DBusExportedObject.wrapJSObject(MagnifierIface, this);
        this._dbusImpl.export(Gio.DBus.session, MAG_SERVICE_PATH);
    }

    /**
     * setActive:
     * @param {bool} activate: activate or de-activate the magnifier.
     */
    setActive(activate) {
        Main.magnifier.setActive(activate);
    }

    /**
     * isActive:
     * @returns {bool} Whether the magnifier is active.
     */
    isActive() {
        return Main.magnifier.isActive();
    }

    /**
     * showCursor:
     * Show the system mouse pointer.
     */
    showCursor() {
        Main.magnifier.showSystemCursor();
    }

    /**
     * hideCursor:
     * Hide the system mouse pointer.
     */
    hideCursor() {
        Main.magnifier.hideSystemCursor();
    }

    /**
     * createZoomRegion:
     * Create a new ZoomRegion and return its object path.
     * @param {number} xMagFactor:
     *     The power to set horizontal magnification of the ZoomRegion.
     *     A value of 1.0 means no magnification. A value of 2.0 doubles
     *     the size.
     * @param {number} yMagFactor:
     *     The power to set the vertical magnification of the
     *     ZoomRegion.
     * @param {number[]} roi
     *     Array of integers defining the region of the screen/desktop
     *     to magnify.  The array has the form [left, top, right, bottom].
     * @param {number[]} viewPort
     *     Array of integers, [left, top, right, bottom] that defines
     *     the position of the ZoomRegion on screen.
     *
     * FIXME: The arguments here are redundant, since the width and height of
     *   the ROI are determined by the viewport and magnification factors.
     *   We ignore the passed in width and height.
     *
     * @returns {ZoomRegion} The newly created ZoomRegion.
     */
    createZoomRegion(xMagFactor, yMagFactor, roi, viewPort) {
        let ROI = { x: roi[0], y: roi[1], width: roi[2] - roi[0], height: roi[3] - roi[1] };
        let viewBox = { x: viewPort[0], y: viewPort[1], width: viewPort[2] - viewPort[0], height: viewPort[3] - viewPort[1] };
        let realZoomRegion = Main.magnifier.createZoomRegion(xMagFactor, yMagFactor, ROI, viewBox);
        let objectPath = `${ZOOM_SERVICE_PATH}/zoomer${_zoomRegionInstanceCount}`;
        _zoomRegionInstanceCount++;

        let zoomRegionProxy = new ShellMagnifierZoomRegion(objectPath, realZoomRegion);
        let proxyAndZoomRegion = {};
        proxyAndZoomRegion.proxy = zoomRegionProxy;
        proxyAndZoomRegion.zoomRegion = realZoomRegion;
        this._zoomers[objectPath] = proxyAndZoomRegion;
        return objectPath;
    }

    /**
     * addZoomRegion:
     * Append the given ZoomRegion to the magnifier's list of ZoomRegions.
     * @param {string} zoomerObjectPath: The object path for the zoom
     *     region proxy.
     * @returns {bool} whether the region was added successfully
     */
    addZoomRegion(zoomerObjectPath) {
        let proxyAndZoomRegion = this._zoomers[zoomerObjectPath];
        if (proxyAndZoomRegion && proxyAndZoomRegion.zoomRegion) {
            Main.magnifier.addZoomRegion(proxyAndZoomRegion.zoomRegion);
            return true;
        } else {
            return false;
        }
    }

    /**
     * getZoomRegions:
     * Return a list of ZoomRegion object paths for this Magnifier.
     * @returns {string[]}: The Magnifier's zoom region list as an array
     *     of DBus object paths.
     */
    getZoomRegions() {
        // There may be more ZoomRegions in the magnifier itself than have
        // been added through dbus.  Make sure all of them are associated with
        // an object path and proxy.
        let zoomRegions = Main.magnifier.getZoomRegions();
        let objectPaths = [];
        let thoseZoomers = this._zoomers;
        zoomRegions.forEach(aZoomRegion => {
            let found = false;
            for (let objectPath in thoseZoomers) {
                let proxyAndZoomRegion = thoseZoomers[objectPath];
                if (proxyAndZoomRegion.zoomRegion === aZoomRegion) {
                    objectPaths.push(objectPath);
                    found = true;
                    break;
                }
            }
            if (!found) {
                // Got a ZoomRegion with no DBus proxy, make one.
                let newPath = `${ZOOM_SERVICE_PATH}/zoomer${_zoomRegionInstanceCount}`;
                _zoomRegionInstanceCount++;
                let zoomRegionProxy = new ShellMagnifierZoomRegion(newPath, aZoomRegion);
                let proxyAndZoomer = {};
                proxyAndZoomer.proxy = zoomRegionProxy;
                proxyAndZoomer.zoomRegion = aZoomRegion;
                thoseZoomers[newPath] = proxyAndZoomer;
                objectPaths.push(newPath);
            }
        });
        return objectPaths;
    }

    /**
     * clearAllZoomRegions:
     * Remove all the zoom regions from this Magnfier's ZoomRegion list.
     */
    clearAllZoomRegions() {
        Main.magnifier.clearAllZoomRegions();
        for (let objectPath in this._zoomers) {
            let proxyAndZoomer = this._zoomers[objectPath];
            proxyAndZoomer.proxy.destroy();
            proxyAndZoomer.proxy = null;
            proxyAndZoomer.zoomRegion = null;
            delete this._zoomers[objectPath];
        }
        this._zoomers = {};
    }

    /**
     * fullScreenCapable:
     * Consult if the Magnifier can magnify in full-screen mode.
     * @returns {bool} Always return true.
     */
    fullScreenCapable() {
        return true;
    }

    /**
     * setCrosswireSize:
     * Set the crosswire size of all ZoomRegions.
     * @param {number} size: The thickness of each line in the cross wire.
     */
    setCrosswireSize(size) {
        Main.magnifier.setCrosshairsThickness(size);
    }

    /**
     * getCrosswireSize:
     * Get the crosswire size of all ZoomRegions.
     * @returns {number}: The thickness of each line in the cross wire.
     */
    getCrosswireSize() {
        return Main.magnifier.getCrosshairsThickness();
    }

    /**
     * setCrosswireLength:
     * Set the crosswire length of all zoom-regions..
     * @param {number} length: The length of each line in the cross wire.
     */
    setCrosswireLength(length) {
        Main.magnifier.setCrosshairsLength(length);
    }

    /**
     * getCrosswireSize:
     * Get the crosswire length of all zoom-regions.
     * @returns {number} size: The length of each line in the cross wire.
     */
    getCrosswireLength() {
        return Main.magnifier.getCrosshairsLength();
    }

    /**
     * setCrosswireClip:
     * Set if the crosswire will be clipped by the cursor image..
     * @param {bool} clip: Flag to indicate whether to clip the crosswire.
     */
    setCrosswireClip(clip) {
        Main.magnifier.setCrosshairsClip(clip);
    }

    /**
     * getCrosswireClip:
     * Get the crosswire clip value.
     * @returns {bool}: Whether the crosswire is clipped by the cursor image.
     */
    getCrosswireClip() {
        return Main.magnifier.getCrosshairsClip();
    }

    /**
     * setCrosswireColor:
     * Set the crosswire color of all ZoomRegions.
     * @param {number} color: Unsigned int of the form rrggbbaa.
     */
    setCrosswireColor(color) {
        Main.magnifier.setCrosshairsColor('#%08x'.format(color));
    }

    /**
     * getCrosswireClip:
     * Get the crosswire color of all ZoomRegions.
     * @returns {number}: The crosswire color as an unsigned int in
     *     the form rrggbbaa.
     */
    getCrosswireColor() {
        let colorString = Main.magnifier.getCrosshairsColor();
        // Drop the leading '#'.
        return parseInt(colorString.slice(1), 16);
    }
};

/**
 * ShellMagnifierZoomRegion:
 * Object that implements the DBus ZoomRegion interface.
 * @zoomerObjectPath:   String that is the path to a DBus ZoomRegion.
 * @zoomRegion:         The actual zoom region associated with the object path.
 */
var ShellMagnifierZoomRegion = class ShellMagnifierZoomRegion {
    constructor(zoomerObjectPath, zoomRegion) {
        this._zoomRegion = zoomRegion;

        this._dbusImpl = Gio.DBusExportedObject.wrapJSObject(ZoomRegionIface, this);
        this._dbusImpl.export(Gio.DBus.session, zoomerObjectPath);
    }

    /**
     * setMagFactor:
     * @param {number} xMagFactor: The power to set the horizontal
     *     magnification factor to of the magnified view. A value of
     *     1.0 means no magnification. A value of 2.0 doubles the size.
     * @param {number} yMagFactor: The power to set the vertical
     *     magnification factor to of the magnified view.
     */
    setMagFactor(xMagFactor, yMagFactor) {
        this._zoomRegion.setMagFactor(xMagFactor, yMagFactor);
    }

    /**
     * getMagFactor:
     * @returns {number[]}: [xMagFactor, yMagFactor], containing the horizontal
     *          and vertical magnification powers.  A value of 1.0 means no
     *          magnification.  A value of 2.0 means the contents are doubled
     *          in size, and so on.
     */
    getMagFactor() {
        return this._zoomRegion.getMagFactor();
    }

    /**
     * setRoi:
     * Sets the "region of interest" that the ZoomRegion is magnifying.
     * @param {number[]} roi: [left, top, right, bottom], defining the
     *     region of the screen to magnify.
     *     The values are in screen (unmagnified) coordinate space.
     */
    setRoi(roi) {
        let roiObject = { x: roi[0], y: roi[1], width: roi[2] - roi[0], height: roi[3] - roi[1] };
        this._zoomRegion.setROI(roiObject);
    }

    /**
     * getRoi:
     * Retrieves the "region of interest" -- the rectangular bounds of that part
     * of the desktop that the magnified view is showing (x, y, width, height).
     * The bounds are given in non-magnified coordinates.
     * @returns {Array}: [left, top, right, bottom], representing the bounding
     *          rectangle of what is shown in the magnified view.
     */
    getRoi() {
        let roi = this._zoomRegion.getROI();
        roi[2] += roi[0];
        roi[3] += roi[1];
        return roi;
    }

    /**
     * Set the "region of interest" by centering the given screen coordinate
     * within the zoom region.
     * @param {number} x: The x-coord of the point to place at the
     *     center of the zoom region.
     * @param {number} y: The y-coord.
     * @returns {bool} Whether the shift was successful (for GS-mag, this
     *     is always true).
     */
    shiftContentsTo(x, y) {
        this._zoomRegion.scrollContentsTo(x, y);
        return true;
    }

    /**
     * moveResize
     * Sets the position and size of the ZoomRegion on screen.
     * @param {number[]} viewPort: [left, top, right, bottom], defining
     *     the position and size on screen to place the zoom region.
     */
    moveResize(viewPort) {
        let viewRect = { x: viewPort[0], y: viewPort[1], width: viewPort[2] - viewPort[0], height: viewPort[3] - viewPort[1] };
        this._zoomRegion.setViewPort(viewRect);
    }

    destroy() {
        this._dbusImpl.unexport();
    }
};
