import { Points } from '../../utils.js'

export class ObjectUtils {
    /**
     * Finds the previous key in an object.
     *
     * @param {string} targetKey - The target key, of whom the predecessor is wanted.
     */
    static findPredecessor(targetKey, obj) {
        // Initialize pointers ...
        let prev = null
        let first = null
        // ... and a hit flag.
        let hit = false

        //Iterate over all available items.
        for (let key in obj)
            if (obj.hasOwnProperty(key)) {
                if (!hit) {
                    //Assign both values,
                    //while not hit
                    prev = first
                    first = key

                    //When hit, set flag.
                    if (key == targetKey) {
                        hit = true
                        if (prev) return prev //If it was not hit on the first item, we can return prev.
                    }
                } else {
                    //Otherwise, when first item was hit, we iterate over the entire object.
                    first = key
                }
            }

        return first
    }

    /**
     * Finds the successing key in an object.
     *
     * @param {string} targetKey - The key of whom the successor key should be found.
     * @param {object} obj  - The object, that is scanned for the successor.
     */
    static findSuccessor(targetKey, obj) {
        let first = null
        let next = false

        for (let key in obj)
            if (obj.hasOwnProperty(key)) {
                if (!first) first = key

                if (next) {
                    return key
                }

                if (targetKey == key) {
                    next = true
                }
            }

        return first
    }

    static fromPath(obj, path, separator = '.') {
        let arr = path.split(separator)
        let result = obj

        for (let i = 0; i < arr.length; i++) {
            if (result[arr[i]] !== null) {
                result = result[arr[i]]
            } else return null
        }

        return result
    }
}

export class PathUtils {
    static fixTrailingSlash(url) {
        return url.replace(/\/?$/, '/')
    }
}

export class TimeUtils {
    static minutesToMs(minutes) {
        return TimeUtils.minutesToSeconds(minutes) * 1000
    }

    static minutesToSeconds(minutes) {
        return minutes * 60
    }
}

export class StringUtils {
    /* Used from: https://stackoverflow.com/questions/1026069/how-do-i-make-the-first-letter-of-a-string-uppercase-in-javascript */
    static capitalize(string) {
        return string.charAt(0).toUpperCase() + string.slice(1)
    }
}

export class GeometryUtils {
    static RandomRange(min, max) {
        return GeometryUtils.toRadians(Math.random() * (max - min) + min)
    }

    static RandomSign() {
        return Math.sign(Math.random() - 0.5)
    }

    static toDegrees(radians) {
        return (radians * 180) / Math.PI
    }

    static toRadians(degrees) {
        return (degrees / 180) * Math.PI
    }

    /*
    Recursion is buggy.
    */
    static subdivide(polygon, factor = 0.5, n = 0) {
        console.log(polygon.length, factor)
        let points = []
        for (let i = 0; i < polygon.length; i++) {
            let prev = i - 1 < 0 ? polygon[polygon.length - 1] : polygon[i - 1]
            let next = i + 1 < polygon.length ? polygon[i + 1] : polygon[0]
            let current = polygon[i]
            points.push(GeometryUtils.linearInterpolation(prev, current, factor))
            points.push(GeometryUtils.linearInterpolation(current, next, 1 - factor))
        }
        if (n > 0) {
            n--
            points = GeometryUtils.subdivide(points, n)
        }
        return points
    }

    static linearInterpolation(first, second, n) {
        let point = new PIXI.Point()

        point.x = first.x * n + second.x * (1 - n)
        point.y = first.y * n + second.y * (1 - n)
        return point
    }
}

export class Matrix {
    constructor(a, b, d, e, c = 0, f = 0) {
        Object.assign(this, {
            a,
            b,
            c,
            d,
            e,
            f,
            g: 0,
            h: 0,
            i: 1
        })
    }

    static Transpose(m) {
        let first = ['b', 'c', 'f']
        let second = ['d', 'g', 'h']

        for (let i = 0; i < first.length; i++) {
            let tmp = m[first[i]]
            first[i] = second[i]
            second[i] = tmp
        }
    }

    static Rotation(deg) {
        let rad = (2 * Math.PI * deg) / 180
        return new Matrix(Math.cos(rad), -Math.sin(rad), Math.sin(rad), Math.cos(rad))
    }

    static Transform(x, y) {
        return new Matrix(1, 0, 0, 1, x, y)
    }

