/* ES Lint  */
/* globals PIXI, requestAnimationFrame, performance, app*/

import { DisplayObjectScatter, ScatterContainer } from '../scatter.js'
import { Points } from '../../utils.js'
import { EventHandler } from './utils.js'

/**
 * The AdvancedScatterContainer extends the ScatterContainer, but
 * uses the findTargetNew instead of the findTarget method, which takes interactive elements
 * into account, when calculating the target.
 */
export class AdvancedScatterContainer extends ScatterContainer {
    applyToChildScattersRecursively(parent, root, applyFunc) {
        for (let child of parent.children) {
            if (child.scatter) {
                applyFunc(child, root)
                this.applyToChildScattersRecursively(child, root, applyFunc)
            }
        }
    }

    /**
     * The update method was adjusted to draw bounds and polygpns
     */
    update(dt) {
        this.clear()
        this.lineStyle(1, 0x0000ff)
        if (this.showBounds) {
            this.applyToChildScattersRecursively(this, this, (child, root) => {
                let position = child.worldTransform.apply(new PIXI.Point(0, 0))

                this.drawShape(child.scatter.bounds)

                let center = {
                    x: child.scatter.width / 2 / child.scatter.scale,
                    y: child.scatter.height / 2 / child.scatter.scale
                }
                center = child.worldTransform.apply(center)

                this.drawCircle(center.x, center.y, 4)
                this.drawCircle(position.x, position.y, 4)
            })

            this.lineStyle(2, 0x0000ff)
            this.drawShape(this.bounds)
        }
        if (this.showPolygon) {
            this.lineStyle(2, 0xff0000)
            for (let child of this.children) {
                if (child.scatter) {
                    let polygon = child.scatter.polygon
                    let shape = new PIXI.Polygon(polygon.flatAbsolutePoints())
                    if (
                        shape.points[0] !== shape.points[shape.points.length - 2] ||
                        shape.points[1] !== shape.points[shape.points.length - 1]
                    ) {
                        shape.points.push(shape.points[0], shape.points[1])
                    }
                    this.drawShape(shape)
                }
            }
        }
        if (this.showTouches) {
            let current = this.delegate.interaction.current
            for (let [key, point] of current.entries()) {
                let local = this.mapPositionToPoint(point)
                this.drawCircle(local.x, local.y, 12)
            }
        }
    }

    get width() {
        return this.bounds.width
    }

    get height() {
        return this.bounds.width
    }

    findTarget(event, local, global) {
        // UO: still problematic. Does not find non interactive elements
        // which are needed for some stylus applications
        if (event.claimedByScatter) {
            return null
        }

        this.hitScatter = null
        let interactionManager = this.renderer.plugins.interaction

        let displayObject = interactionManager.hitTest(local, this)
        if (displayObject != null) {
            if (displayObject.dontBlockScatter && displayObject.parent != null) {
                displayObject = interactionManager.hitTest(local, displayObject.parent)
            }

            if (displayObject.scatter != null) this.hitScatter = displayObject.scatter
            if (this.claimEvents) event.claimedByScatter = this.hitScatter
        }

        return this.hitScatter
    }
}

export class RigidScatterContainer extends AdvancedScatterContainer {
    constructor(width, height, renderer, opts) {
        super(renderer, opts)
        this._width = width
        this._height = height
    }

    resize(width, height) {
        this._width = width
        this._height = height
    }

    get width() {
        return this._width
    }

    get height() {
        return this._height
    }

    _calculateBounds() {
        this._bounds.clear()
        let bounds = new PIXI.Bounds()
        bounds.minX = this.parent.position.x
        bounds.minY = this.parent.position.y
        bounds.maxX = this.parent.position.x + this.width
        bounds.maxY = this.parent.position.y + this.height
        this._bounds.addBounds(bounds)
    }
}

export class RigidContainer extends PIXI.Graphics {
    constructor(width, height, { onResize = null } = {}) {
        super()
        if (!width || !height) {
            console.log(
                `Width (${width}) or height (${height}) not set! If this is intended use another PIXI.DisplayObject instead.`,
                this
            )
        }
        this._width = width
        this._height = height
        this.beginFill(0xffffff, 0.3)
        this.drawRect(0, 0, this.width, this.height)

        this.onResize = new EventHandler('resize', {
            listeners: onResize ? [onResize] : []
        })
    }

