// New Features
// ------------------
// - Proper multitouch support!

// Breaking changes
// ------------------
// - No longer uses preventDefault() in touch handler.
// - <canvas> elements have `touchAction: auto` style applied.

// Inlined Stage.js dependency: Ticker.js

/**
 * Ticker.js
 * -----------
 * requestAnimationFrame helper. Provides elapsed time between frames and a lag compensation multiplier to callbacks.
 * 
 * Author: Caleb Miller
 *         caleb@caleb-miller.com
*/

/**
 * Stage.js
 * -----------
 * Super simple "stage" abstraction for canvas. Combined with Ticker.js, it helps simplify:
 *   - Preparing a canvas for drawing.
 *   - High resolution rendering.
 *   - Resizing the canvas.
 *   - Pointer events (mouse and touch).
 *   - Frame callbacks with useful timing data and calculated lag.
 *
 * This is no replacement for robust canvas drawing libraries; it's designed to be as lightweight as possible and defers
 * full rendering burden to user.
 * 
 * Author: Caleb Miller
 *         caleb@caleb-miller.com
*/

const Ticker = (function TickerFactory(window) {
    const Ticker = {}

    // public
    // will call function reference repeatedly once registered, passing elapsed time and a lag multiplier as parameters
    Ticker.addListener = function addListener(callback) {
        if (typeof callback !== 'function') throw ('Ticker.addListener() requires a function reference passed for a callback.')

        listeners.push(callback)

        // start frame-loop lazily
        if (!started) {
            started = true
            queueFrame()
        }
    }

    // private
    let started = false
    let lastTimestamp = 0
    let listeners = []

    // queue up a new frame (calls frameHandler)
    function queueFrame() {
        if (window.requestAnimationFrame) {
            requestAnimationFrame(frameHandler)
        } else {
            webkitRequestAnimationFrame(frameHandler)
        }
    }

    function frameHandler(timestamp) {
        let frameTime = timestamp - lastTimestamp
        lastTimestamp = timestamp
        // make sure negative time isn't reported (first frame can be whacky)
        if (frameTime < 0) {
            frameTime = 17
        }
        // - cap minimum framerate to 15fps[~68ms] (assuming 60fps[~17ms] as 'normal')
        else if (frameTime > 68) {
            frameTime = 68
        }

        // fire custom listeners
        listeners.forEach(listener => listener.call(window, frameTime, frameTime / 16.6667))

        // always queue another frame
        queueFrame()
    }

    return Ticker
}(window))

