onmessage = function (e) {

    const result = dither(e.data.imageData, e.data.pixelSize, e.data.cutoff, e.data.blackRGBA, e.data.whiteRGBA)
    const reply = {}
    reply.imageData = result
    reply.pixelSize = e.data.pixelSize
    reply.cutoff = e.data.cutoff
    postMessage(reply)
}

function getRGBAArrayBuffer(color) {
    let buffer = new ArrayBuffer(4)
    for (let i = 0; i < 4; ++i) {
        buffer[i] = color[i]
    }
    return buffer
}

function dither(imageData, scaleFactor, cutoff, blackRGBA, whiteRGBA) {
    const blackRGBABuffer = getRGBAArrayBuffer(blackRGBA)
    const whiteRGBABuffer = getRGBAArrayBuffer(whiteRGBA)
    let output = new ImageData(imageData.width * scaleFactor, imageData.height * scaleFactor)
    for (let i = 0; i < imageData.data.length; i += 4) {
        imageData.data[i] = imageData.data[i + 1] = imageData.data[i + 2] = Math.floor(imageData.data[i] * 0.3 + imageData.data[i + 1] * 0.59 + imageData.data[i + 2] * 0.11)
    }

    // most implementations I see just distribute error into the existing image, wrapping around edge pixels
    // this implementation uses a sliding window of floats for more accuracy (probably not needed really)

    let slidingErrorWindow = [new Float32Array(imageData.width), new Float32Array(imageData.width), new Float32Array(imageData.width)]
    const offsets = [[1, 0], [2, 0], [-1, 1], [0, 1], [1, 1], [0, 2]]

    for (let y = 0, limY = imageData.height; y < limY; ++y) {
        for (let x = 0, limX = imageData.width; x < limX; ++x) {
            let i = ((y * limX) + x) * 4;
            let accumulatedError = Math.floor(slidingErrorWindow[0][x])
            let expectedMono = imageData.data[i] + accumulatedError
            let monoValue = expectedMono
            if (monoValue <= Math.floor(cutoff * 255)) {
                monoValue = 0
            } else {
                monoValue = 255
            }
            let error = (expectedMono - monoValue) / 8.0
            for (let q = 0; q < offsets.length; ++q) {
                let offsetX = offsets[q][0] + x
                let offsetY = offsets[q][1] + y
                if ((offsetX >= 0) && (offsetX < slidingErrorWindow[0].length))
                    slidingErrorWindow[offsets[q][1]][offsetX] += error
            }

            // this is stupid but we have to do the pixel scaling ourselves because safari insists on interpolating putImageData
            // which gives us blurry pixels (and it doesn't support the createImageBitmap call with an ImageData instance which
            // would make this easy)
            let rgba = (monoValue == 0) ? blackRGBABuffer : whiteRGBABuffer

            for (let scaleY = 0; scaleY < scaleFactor; ++scaleY) {
                let pixelOffset = (((y * scaleFactor + scaleY) * output.width) + (x * scaleFactor)) * 4
                for (let scaleX = 0; scaleX < scaleFactor; ++scaleX) {
                    output.data[pixelOffset] = rgba[0]
                    output.data[pixelOffset + 1] = rgba[1]
                    output.data[pixelOffset + 2] = rgba[2]
                    output.data[pixelOffset + 3] = rgba[3]
                    pixelOffset += 4
                }
            }
        }
        // move the sliding window
        slidingErrorWindow.push(slidingErrorWindow.shift())
        slidingErrorWindow[2].fill(0, 0, slidingErrorWindow[2].length)
    }
    return output
}