    /**
     * Resizes the scattercontainer to the provided dimensions.
     *
     * @param {number} width - Target width of the resize.
     * @param {number} height - Target height of the resize.
     * @memberof RigidContainer
     */
    resize(width, height) {
        this._width = width
        this._height = height
        this._updateHitArea()
        this.onResize.call(this, 'resize', { x: width, y: height })
    }

    _updateHitArea() {
        // this.hitArea = new PIXI.Rectangle(0,0, this.width, this.height)
    }

    get width() {
        return this._width
    }
    get height() {
        return this._height
    }
}

/**
 * Specialization of the DisplayObjectScatter with special behavior needed for maps.
 */
class AdvancedScatter extends DisplayObjectScatter {
    /**
     * Animates the throw and ensures that the map is always visible. This
     * is different from the standard behavior.
     *
     * @param {*} time
     * @memberof AdvancedScatter
     */
    animateThrow(time) {
        // In rare cases animateThrow is called when the displayObject is already removed
        if (this.displayObject.parent == null) {
            return
        }

        //Moved this outside, as the time continously increases,
        //when the object is not thrown.

        if (this.velocity != null) {
            let dt = this._throwDeltaTime()
            // This seems to just correct the velocity when its calculated wrong.
            // In what case can it get bigger? Velocity tends to always shrink, when no force is added.
            let d = this._getThrowDelta(dt)
            this._move(d)
            this.onDragUpdate(d)
            if (dt == 0 || this.needsAnimation()) {
                requestAnimationFrame(this.animateThrow.bind(this))
                return
            } else {
                if (this.isOutside()) {
                    requestAnimationFrame(this.animateThrow.bind(this))
                    return
                }
            }
        }
        this.onDragComplete()
    }

    get parent() {
        // Returns the parent is a scatter get the parent scatter.
        // If it's a scatter container, get the displayObject.
        // Other cases are not allowed.
        return this.displayObject.parent != null && this.displayObject.parent.scatter != null
            ? this.displayObject.parent.scatter
            : this.displayObject.parent
    }

    _getThrowDelta(dt) {
        this.velocity = this.nextVelocity(this.velocity)
        if (this.velocity != null) {
            return Points.multiplyScalar(this.velocity, dt)
        }
        return { x: 0, y: 0 }
    }

    freeze({ translatable = false, scalable = false, rotatable = false, movableX = false, movableY = false } = {}) {
        this.translatable = translatable
        this.scalable = scalable
        this.rotatable = rotatable
        this.movableX = movableX
        this.movableY = movableY
    }

    unfreeze({ translatable = true, scalable = true, rotatable = true, movableX = true, movableY = true } = {}) {
        this.freeze({
            translatable,
            scalable,
            rotatable: false,
            movableX,
            movableY
        })
    }
}

export class SubmapScatter extends DisplayObjectScatter {
    constructor(displayObject, renderer, opts = {}) {
        /*
            Currently the submaps are only working on one scale to
            avoid recalculations of the shown map.

            Therfore we force the scatter to not be scaleable.
        */
        Object.assign(opts, {
            minScale: 1,
            maxScale: 1,
            startScale: 1,
            overdoScaling: 1,
            scalable: false,
            resizable: true
        })

        super(displayObject, renderer, opts)
        this.onResize = opts.onResize
    }


    /**
     * Adds mouse wheel resizing to the submap.
     * 
     * @param {MouseEvent} event - Event containing the mouse data.
     * @memberof SubmapScatter
     */
    onMouseWheel(event) {
        super.onMouseWheel(event)

        if (this.onResize) {
            const zoomFactor = 0.001
            let zoom = 1 + zoomFactor * event.deltaY

            let anchor = {
                x: event.offsetX,
                y: event.offsetY
            }

            this.onResize(anchor, zoom)
        }
    }


    /**
     * Overwrites the gesture to get access to the interaction pivot and scaling information.
     * This calls the resize method to resize the submap.
     *
     * @private
     * @param {interaction} interaction - Interaction element containing various informations about the touch interaction.
     * @memberof SubmapScatter
     */
    gesture(interaction) {
        super.gesture(interaction)

        if (this.onResize) {
            let delta = interaction.delta()

            this.onResize(delta.about, delta.zoom)
        }
    }

