// ==UserScript==
// @name         DK Art Bot
// @namespace
// @version      3.0
// @description  For DK I guess?
// @author       DK (Stolen from Union Flag Project)
// @match        https://www.reddit.com/r/place/*
// @match        https://new.reddit.com/r/place/*
// @match        https://www.reddit.com/r/place/
// @icon         https://www.google.com/s2/favicons?sz=64&domain=reddit.com
// @require	     https://cdn.jsdelivr.net/npm/toastify-js
// @resource     TOASTIFY_CSS https://cdn.jsdelivr.net/npm/toastify-js/src/toastify.min.css
// @updateURL    https://raw.githubusercontent.com/jeppevinkel/DKplace-client/master/userscript.user.js
// @downloadURL  https://raw.githubusercontent.com/jeppevinkel/DKplace-client/master/userscript.user.js
// @grant        GM_getResourceText
// @grant        GM_addStyle
// @grant        GM.xmlHttpRequest
// ==/UserScript==

var socket;
var order = undefined;
// var placeOrders = [];
// var canvas = document.createElement('canvas');
var accessToken;
var currentOrderCanvas = document.createElement('canvas');
var currentOrderCtx = currentOrderCanvas.getContext('2d');
var currentPlaceCanvas = document.createElement('canvas');
var cnc_url = 'place.jeppevinkel.com'

const DEFAULT_TOAST_DURATION_MS = 10000

const COLOR_MAPPINGS = {
    '#6D001A': 0,
    '#BE0039': 1,
    '#FF4500': 2,
    '#FFA800': 3,
    '#FFD635': 4,
    '#FFF8B8': 5,
    '#00A368': 6,
    '#00CC78': 7,
    '#7EED56': 8,
    '#00756F': 9,
    '#009EAA': 10,
    '#00CCC0': 11,
    '#2450A4': 12,
    '#3690EA': 13,
    '#51E9F4': 14,
    '#493AC1': 15,
    '#6A5CFF': 16,
    '#94B3FF': 17,
    '#811E9F': 18,
    '#B44AC0': 19,
    '#E4ABFF': 20,
    '#DE107F': 21,
    '#FF3881': 22,
    '#FF99AA': 23,
    '#6D482F': 24,
    '#9C6926': 25,
    '#FFB470': 26,
    '#000000': 27,
    '#515252': 28,
    '#898D90': 29,
    '#D4D7D9': 30,
    '#FFFFFF': 31,
}

let getRealWork = (rgbaOrder) => {
    let order = []
    for (var i = 0; i < 4000000; i++) {
        if (rgbaOrder[i * 4 + 3] !== 0) {
            order.push(i)
        }
    }
    return order
}

let getPendingWork = (work, rgbaOrder, rgbaCanvas) => {
    let pendingWork = []
    for (const i of work) {
      // pendingWork.push(i)
      // continue
        if (rgbaOrderToHex(i, rgbaOrder) !== rgbaOrderToHex(i, rgbaCanvas)) {
            pendingWork.push(i)
        }
    }
    return pendingWork
}

(async function () {
    GM_addStyle(GM_getResourceText('TOASTIFY_CSS'))
    currentOrderCanvas.width = 2000
    currentOrderCanvas.height = 2000
    currentOrderCanvas.style.display = 'none'
    currentOrderCanvas = document.body.appendChild(currentOrderCanvas)
    currentPlaceCanvas.width = 2000
    currentPlaceCanvas.height = 2000
    currentPlaceCanvas.style.display = 'none'
    currentPlaceCanvas = document.body.appendChild(currentPlaceCanvas)
    // canvas.width = 2000
    // canvas.height = 2000
    // canvas.style.display = 'none'
    // canvas = document.body.appendChild(canvas)

    Toastify({
        text: 'Getting Access Token...',
        duration: DEFAULT_TOAST_DURATION_MS,
    }).showToast()

    accessToken = await getAccessToken()

    Toastify({
        text: 'Collected!!',
        duration: DEFAULT_TOAST_DURATION_MS,
    }).showToast()

    connectSocket()
    attemptPlace()

    setInterval(async () => {
        if (socket) {
            const progress = await getProgress()
            if (progress.percentComplete >= 0) socket.send(JSON.stringify({type: 'progress', progress: progress.percentComplete, pendingPixels: progress.pendingPixels}))
        }
    }, 10000)
    setInterval(async () => {
        accessToken = await getAccessToken()
    }, 30 * 60 * 1000)
})()