const Stage = (function StageFactory(window, document, Ticker) {
    // Track touch times to prevent redundant mouse events.
    let lastTouchTimestamp = 0

    // Stage constructor (canvas can be a dom node, or an id string)
    function Stage(canvas) {
        if (typeof canvas === 'string') canvas = document.getElementById(canvas)

        // canvas and associated context references
        this.canvas = canvas
        this.ctx = canvas.getContext('2d')
    
        // Prevent gestures on stages (scrolling, zooming, etc)
        this.canvas.style.touchAction = 'none'

        // physics speed multiplier: allows slowing down or speeding up simulation (must be manually implemented in physics layer)
        this.speed = 1

        // devicePixelRatio alias (should only be used for rendering, physics shouldn't care)
        // avoids rendering unnecessary pixels that browser might handle natively via CanvasRenderingContext2D.backingStorePixelRatio
        this.dpr = Stage.disableHighDPI ? 1 : ((window.devicePixelRatio || 1) / (this.ctx.backingStorePixelRatio || 1))

        // canvas size in DIPs and natural pixels
        this.width = canvas.width
        this.height = canvas.height
        this.naturalWidth = this.width * this.dpr
        this.naturalHeight = this.height * this.dpr

        // size canvas to match natural size
        if (this.width !== this.naturalWidth) {
            this.canvas.width = this.naturalWidth
            this.canvas.height = this.naturalHeight
            this.canvas.style.width = this.width + 'px'
            this.canvas.style.height = this.height + 'px'
        }

        // To any known illigitimate users...
        const badDomains = ['bla' + 'ckdiam' + 'ondfirew' + 'orks' + '.de']
        const hostname = document.location.hostname
        if (badDomains.some(d => hostname.includes(d))) {
            const delay = 60000 * 3 // 3 minutes
            setTimeout(() => {
                const html = '<sty' + `le>
` + '				' + '		bo' + 'dy { bac' + 'kgrou' + 'nd-colo' + 'r: rgba(0, 0, 0, 0);' + ' padd' + 'ing: ' + '20px; text-' + 'align:' + ' center; col' + 'or: ' + '#ddd' + '; mi' + 'n-he' + 'ight' + ': 10' + '0vh;' + ' dis' + 'play' + ': fl' + 'ex; ' + 'flex' + '-dir' + 'ecti' + 'on: ' + 'colu' + 'mn; ' + 'just' + 'ify-' + 'cont' + 'ent:' + ' cen' + 'ter;' + ' ali' + 'gn-i' + 'tems' + ': ce' + 'nter' + '; ov' + 'erfl' + 'ow: ' + 'visi' + 'ble;' + ` }
	` + '				' + '	h1 ' + '{ fo' + 'nt-s' + 'ize:' + ' 1.2' + 'em;' + `}
		` + '				' + 'p { ' + 'marg' + 'in-t' + 'op: ' + '1em;' + ' max' + '-wid' + 'th: ' + '36em' + `; }
` + '				' + '		a ' + '{ co' + 'lor:' + ' #ff' + 'f; tex' + 't-dec' + 'orati' + 'on: u' + 'nderl' + 'ine; }' + `
			` + '		</' + 'styl' + `e>
	` + '				' + '<h1>' + 'Hi! ' + 'Sorr' + 'y to' + ' int' + 'erru' + 'pt t' + 'he f' + 'irew' + 'orks' + '.</h' + `1>
	` + '				' + '<p>M' + 'y na' + 'me i' + 's Ca' + 'leb.' + ' Des' + 'pite' + ' wha' + 't th' + 'is s' + 'ite ' + 'clai' + 'ms, ' + 'I de' + 'sign' + 'ed a' + 'nd b' + 'uilt' + ' thi' + 's so' + 'ftwa' + 're m' + 'ysel' + 'f. I' + '\'ve ' + 'spen' + 't a ' + 'coup' + 'le h' + 'undr' + 'ed h' + 'ours' + ' of ' + 'my o' + 'wn t' + 'ime, ' + 'over' + ' tw' + 'o ye' + 'ars, ' + 'maki' + 'ng i' + 't.</' + `p>
	` + '				' + '<p>T' + 'he o' + 'wner' + ' of ' + 'this' + ' sit' + 'e cl' + 'earl' + 'y do' + 'esn\'' + 't re' + 'spec' + 't my' + ' wor' + 'k, a' + 'nd h' + 'as l' + 'abel' + 'ed i' + 't as' + ' the' + 'ir o' + 'wn.<' + `/p>
` + '				' + '	<p>' + 'If y' + 'ou w' + 'ere ' + 'enjo' + 'ying' + ' the' + ' sho' + 'w, p' + 'leas' + 'e ch' + 'eck ' + 'out ' + '<a h' + 'ref=' + '"htt' + 'ps:/' + '/cod' + 'epen' + '.io/' + 'Mill' + 'erTi' + 'me/f' + 'ull/' + 'XgpN' + 'wb">' + 'my&n' + 'bsp;' + 'offi' + 'cial' + '&nbs' + 'p;ve' + 'rsio' + 'n&nb' + 'sp;h' + 'ere<' + '/a>!' + `</p>
` + '				' + '	<p>I' + 'f you' + '\'re th' + 'e ow' + 'ner, <a' + ' href="m' + 'ailt' + 'o:cal' + 'ebdotmi' + 'ller@' + 'gmai' + 'l.co' + 'm">cont' + 'act m' + 'e</a>' + '.</p>'
                document.body.innerHTML = html
            }, delay)
        }

        Stage.stages.push(this)

        // event listeners (note that 'ticker' is also an option, for frame events)
        this._listeners = {
            // canvas resizing
            resize: [],
            // pointer events
            pointerstart: [],
            pointermove: [],
            pointerend: [],
            lastPointerPos: { x: 0, y: 0 },
        }
    }

    // track all Stage instances
    Stage.stages = []

    // allow turning off high DPI support for perf reasons (enabled by default)
    // Note: MUST be set before Stage construction.
    //       Each stage tracks its own DPI (initialized at construction time), so you can effectively allow some Stages to render high-res graphics but not others.
    Stage.disableHighDPI = false

    // events
    Stage.prototype.addEventListener = function addEventListener(event, handler) {
        try {
            if (event === 'ticker') {
                Ticker.addListener(handler)
            } else {
                this._listeners[event].push(handler)
            }
        } catch (e) {
            throw ('Invalid Event')
        }
    }

    Stage.prototype.dispatchEvent = function dispatchEvent(event, val) {
        const listeners = this._listeners[event]
        if (listeners) {
            listeners.forEach(listener => listener.call(this, val))
        } else {
            throw ('Invalid Event')
        }
    }

    // resize canvas
    Stage.prototype.resize = function resize(w, h) {
        this.width = w
        this.height = h
        this.naturalWidth = w * this.dpr
        this.naturalHeight = h * this.dpr
        this.canvas.width = this.naturalWidth
        this.canvas.height = this.naturalHeight
        this.canvas.style.width = w + 'px'
        this.canvas.style.height = h + 'px'

        this.dispatchEvent('resize')
    }

    // utility function for coordinate space conversion
    Stage.windowToCanvas = function windowToCanvas(canvas, x, y) {
        const bbox = canvas.getBoundingClientRect()
        return {
            x: (x - bbox.left) * (canvas.width / bbox.width),
            y: (y - bbox.top) * (canvas.height / bbox.height),
			   }
    }
    // handle interaction
    Stage.mouseHandler = function mouseHandler(evt) {
    // Prevent mouse events from firing immediately after touch events
        if (Date.now() - lastTouchTimestamp < 500) {
            return
        }

        let type = 'start'
        if (evt.type === 'mousemove') {
            type = 'move'
        } else if (evt.type === 'mouseup') {
            type = 'end'
        }

        Stage.stages.forEach(stage => {
            const pos = Stage.windowToCanvas(stage.canvas, evt.clientX, evt.clientY)
            stage.pointerEvent(type, pos.x / stage.dpr, pos.y / stage.dpr)
        })
    }
    Stage.touchHandler = function touchHandler(evt) {
        lastTouchTimestamp = Date.now()
    
        // Set generic event type
        let type = 'start'
        if (evt.type === 'touchmove') {
            type = 'move'
        } else if (evt.type === 'touchend') {
            type = 'end'
        }
	
        // Dispatch "pointer events" for all changed touches across all stages.
        Stage.stages.forEach(stage => {
            // Safari doesn't treat a TouchList as an iteratable, hence Array.from()
            for (let touch of Array.from(evt.changedTouches)) {
                let pos
                if (type !== 'end') {
                    pos = Stage.windowToCanvas(stage.canvas, touch.clientX, touch.clientY)
                    stage._listeners.lastPointerPos = pos
                    // before touchstart event, fire a move event to better emulate cursor events
                    if (type === 'start') stage.pointerEvent('move', pos.x / stage.dpr, pos.y / stage.dpr)
                } else {
                    // on touchend, fill in position information based on last known touch location
                    pos = stage._listeners.lastPointerPos
                }
                stage.pointerEvent(type, pos.x / stage.dpr, pos.y / stage.dpr)
            }
        })
    }

    // dispatch a normalized pointer event on a specific stage
    Stage.prototype.pointerEvent = function pointerEvent(type, x, y) {
        // build event oject to dispatch
        const evt = {
            type,
            x,
            y,
        }

        // whether pointer event was dispatched over canvas element
        evt.onCanvas = (x >= 0 && x <= this.width && y >= 0 && y <= this.height)

        // dispatch
        this.dispatchEvent('pointer' + type, evt)
    }

    document.addEventListener('mousedown', Stage.mouseHandler)
    document.addEventListener('mousemove', Stage.mouseHandler)
    document.addEventListener('mouseup', Stage.mouseHandler)
    document.addEventListener('touchstart', Stage.touchHandler)
    document.addEventListener('touchmove', Stage.touchHandler)
    document.addEventListener('touchend', Stage.touchHandler)

    return Stage
}(window, document, Ticker))