    static MultiplyPoint(m, p) {
        x = p.x * m.a + m.b * p.y + m.c * 1
        y = p.x * m.d + p.y * m.e + m.f * 1

        return {
            x,
            y
        }
    }
}

export class DomUtils {
    static getCenter(element) {
        let x = (element.clientLeft + element.clientWidth) / 2
        let y = (element.clientTop + element.clientHeight) / 2
        return { x, y }
    }

    static getTransformedPosition(element) {
        let mat = this.getCSSMatrix(element)
        return { x: mat[4], y: mat[5] }
    }

    static getCSSMatrix(element) {
        let matrix = window.getComputedStyle(element).transform

        if (matrix == 'none') {
            return [1, 0, 0, 1, 0, 0]
        } else {
            const pre = 'matrix('
            let values = matrix.substring(pre.length, matrix.length - 1).split(',')
            for (let i in values) {
                values[i] = parseFloat(values[i])
            }
            return values
        }
    }

    static positionOnElement(element, position) {
        const matrix = this.getCSSMatrix(element)

        const inX = element.offsetWidth * (position.x - 0.5)
        const inY = element.offsetHeight * (position.y - 0.5)

        let outX = matrix[0] * inX + matrix[2] * inY + matrix[4]
        let outY = matrix[1] * inX + matrix[3] * inY + matrix[5]

        let out = {
            x: outX,
            y: outY
        }

        return out
    }

    static applyTransform(target, transformedPosition, size = 0) {
        return {
            x: target.offsetLeft + target.offsetWidth / 2 + transformedPosition.x - size / 2,
            y: target.offsetTop + target.offsetHeight / 2 + transformedPosition.y - size / 2
        }
    }
}

export class Vector {
    static length(vector) {
        return Math.sqrt(vector.x * vector.x + vector.y * vector.y)
    }

    static normalize(vector) {
        return Points.multiplyScalar(vector, 1 / this.length(vector))
    }

    static scaleTo(vector, length) {
        let normalized = this.normalize(vector)
        return Points.multiplyScalar(normalized, length)
    }
}

export class PIXIUtils {
    /*
     * Transform a pixi text to it's actual screensize,
     * ignoring it's local transforms
     */
    static toScreenFontSize(pixiText, fontSize = null) {
        pixiText._recursivePostUpdateTransform();

        let normalizedScale = {
            x: pixiText.scale.x / pixiText.transform.worldTransform.a,
            y: pixiText.scale.x / pixiText.transform.worldTransform.d
        };

        pixiText.scale = { x: normalizedScale.x, y: normalizedScale.y };
        if (fontSize) pixiText.style.fontSize = fontSize;
    }

    static saveFill(graphics) {
        return {
            fill: graphics.fill.color,
            alpha: graphics.fill.alpha
        }
    }
}

/**
 * The EventHandler class is used to take care of a event based design
 * pattern. Callbacks can subscribe to an event and these unknown sources
 * get notified whenever the event changes.
 *
 * @export
 * @class EventHandler
 */
export class EventHandler {
    /**
     * Creates an instance of EventHandler.
     * @param {any} name
     * @param {any} [{
     *         listeners = [] - With the listnerers parameter the user can specify a function, array of functions or null (no function - useful when used in constructor with optional parameter).
     *     }={}]
     * @memberof EventHandler
     */
    constructor(name, { listeners = [] } = {}) {
        this.name = name
        this.listeners = []
        this.onces = []

        /**
         * One may initialize the eventListener using a parameter
         * that is either passed or null.
         */

        if (listeners == null) {
            // Null is a valid value as the EventHandler assumes no listener is passed on purpose.
            // This is useful, when a default parameter is passed as null.
        } else if (Array.isArray(listeners)) this.listeners = listeners
        else if (typeof listeners == 'function') {
            this.listeners = []
            this.add(listeners)
        } else {
            console.warn(
                "The provided 'listeners' is neither an Array of functions, nor a function. No eventcallback was added!",
                listeners,
                this
            )
        }
    }

    addMultiple(...callbacks) {
        for (let callback of callbacks) {
            this.listeners.push(callback)
        }
    }

    add(callback) {
        this.listeners.push(callback)
        return callback
    }

    once(callback) {
        this.onces.push(callback)
    }

    remove(func) {
        for (const [idx, listener] of this.listeners.entries()) {
            if (listener === func) {
                this.listeners.splice(idx, 1)
                return true
            }
        }
        return false
    }

    empty() {
        this.listeners = []
    }