function connectSocket() {
    Toastify({
        text: 'Connecting to Union Flag server...',
        duration: DEFAULT_TOAST_DURATION_MS,
    }).showToast()

    socket = new WebSocket(`wss://${cnc_url}/ws`)

    socket.onopen = function () {
        Toastify({
            text: 'Connected!',
            duration: DEFAULT_TOAST_DURATION_MS,
        }).showToast()
        socket.send(JSON.stringify({type: 'getmap'}))
        socket.send(JSON.stringify({type: 'brand', brand: 'userscriptV20'}))
    }

    socket.onmessage = async function (message) {
        var data
        try {
            data = JSON.parse(message.data)
        } catch (e) {
            return
        }

        switch (data.type.toLowerCase()) {
            case 'map':
                Toastify({
                    text: `New map loaded (Reason: ${
                        data.reason ? data.reason : 'connected to server'
                    })...`,
                    duration: DEFAULT_TOAST_DURATION_MS,
                }).showToast()
                currentOrderCtx = await getCanvasFromUrl(
                    `https://${cnc_url}/maps/${data.data}`,
                    currentOrderCanvas,
                    0,
                    0,
                    true,
                )
                order = getRealWork(currentOrderCtx.getImageData(0, 0, 2000, 2000).data)
                Toastify({
                    text: `New map loaded, ${order.length} pixels in total`,
                    duration: DEFAULT_TOAST_DURATION_MS,
                }).showToast()
                break
            case 'toast':
                Toastify({
                    text: `Message from server: ${data.message}`,
                    duration: data.duration || DEFAULT_TOAST_DURATION_MS,
                    style: data.style || {},
                }).showToast()
                break
            default:
                break
        }
    }

    socket.onclose = function (e) {
        Toastify({
            text: `Unoin Flag Server Disconnected: ${e.reason}`,
            duration: DEFAULT_TOAST_DURATION_MS,
        }).showToast()
        console.error('Socket timeout: ', e.reason)
        socket.close()
        setTimeout(connectSocket, 1000)
    }
}

function shuffleWeighted(array) {
	for (const item of array) {
		item.rndPriority = Math.round(placeOrders.priorities[item.priority] * Math.random());
	}
	array.sort((a, b) => b.rndPriority - a.rndPriority);
}

function getPixelList() {
	const structures = [];
	if (Date.now() > 1649133000 * 1000) {
		structures.push(placeOrders.structues["overwrite"])
	} else {
		for (const structureName in placeOrders.structures) {
			if (structureName != "overwrite") {
				shuffleWeighted(placeOrders.structures[structureName].pixels);
				structures.push(placeOrders.structures[structureName]);
			}
		}
		shuffleWeighted(structures);
	}
	return structures.map(structure => structure.pixels).flat();
}

