(function () {
    let pi = Math.PI
    let pi2 = 2 * Math.PI

    this.Waves = function (holder, options) {
        let Waves = this

        Waves.options = extend(options || {}, {
            resize: false,
            rotation: 45,
            waves: 5,
            width: 100,
            hue: [11, 14],
            amplitude: 0.5,
            background: true,
            preload: true,
            speed: [0.004, 0.008],
            debug: false,
            fps: false,
        })

        Waves.waves = []

        Waves.holder = document.querySelector(holder)
        Waves.canvas = document.createElement('canvas')
        Waves.ctx = Waves.canvas.getContext('2d')
        Waves.holder.appendChild(Waves.canvas)

        Waves.hue = Waves.options.hue[0]
        Waves.hueFw = true
        Waves.stats = new Stats()

        Waves.resize()
        Waves.init(Waves.options.preload)

        if (Waves.options.resize) {
            window.addEventListener('resize', () => {
                Waves.resize()
            }, false) 
        }
    }

    Waves.prototype.init = function (preload) {
        let Waves = this
        let options = Waves.options

        for (let i = 0; i < options.waves; i++) { Waves.waves[i] = new Wave(Waves) }

        if (preload) Waves.preload()
    }

    Waves.prototype.preload = function () {
        let Waves = this
        let options = Waves.options

        for (let i = 0; i < options.waves; i++) {
            Waves.updateColor()
            for (let j = 0; j < options.width; j++) {
                Waves.waves[i].update()
            }
        }
    }

    Waves.prototype.render = function () {
        let Waves = this
        let ctx = Waves.ctx
        let options = Waves.options

        Waves.updateColor()
        Waves.clear()

        if (Waves.options.debug) {
            ctx.beginPath()
            ctx.strokeStyle = '#f00'
            ctx.arc(Waves.centerX, Waves.centerY, Waves.radius, 0, pi2)
            ctx.stroke()
        }

        if (Waves.options.background) {
            Waves.background()
        }

        each(Waves.waves, (wave, i) => {
            wave.update()
            wave.draw()
        })
    }

    Waves.prototype.animate = function () {
        let Waves = this

        Waves.render()

        if (Waves.options.fps) {
            Waves.stats.log()
            Waves.ctx.font = '12px Arial'
            Waves.ctx.fillStyle = '#fff'
            Waves.ctx.fillText(Waves.stats.fps() + ' FPS', 10, 22)
        }

        window.requestAnimationFrame(Waves.animate.bind(Waves))
    }

    Waves.prototype.clear = function () {
        let Waves = this
        Waves.ctx.clearRect(0, 0, Waves.width, Waves.height)
    }

    Waves.prototype.background = function () {
        let Waves = this
        let ctx = Waves.ctx

        let gradient = Waves.ctx.createLinearGradient(0, 0, 0, Waves.height)
        gradient.addColorStop(0, '#000')
        gradient.addColorStop(1, Waves.color)

        ctx.fillStyle = gradient
        ctx.fillRect(0, 0, Waves.width, Waves.height)
    }

    Waves.prototype.resize = function () {
        let Waves = this
        let width = Waves.holder.offsetWidth
        let height = Waves.holder.offsetHeight
        Waves.scale = window.devicePixelRatio || 1
        Waves.width = width * Waves.scale
        Waves.height = height * Waves.scale
        Waves.canvas.width = Waves.width
        Waves.canvas.height = Waves.height
        Waves.canvas.style.width = width + 'px'
        Waves.canvas.style.height = height + 'px'
        Waves.radius = Math.sqrt(Waves.width ** 2 + Waves.height ** 2) / 2
        Waves.centerX = Waves.width / 2
        Waves.centerY = Waves.height / 2
    // Waves.radius /= 2; // REMOVE FOR FULLSREEN
    }

    Waves.prototype.updateColor = function () {
        let Waves = this

        Waves.hue += (Waves.hueFw) ? 0.01 : -0.01

        if (Waves.hue > Waves.options.hue[1] && Waves.hueFw) {
            Waves.hue = Waves.options.hue[1]
            Waves.Waves = false
        } else if (Waves.hue < Waves.options.hue[0] && !Waves.hueFw) {
            Waves.hue = Waves.options.hue[0]
            Waves.Waves = true
        }

        let a = Math.floor(127 * Math.sin(0.3 * Waves.hue + 0) + 128)
        let b = Math.floor(127 * Math.sin(0.3 * Waves.hue + 2) + 128)
        let c = Math.floor(127 * Math.sin(0.3 * Waves.hue + 4) + 128)

        Waves.color = 'rgba(' + a + ',' + b + ',' + c + ', 0.1)'
    }

    function Wave(Waves) {
        let Wave = this
        let speed = Waves.options.speed

        Wave.Waves = Waves
        Wave.Lines = []

        Wave.angle = [
            rnd(pi2),
            rnd(pi2),
            rnd(pi2),
            rnd(pi2),
        ]

        Wave.speed = [
            rnd(speed[0], speed[1]) * rnd_sign(),
            rnd(speed[0], speed[1]) * rnd_sign(),
            rnd(speed[0], speed[1]) * rnd_sign(),
            rnd(speed[0], speed[1]) * rnd_sign(),
        ]

        return Wave
    }

    Wave.prototype.update = function () {
        let Wave = this
        let Lines = Wave.Lines
        let color = Wave.Waves.color

        Lines.push(new Line(Wave, color))

        if (Lines.length > Wave.Waves.options.width) {
            Lines.shift()
        }
    }

    Wave.prototype.draw = function () {
        let Wave = this
        let Waves = Wave.Waves

        let ctx = Waves.ctx
        let radius = Waves.radius
        let radius3 = radius / 3
        let x = Waves.centerX
        let y = Waves.centerY
        let rotation = dtr(Waves.options.rotation)
        let amplitude = Waves.options.amplitude
        let debug = Waves.options.debug

        let Lines = Wave.Lines

        each(Lines, (line, i) => {
            if (debug && i > 0) return

            let angle = line.angle

            let x1 = x - radius * Math.cos(angle[0] * amplitude + rotation)
            let y1 = y - radius * Math.sin(angle[0] * amplitude + rotation)
            let x2 = x + radius * Math.cos(angle[3] * amplitude + rotation)
            let y2 = y + radius * Math.sin(angle[3] * amplitude + rotation)
            let cpx1 = x - radius3 * Math.cos(angle[1] * amplitude * 2)
            let cpy1 = y - radius3 * Math.sin(angle[1] * amplitude * 2)
            let cpx2 = x + radius3 * Math.cos(angle[2] * amplitude * 2)
            let cpy2 = y + radius3 * Math.sin(angle[2] * amplitude * 2)

            ctx.strokeStyle = (debug) ? '#fff' : line.color

            ctx.beginPath()
            ctx.moveTo(x1, y1)
            ctx.bezierCurveTo(cpx1, cpy1, cpx2, cpy2, x2, y2)
            ctx.stroke()

            if (debug) {
                ctx.strokeStyle = '#fff'
                ctx.globalAlpha = 0.3

                ctx.beginPath()
                ctx.moveTo(x1, y1)
                ctx.lineTo(cpx1, cpy1)
                ctx.stroke()
                ctx.beginPath()
                ctx.moveTo(x2, y2)
                ctx.lineTo(cpx2, cpy2)
                ctx.stroke()

                ctx.globalAlpha = 1
            }
        })
    }

    function Line(Wave, color) {
        let Line = this

        let angle = Wave.angle
        let speed = Wave.speed

        Line.angle = [
            Math.sin(angle[0] += speed[0]),
            Math.sin(angle[1] += speed[1]),
            Math.sin(angle[2] += speed[2]),
            Math.sin(angle[3] += speed[3]),
        ]

        Line.color = color
    }

    function Stats() {
        this.data = []
    }

    Stats.prototype.time = function () {
        return (performance || Date)
            .now()
    }

    Stats.prototype.log = function () {
        if (!this.last) {
            this.last = this.time()
            return 0
        }

        this.new = this.time()
        this.delta = this.new - this.last
        this.last = this.new

        this.data.push(this.delta)
        if (this.data.length > 10) { this.data.shift() }
    }

    Stats.prototype.fps = function () {
        let fps = 0
        each(this.data, (data, i) => {
            fps += data
        })

        return Math.round(1000 / (fps / this.data.length))
    }

    function each(items, callback) {
        for (let i = 0; i < items.length; i++) {
            callback(items[i], i)
        }
    }

    function extend(options, defaults) {
        for (let key in options) { if (defaults.hasOwnProperty(key)) defaults[key] = options[key] }
        return defaults
    }

    function dtr(deg) {
        return deg * pi / 180
    }

    function rtd(rad) {
        return rad * 180 / pi
    }

    function diagonal_angle(w, h) {
        let a = Math.atan2(h, w) * 1.27325
        return a
    }

    function rnd(a, b) {
        if (arguments.length == 1) { return Math.random() * a }
        return a + Math.random() * (b - a)
    }

    function rnd_sign() {
        return (Math.random() > 0.5) ? 1 : -1
    }
}())

let waves = new Waves('#holder', {
    fps: false,
    waves: 3,
    width: 200,
})

waves.animate()