    call(context, ...args) {
        if (context == null) {
            this.listeners.forEach(listener => listener(...args))
            this.onces.forEach(listener => listener(...args))
        } else {
            this.listeners.forEach(listener => listener.call(context, ...args))
            this.onces.forEach(listener => listener.call(context, ...args))
        }

        this.onces = []
    }

    get length() {
        return this.listeners.length + this.onces.length
    }
}

export class Dom {
    /**
     *  Popups should be displayed right over the text.
     *  Normally we would expect the popup to appear right over
     * the center of the text. A problem in HTML is, that it's hard
     * to determine the position of a text link, when it has a line-break
     * in it.
     *
     * This function solves this problem in the (so far) only possible way.
     *
     * 1. It removes the link from the dom tree.
     * 2. It adds an empty copy A of the link to the dom tree. (Copy is important, as the same styles have to be applied.)
     * 3. The contents of the link are added one by one to A.
     * 4. If the resulting boundingRect is bigger than the previous one, a line break is detected.
     * 5. The old line is tested, if the point was inside that boundingBox. If so save that bounding box (Goto: 7), else:
     * 6. Saves the content to a preceding clone B. And repeats from 3.
     * 7. Replace A with the initial content
     * 8. Return the found BoundingBox. If none found. Return the last bounding box.
     */
    static getTextHitRect(link, point) {
        // We cannot use it as it produces axis aligned bounding boxes
        /* if (true) {
             let rects = link.getClientRects()
             let target = null
             for (let [idx, rect] of Object.entries(rects)) {
                 target = rect
                 if (Rect.contains(rect, point))
                     break
             }
             return target
 
         } else {*/
        let processedText = link.cloneNode(true)
        let content = processedText.innerHTML
        let words = content.split(/ /g)
        processedText.innerHTML = ''
        event.target.innerHTML = ''
        // let lineRect = event.target.getBoundingClientRect()
        let local = Points.fromPageToNode(event.target.parentNode, point)
        console.log(local)

        let target = event.target
        let height = 0

        while (words.length > 0) {
            let word = words.pop()
            target.innerHTML += word + ' '
            if (target.height != height) {
                // New line was reached.
                console.log('NEW LINE WAS REACHED!')
            }
        }

        return {
            top: 0,
            left: 0,
            right: 100,
            bottom: 100,
            width: 100,
            height: 100
        }

        // let total = words.length
        // while (words.length > 0) {
        //     let lastRect = lineRect
        //     let lastContent = event.target.innerHTML
        //     let added = words.length == total ? "" : " "
        //     added += words.shift()
        //     event.target.innerHTML += added
        //     lineRect = event.target.getBoundingClientRect()

        //     // When new line or last line:
        //     if (lineRect.height != lastRect.height) {
        //         //Reconstructure last line.
        //         event.target.innerHTML = lastContent

        //         //Create Rect from last line.
        //         lineRect = event.target.getBoundingClientRect()
        //         rects.push(lineRect)

        //         //Copy last line content to processed text.
        //         processedText.innerHTML += lastContent

        //         // Create content of new line.
        //         event.target.innerHTML = added

        //         if (!processedText.parentNode) event.target.parentNode.insertBefore(processedText, event.target)

        //         if (Rect.contains(lineRect, point)) {
        //             break
        //         }
        //     }

        //     if (words.length == 0) {
        //         lineRect = event.target.getBoundingClientRect()
        //         processedText.innerHTML += event.target.innerHTML
        //         rects.push(lineRect)
        //     }
        // }

        // event.target.innerHTML = content
        // if (processedText.parentNode) processedText.parentNode.removeChild(processedText)

        // return lineRect
        // }
    }

    static printDomRect(rect, { color = 'red', pad = 0, parent = document.body }) {
        let element = document.createElement('div')
        Object.assign(element.style, {
            padding: pad + 'px',
            borderColor: color,
            borderWidth: '2px',
            borderStyle: 'solid',
            top: rect.top + 'px',
            left: rect.left + 'px',
            width: rect.width + 'px',
            height: rect.height + 'px',
            position: 'absolute',
            zIndex: 10000,
            opacity: 0.3
        })
        parent.appendChild(element)
    }

    static printDomPoint(point, { color = 'red', parent = document.body }) {
        let element = document.createElement('div')
        Object.assign(element.style, {
            position: 'absolute',
            top: point.y + 'px',
            left: point.x + 'px',
            width: '10px',
            height: '10px',
            backgroundColor: color,
            zIndex: 10000
        })
        parent.appendChild(element)
    }
}