    get width() {
        return this.displayObject.width * this.displayObject.scale.x
    }

    get height() {
        return this.displayObject.height * this.displayObject.scale.y
    }

    freeze({ translatable = false, scalable = false, rotatable = false, movableX = false, movableY = false } = {}) {
        this.translatable = translatable
        this.scalable = scalable
        this.rotatable = rotatable
        this.movableX = movableX
        this.movableY = movableY
    }

    unfreeze({ translatable = true, scalable = true, rotatable = true, movableX = true, movableY = true } = {}) {
        this.freeze({
            translatable,
            scalable,
            rotatable: false,
            movableX,
            movableY
        })
    }

    onZoomed(about) {
        super.onZoomed(about)
    }
}

/**
 * TODO
 * A scatter which width does not increase when child elements are added.
 */
export class CompactScatter extends AdvancedScatter {}

/**
 * A problem with PIXI Objects is, that the bounding box changes
 * with child elements. Therefore the sized scatter has a rigid size, that is not
 * changed by elements, but only with their width and height property.
 */
export class CoverScatter extends AdvancedScatter {
    constructor(a, b, opts) {
        super(a, b, opts)

        opts = Object.assign(
            {
                debug: false,
                cover: true,
                boundaries: {
                    min: { x: 0, y: 0 },
                    max: { x: 1, y: 1 }
                }
            },
            opts
        )

        this.debug = opts.debug
        this.cover = opts.cover
        this._boundaries = opts.boundaries

        if (this.debug) {
            this.debugGraphics = new PIXI.Graphics()
            this.displayObject.addChild(this.debugGraphics)
            this.debugGraphics.lineStyle(0.2, 0x00ff00)
            this.debugGraphics.drawRect(
                this.width * this.boundaries.min.x,
                this.height * this.boundaries.min.y,
                this.width * (this.boundaries.max.x - this.boundaries.min.x),
                this.height * (this.boundaries.max.y - this.boundaries.min.y)
            )

            this.debugGraphics.endFill()
        }

        if (this.cover) {
            // The reference to the element handler needs to be stored,
            // that we can remove it later on.
            this._applyInitialCover = this._applyInitialCover.bind(this)
            this.displayObject.on('added', this._applyInitialCover)
            this._applyInitialCover()
        }
    }

    _applyInitialCover() {
        if (this.debug) console.log('ApplyInitialCover: ', parent)

        if (this.displayObject.parent)
            this.forceCover(this.displayObject.parent.width, this.displayObject.parent.height)
    }

    get boundaries() {
        if (this._boundaries) return this._boundaries
        else
            return {
                min: { x: 0, y: 0 },
                max: { x: 1, y: 1 }
            }
    }

    transform(translate, zoom, rotate, anchor) {
        if (this.cover) {
            if (!this.parent) {
                return new PIXI.Rectangle(0, 0, window.innerWidth, window.innerHeight)
            }

            /**
             * If the scaling violates the minCoverScale, the zoom is adjusted to perfectly match
             * the minCoverScale.
             */
            let minCoverScale = this.calculateMinCoverScale(this.parent.width, this.parent.height)

            let { zoom: fixedZoom, scale } = this.calculateScale(zoom)
            if (scale < minCoverScale) {
                zoom = minCoverScale / this.scale
            }
        }

        super.transform(translate, zoom, rotate, anchor)
        if (this.cover) {
            let postTranslate = this.outsideBoundaries()
            super.transform(postTranslate, 1, 0, { x: 0, y: 0 })
        }
    }