async function attemptPlace() {
    if (order == undefined) {
        setTimeout(attemptPlace, 10000) // probeer opnieuw in 2sec.
        return
    }
    var ctx
    try {
        ctx = await getCanvasFromUrl(
            await getCurrentImageUrl('0'),
            currentPlaceCanvas,
            0,
            0,
            false,
        )
        ctx = await getCanvasFromUrl(
            await getCurrentImageUrl('1'),
            currentPlaceCanvas,
            1000,
            0,
            false,
        )
        ctx = await getCanvasFromUrl(
            await getCurrentImageUrl('2'),
            currentPlaceCanvas,
            0,
            1000,
            false,
        )
        ctx = await getCanvasFromUrl(
            await getCurrentImageUrl('3'),
            currentPlaceCanvas,
            1000,
            1000,
            false,
        )
    } catch (e) {
        console.warn('Error retrieving map: ', e)
        Toastify({
            text: 'Error retrieving map. Try again in 10 sec...',
            duration: DEFAULT_TOAST_DURATION_MS,
        }).showToast()
        setTimeout(attemptPlace, 10000) // Try again in 10sec.
        return
    }

    const rgbaOrder = currentOrderCtx.getImageData(0, 0, 2000, 2000).data
    const rgbaCanvas = ctx.getImageData(0, 0, 2000, 2000).data
    // const rgbaCanvas = ''
    const work = getPendingWork(order, rgbaOrder, rgbaCanvas)

    if (work.length === 0) {
        Toastify({
            text: `All pixels are already in the right place! Try again in 30 sec...`,
            duration: 30000,
        }).showToast()
        setTimeout(attemptPlace, 30000) // Try again in 30sec.
        return
    }

    const percentComplete = 100 - Math.ceil((work.length * 100) / order.length)
    // const percentComplete = 'NaN'
    const workRemaining = work.length
    const idx = Math.floor(Math.random() * work.length)
    const i = work[idx]
    const x = i % 2000
    const y = Math.floor(i / 2000)
    const hex = rgbaOrderToHex(i, rgbaOrder)

    Toastify({
        text: `Trying to place pixel ${x}, ${y}... (${percentComplete}% complete, ${workRemaining} left)`,
        duration: DEFAULT_TOAST_DURATION_MS,
    }).showToast()

    console.log(
        `Trying to place pixel ${x}, ${y}... (${percentComplete}% complete, ${workRemaining} left)`,
    )

    const res = await place(x, y, COLOR_MAPPINGS[hex])
    const data = await res.json()
    try {
        if (data.errors) {
            const error = data.errors[0]
            console.log('data.errors :>> ', data.errors)
            try {
                const nextPixel = error.extensions.nextAvailablePixelTs + 3000
                const nextPixelDate = new Date(nextPixel)
                
                if (nextPixelDate.getTime() != nextPixelDate.getTime()) {
                  let d = new Date(Date.now() + 10000);
                  let delay = d.getTime() - Date.now();
                  
                  Toastify({
                    text: `You are on cooldown! Trying again at ${d.toLocaleTimeString()}.`,
                    duration: delay,
                  }).showToast()
                  setTimeout(attemptPlace, delay)
                  return
                }
                
                const delay = nextPixelDate.getTime() - Date.now()
                const toast_duration = delay > 0 ? delay : DEFAULT_TOAST_DURATION_MS
                Toastify({
                    text: `You are on cooldown! Next pixel at ${nextPixelDate.toLocaleTimeString()}.`,
                    duration: toast_duration,
                }).showToast()
                setTimeout(attemptPlace, delay)
            } catch (e) {
                console.log("If you ignore it it doesn't exist." + e)
                Toastify({
                    text: `You are on cooldown!`,
                    duration: 10000,
                }).showToast()
                setTimeout(attemptPlace, 20000)
            }
        } else {
            const nextPixel =
                data.data.act.data[0].data.nextAvailablePixelTimestamp + 3000
            const nextPixelDate = new Date(nextPixel)
            const delay = nextPixelDate.getTime() - Date.now()
            const toast_duration = delay > 0 ? delay : DEFAULT_TOAST_DURATION_MS
            console.log(
                `Pixel placed at ${x}, ${y}! Next pixel will be placed at ${nextPixelDate.toLocaleTimeString()}`,
            )
            Toastify({
                text: `Pixel placed at ${x}, ${y}! Next pixel will be placed at ${nextPixelDate.toLocaleTimeString()}. Click to zoom to placed pixel`,
                duration: toast_duration,
                destination: `https://www.reddit.com/r/place/?cx=${x}&cy=${y}&px=2`,
                newWindow: true,
            }).showToast()
            setTimeout(attemptPlace, delay)
        }
    } catch (e) {
        console.warn('Response analysis error', e)
        Toastify({
            text: `Response analysis error: ${e}.`,
            duration: DEFAULT_TOAST_DURATION_MS,
        }).showToast()
        setTimeout(attemptPlace, 10000)
    }
}

async function place(x, y, color) {
    socket.send(JSON.stringify({type: 'placepixel', x, y, color}))
    return await fetch('https://gql-realtime-2.reddit.com/query', {
        method: 'POST',
        body: JSON.stringify({
            operationName: 'setPixel',
            variables: {
                input: {
                    actionName: 'r/replace:set_pixel',
                    PixelMessageData: {
                        coordinate: {
                            x: x % 1000,
                            y: y % 1000,
                        },
                        colorIndex: color,
                        canvasIndex: getCanvasId(x, y),
                    },
                },
            },
            query:
                `mutation setPixel($input: ActInput!) {
				act(input: $input) {
					data {
						... on BasicMessage {
							id
							data {
								... on GetUserCooldownResponseMessageData {
									nextAvailablePixelTimestamp
									__typename
								}
								... on SetPixelResponseMessageData {
									timestamp
									__typename
								}
								__typename
							}
							__typename
						}
						__typename
					}
					__typename
				}
			}
			`,
        }),
        headers: {
            origin: 'https://hot-potato.reddit.com',
            referer: 'https://hot-potato.reddit.com/',
            'apollographql-client-name': 'mona-lisa',
            Authorization: `Bearer ${accessToken}`,
            'Content-Type': 'application/json',
        },
    }).catch((error) => console.error('Error placing Pixel: ', error))
}

function getCanvasId(x,y) {
	return (x > 1000) + (y > 1000)*2
}

