// item_placement.js
// This module takes an image that acts as a container for a group of items.
//
// It places the images of the items against the backdrop of the container image.
//
// This module allows the items to be dragged to different locations and keeps track of their locations.
//
// The most important thing is that it allows the container image to be resized and handles the resizing and
// positioning of the items contained.

// Note on storing dimensions from CSS
// This module depends heavily on getting computed properties via the jquery css function.
// The properties are returned as strings with "px" appended regardless of how the style for that
// element was originally defined. (This is guaranteed by jquery for properties that can be specified in pixels.)
// We use parseFloat to convert them to float numbers since we have to do calculations with them.
// When specified in jQuery css to set values they are automatically converted back to px strings.


// The ItemPlacement class keeps items on a web page in place against a background image, maintaining
// the relative sizes during window resize. It makes the positions of the items available to be saved
// and accepts handlers for clicking and dragging the items.

// Usage:
//   itemPlacement = new ItemPlacement(config)
//
// where:
//
//   config = {
//              originalBackgroundSize: size,      // size of background image (before scaling)
//              backgroundDivID:        divID,     // ID of div containing background image
//              backgroundImageClass    className, // name of class of background img element if used (assumed "bkg-img" if not specified)
//              backgroundImageID       imgID,     // If null, img is located using backgroundImageClass
//              backgroundImagePath     imgPath,   // Initial value for img src. If null, we assume background img src is hard coded in the HTML
//              itemDivClass:           divClass,  // Class assigned to each displayed item (for styling)
//              displayItems:           items,     // items is an array (see below)
//              dragStopHandler:        handler,   // optional end of drag handler (see below)
//              clickHandler:           handler,   // optional click handler (see below)
//              moveHandler:            handler,   // optional move handler (see below)
//            };
//
// The displayItems array has an array of objects providing info on each display item:
//
//   displayItems = [displayItem, displayItem, ...];
//
// where:
//
//  displayItem = {
//                  displayItemID: uniqueDivID,           // ID, will be used as div ID, must be unique
//                  image:         path to image file,
//                  location:      {                      // In fixed coordinate system established by originalBackgroundSize
//                                    left: position,
//                                    top:  position
//                                 },
//                   scaling:      scale factor,
//                   draggable:    boolean,               // optional (false if not specified)
//                   description:  string,                // optional (goes in alt attribute)
//                   htmlAttributes: object,               // optional (add HTML attributes)
//                   cssProperties:  object                // optional (add CSS properties)
//                 }

import $ from "./jquery.module.js";
import * as drag from "./drag.js";

class ItemPlacement {

    // Parameters from constructor
    originalBackgroundSize;
    backgroundDivID;
    itemDivClass;
    displayItems;
    dragStopHandler = null;
    clickHandler = null;

    // Working variables
    currentImageSize; // call setCurrentImageSize to set this
    currentImagePos;  // ditto
    imageScale = 1;
    itemMap = {};
    itemsContainer;
    backgroundImageClass;

    constructor(config) {
        this.originalBackgroundSize = config.originalBackgroundSize;
        this.backgroundDivID        = config.backgroundDivID;
        this.itemDivClass           = config.itemDivClass;
        // this.displayItems           = config.displayItems.length === 0 ? [] : [... config.displayItems]; // Get shallow copy of items array
        this.displayItems           = [... config.displayItems]; // Get shallow copy of items array
        this.backgroundImageClass   = config.backgroundImageClass ?? "bkg-img";
        this.backgroundImageID      = config.backgroundImageID;  // If null, we assume background img element is hard coded in the HTML
        this.backgroundImagePath    = config.backgroundImagePath; // If null, we assume background img src is hard coded in the HTML

        if ("dragStopHandler" in config) {
            this.dragStopHandler = config.dragStopHandler;
        }

        if ("moveHandler" in config) {
            this.moveHandler = config.moveHandler;
        }

        if ("clickHandler" in config) {
            this.clickHandler = config.clickHandler;
        }
    }

    getBackgroundImageElement() {
        if (this.backgroundImageID) {
            return $(`#${this.backgroundImageID}`);
        }
        else {
            return $(`.${this.backgroundImageClass}`);
        }
    }

    blockUntilEvent($target, event) {
        return new Promise(resolve => {
            $target.on(event, resolve)
        });
    }

    async setNewBackgroundImage(imagePath) {
        let $img = this.getBackgroundImageElement();
        if (imagePath) {
            $img.css("display", "block");
            $img.attr("src", imagePath);
            await $img.get(0).decode();
            this.setCurrentImageSize();
        }
        else {
            $img.css("display", "none");
        }
    }

    setCurrentImageSize() {
        let img = $(`.${this.backgroundImageClass}`);
        if (img.length === 0) { // ...then div's background-image property is being used
            let div = $(`#${this.backgroundDivID}`);
            this.currentImageSize = {width: parseFloat(div.css("width")), height: parseFloat(div.css("height"))};
            this.currentImagePos = {left: 0, top: 0};
        }
        else {
            this.currentImageSize = {width: parseFloat(img.css("width")), height: parseFloat(img.css("height"))};
            if (this.currentImageSize.width === 0 || this.currentImageSize.height === 0) {
                this.currentImageSize = {width: img[0].naturalWidth, height: img[0].naturalHeight};
            }
            this.currentImagePos = img.position();
        }
        this.imageScale = this.currentImageSize.width / this.originalBackgroundSize.width;
    }