    outsideBoundaries(movement = { x: 0, y: 0 }, { boundaries = this.boundaries } = {}) {
        if (this.parent == null) {
            return { x: 0, y: 0 }
        }
        let left = -(this.displayObject.position.x + movement.x + this.width * boundaries.min.x)
        let right = -left + this.width * (boundaries.max.x - boundaries.min.x) - this.parent.width

        let top = -(this.displayObject.position.y + movement.y + this.height * boundaries.min.y)
        let bot = -top + this.height * (boundaries.max.y - boundaries.min.y) - this.parent.height

        // Helper function to limit an Axis inside a container, or 'glue' it
        // to the lower side, if the size is smaller than the size of the
        // container.
        function limitAxis(low, high, contentSize, containerSize) {
            let val = 0
            if (low < 0) {
                val = low
            } else if (high < 0) {
                if (contentSize > containerSize) {
                    // ... and the element is bigger as the container
                    // - reset it to the containersize.
                    val = -high
                } else {
                    // ... and the size is not high enough, then
                    // 'glue' it to the low axis.
                    val = low
                }
            }

            return val
        }

        // Use the helper function to correct the movement to cover x and y.
        let correctionX = limitAxis(left, right, this.width * (boundaries.max.x - boundaries.min.x), this.parent.width)
        let correctionY = limitAxis(top, bot, this.height * (boundaries.max.y - boundaries.min.y), this.parent.height)
        let fixedMovement = {
            x: correctionX == 0 ? movement.x : correctionX + movement.x,
            y: correctionY == 0 ? movement.y : correctionY + movement.y
        }

        return fixedMovement
    }

    calculateMinCoverScale(width, height) {
        let scale = 0

        if (!(this.width == 0 && this.height == 0)) {
            let actualWidth = this.width / this.scale
            let actualHeight = this.height / this.scale

            let boundaryWidth = (this.boundaries.max.x - this.boundaries.min.x) * actualWidth
            let boundaryHeight = (this.boundaries.max.y - this.boundaries.min.y) * actualHeight

            let coverWidth = width / boundaryWidth
            let coverHeight = height / boundaryHeight

            scale = Math.max(coverWidth, coverHeight)
        }

        return scale
    }

    _getThrowDelta(dt) {
        let delta = super._getThrowDelta(dt)
        if (this.cover) {
            delta = this.outsideBoundaries(delta, {
                boundaries: this.boundaries
            })
        }
        return delta
    }

    containerChanged(width, height) {
        if (this.cover) this.forceCover(width, height)
    }

    forceCover() {
        this.requestScale(this.scale)
        let translate = this.outsideBoundaries()
        this.transform(translate, 1, 0, { x: 0, y: 0 })
    }

    requestScale(scale) {
        if (this.scalable && this.parent != null) {
            if (this.cover) {
                let minCoverScale = this.calculateMinCoverScale(this.parent.width, this.parent.height)
                if (scale < minCoverScale) {
                    scale = minCoverScale
                }
            }
            this.scale = scale
        }
    }

    requestFocus(point) {
        let allowedMovement = this.outsideBoundaries(Points.subtract(point, this.position))
        this.transform(allowedMovement, 1, 0, { x: 0, y: 0 })
    }
}

/**
 * The MapObjectScatter somewhat inverts the behaviour of
 * regular throwables. Regular throwables should bounce of the wall, when they
 * overlap to a certain amount. Maps on the otherhand are intended to overlap with the
 * scatter container heavily to hide irrelevant parts of the map without revealing
 * any background.
 *
 * @class
 * @extends DisplayObjectScatter
 */

export class MapObjectScatter extends CoverScatter {
    /**
     * x=null, y=null,
                        minScale=0.1,
                        maxScale=1.0,
                        startScale=1.0,
                        autoBringToFront=true,
                        translatable=true, scalable=true, rotatable=true, resizable=false,
                        movableX=true,
                        movableY=true,
                        throwVisibility=44,
                        throwDamping = 0.95,
                        autoThrow=true,
                        rotationDegrees=null,
                        rotation=null,
                        onTransform = null }
     */
    constructor(displayObject, renderer, opts = {}) {
        opts = Object.assign(
            {
                debug: false,
                cover: false,
                startScale: 1,
                boundaries: {
                    min: { x: 0, y: 0 },
                    max: { x: 1, y: 1 }
                },
                autoBringToFront: false
            },
            opts
        )
        super(displayObject, renderer, opts)

        // if (!renderer) {
        //     console.error('Renderer was not set!')
        //     return
        // }

        this.cover = opts.cover
    }

    moveTo() {
        super.moveTo(...arguments)
    }

    lock() {
        this.rotatable = false
        this.moveable = false
        this.scaleable = false
    }

    unlock() {
        this.rotatable = true
        this.moveable = true
        this.scaleable = true
    }

    getWorldScatter() {
        return this.parent
    }
}