async function getProgress() {
  console.log('Sending progress...')
    if (order == undefined) {
        return -1;
    }
    var ctx;
    try {
        ctx = await getCanvasFromUrl(await getCurrentImageUrl('0'), currentPlaceCanvas, 0, 0, false);
        ctx = await getCanvasFromUrl(await getCurrentImageUrl('1'), currentPlaceCanvas, 1000, 0, false)
        ctx = await getCanvasFromUrl(await getCurrentImageUrl('2'), currentPlaceCanvas, 0, 1000, false)
        ctx = await getCanvasFromUrl(await getCurrentImageUrl('3'), currentPlaceCanvas, 1000, 1000, false)
    } catch (e) {
        return -2;
    }

    const rgbaOrder = currentOrderCtx.getImageData(0, 0, 2000, 2000).data;
    const rgbaCanvas = ctx.getImageData(0, 0, 2000, 2000).data;
    const work = getPendingWork(order, rgbaOrder, rgbaCanvas);
    const pendingPixels = []
  
    for (const curWork of work) {
      const x = curWork % 2000
      const y = Math.floor(curWork / 2000)
      pendingPixels.push({
        x,
        y
      })
    }

    const percentComplete = 100 - (work.length * 100 / order.length);
  
  return {
    percentComplete,
    pendingPixels
  };
}

async function getAccessToken() {
    const usingOldReddit = window.location.href.includes('new.reddit.com')
    const url = usingOldReddit
        ? 'https://new.reddit.com/r/place/'
        : 'https://www.reddit.com/r/place/'
    const response = await fetch(url)
    const responseText = await response.text()

    // TODO: ew
    return responseText.split('"accessToken":"')[1].split('"')[0]
}

async function getCurrentImageUrl(id = '0') {
    return new Promise((resolve, reject) => {
        const ws = new WebSocket(
            'wss://gql-realtime-2.reddit.com/query',
            'graphql-ws',
        )

        ws.onopen = () => {
            ws.send(
                JSON.stringify({
                    type: 'connection_init',
                    payload: {
                        Authorization: `Bearer ${accessToken}`,
                    },
                }),
            )
            ws.send(
                JSON.stringify({
                    id: '1',
                    type: 'start',
                    payload: {
                        variables: {
                            input: {
                                channel: {
                                    teamOwner: 'AFD2022',
                                    category: 'CANVAS',
                                    tag: id,
                                },
                            },
                        },
                        extensions: {},
                        operationName: 'replace',
                        query:
                            `subscription replace($input: SubscribeInput!) {
						subscribe(input: $input) {
							id
							... on BasicMessage {
								data {
									__typename
									... on FullFrameMessageData {
										__typename
										name
										timestamp
									}
								}
								__typename
							}
							__typename
						}
					}
					`,
                    },
                }),
            )
        }

        ws.onmessage = (message) => {
            const {data} = message
            const parsed = JSON.parse(data)

            // TODO: ew
            if (
                !parsed.payload ||
                !parsed.payload.data ||
                !parsed.payload.data.subscribe ||
                !parsed.payload.data.subscribe.data
            )
                return

            ws.close()
            resolve(
                parsed.payload.data.subscribe.data.name +
                `?noCache=${Date.now() * Math.random()}`,
            )
        }

        ws.onerror = reject
    })
}

function getCanvasFromUrl(url, canvas, x = 0, y = 0, clearCanvas = false) {
  return new Promise((resolve, reject) => {
    let loadImage = (ctx) => {
      GM.xmlHttpRequest({
        method: "GET",
        url: url,
        responseType: 'blob',
        onload: function(response) {
        var urlCreator = window.URL || window.webkitURL;
        console.log(response.response)
        var imageUrl = urlCreator.createObjectURL(response.response);
        console.log(imageUrl)
        var img = new Image()
        img.onload = () => {
          if (clearCanvas) {
            ctx.clearRect(0, 0, canvas.width, canvas.height)
          }
          ctx.drawImage(img, x, y)
          resolve(ctx)
        }
        img.onerror = () => {
          Toastify({
            text: 'Error retrieving folder. Try again in 3 sec...',
            duration: 3000,
          }).showToast()
          setTimeout(() => loadImage(ctx), 3000)
        }
        img.src = imageUrl;
    }
  })
  }
    loadImage(canvas.getContext('2d'))
  })
}

function rgbToHex(r, g, b) {
    return (
        '#' +
        ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1).toUpperCase()
    )
}

let rgbaOrderToHex = (i, rgbaOrder) =>
    rgbToHex(rgbaOrder[i * 4], rgbaOrder[i * 4 + 1], rgbaOrder[i * 4 + 2])