    stopDrag(e) {
        const item = e.data.$element;
        const itemID = item.attr("id");
        if (typeof this.dragStopHandler === "function") {
            const continueHandler = this.dragStopHandler(itemID);
            if (!continueHandler) {
                return;
            }
        }
        const left = parseFloat(item.css("left"));
        const top  = parseFloat(item.css("top"));
        const newLocation = {left: Math.floor(left / this.imageScale), top: Math.floor(top / this.imageScale)};
        this.itemMap[itemID].location = newLocation;
        this.moveHandler?.(newLocation, itemID);
    }

    scalePosition(position) {
        return({
            left:  Math.floor(position.left * this.imageScale + this.currentImagePos.left),
            top:   Math.floor(position.top  * this.imageScale + this.currentImagePos.top)
        });
    }

    scaleSizeAndPositionOfItem(itemID, location, scaling) {
        let item = $(`#${itemID}`);
        let width = Math.floor(this.originalBackgroundSize.width * scaling * this.imageScale);
        let scaledLocation = this.scalePosition(location);
        item.css("width", width).css("left", scaledLocation.left).css("top",scaledLocation.top);

        let image = $(`#${itemID} img`);
        image.css("width", width);
    }

    handleResize() {
        this.setCurrentImageSize();
        for (const item of this.displayItems) {
            this.scaleSizeAndPositionOfItem(item.displayItemID, item.location, item.scaling)
        }
    }

    makeDraggable(item) {
        const $itemElement = item instanceof $ ? item : $(`#${item.displayItemID}`);
        drag.makeDraggable({element: $itemElement, stop: (e) => this.stopDrag(e), moveElement: true});
    }

    addDisplayItem(item) {
        this.itemsContainer.append(`
            <div id="${item.displayItemID}" class="${this.itemDivClass}">
                <img src="${item.image}" alt="${item.description ?? "display item"}">
            </div>
            `);
        let $itemElement = $(`#${item.displayItemID}`);
        if (item?.draggable) {
            this.makeDraggable($itemElement);
        }
        this.scaleSizeAndPositionOfItem(item.displayItemID, item.location, item.scaling);
        this.itemMap[item.displayItemID] = item;

        if (this?.clickHandler) {
            $itemElement.on("click", item, this.clickHandler);
        }

        // Optional additional attributes and properties
        let htmlAttributes = item?.htmlAttributes;
        if (htmlAttributes) {
            let attributes = Object.entries(htmlAttributes);
            for (const [attr, value] of attributes) {
                $itemElement.attr(attr, value);
            }
        }

        let cssProperties = item?.cssProperties;
        if (cssProperties) {
            let properties = Object.entries(cssProperties);
            for (const [property, value] of properties) {
                $itemElement.css(property, value);
            }
        }

        this.displayItems.push(item);
    }

    addDisplayItems(displayItems) {
        for (const item of displayItems) {
            this.addDisplayItem(item);
        }
    }

    removeDisplayItem(displayItemID) {
        $(`#${displayItemID}`).remove();
        delete this.itemMap[displayItemID];
    }

    moveDisplayItemTo(displayItemID, newPosition) {
        let item = this.itemMap[displayItemID];
        let $itemElement = $(`#${displayItemID}`);
        let newScaledPosition = this.scalePosition(newPosition);
        item.location = newPosition;
        $itemElement.css("left", newScaledPosition.left);
        $itemElement.css("top", newScaledPosition.top);
    }

    processInitialDisplayItems(displayItems) {
        for (const item of displayItems) {
            this.itemsContainer.append(`
            <div id="${item.displayItemID}" class="${this.itemDivClass}">
                <img src="${item.image}" alt="${item.description ?? "displayItem"}">
            </div>
            `);
            let $itemElement = $(`#${item.displayItemID}`);
            if (item?.draggable) {
                this.makeDraggable($itemElement);
            }
            this.scaleSizeAndPositionOfItem(item.displayItemID, item.location, item.scaling);
            this.itemMap[item.displayItemID] = item;

            if (this?.clickHandler) {
                $itemElement.on("click", item, this.clickHandler);
            }

            // Optional additional attributes and properties
            let htmlAttributes = item?.htmlAttributes;
            if (htmlAttributes) {
                let attributes = Object.entries(htmlAttributes);
                for (const [attr, value] of attributes) {
                    $itemElement.attr(attr, value);
                }
            }

            let cssProperties = item?.cssProperties;
            if (cssProperties) {
                let properties = Object.entries(cssProperties);
                for (const [property, value] of properties) {
                    $itemElement.css(property, value);
                }
            }
        }
    }

    translateItemWithAnimation(item) {
        if (item?.translated) {return;}
        let $itemElement = $(`#${item.displayItemID}`);
        let newLocation = {
            left: item.location.left + item.translate.left,
            top: item.location.top + item.translate.top
        }
        let jQueryAnimation = this.scalePosition( newLocation);
        $itemElement.animate(jQueryAnimation, item.translate.duration*1000/*ms*/);
        item.location = newLocation;
        item.translated = true; // Don't move it twice
    }

    async onDocumentReady() {
        this.itemsContainer = $(`#${this.backgroundDivID}`);
        if (this.backgroundImagePath) {
            await this.setNewBackgroundImage(this.backgroundImagePath);
        }
        else {
            this.setCurrentImageSize();
        }
        this.processInitialDisplayItems(this.displayItems);
        $(window).on("resize", ()=>{this.handleResize();});
    }
}

export {ItemPlacement};