$(document).ready(function() {
    // todo define things like 16x16, 128x128 etc. as constants?
    // also script debounce/throttle times
    const animationTime = 400; // defined in bitsy.js

    var bitsyData = {};

    var palette = {
        id: 0,
        background: {
            red: 62, 
            green: 43, 
            blue: 32,
        },
        tile: {
            red: 208, 
            green: 112, 
            blue: 56,
        },
        sprite: {
            red: 229, 
            green: 92, 
            blue: 68,
        }
    };

    var room = [];

    var tiles = [];

    var tileMatchThreshold = 0;

    var croptions = {
        url: 'https://i.imgur.com/ThQZ94v.jpg',
        viewport: {width: 128, height: 128, type: 'square'},
        boundary: {width: 256, height: 256},
        zoom: 0
    }

    var $croppie = $('#croppie');
    var croppie  = $croppie.croppie(croptions);

    function colourDifference(colour1, colour2) {
        difference = {};

        _.each(['red', 'green', 'blue'], function(key) {
            difference[key] = Math.abs(colour1[key] - colour2[key]);
        });

        // sum rgb differences
        return _.reduce(difference, function(sum, n) {
            return sum + n;
        }, 0);
    }

    function zeroPad(input, desiredLength) {
        while (input.length < desiredLength) {
            input = "0" + input;
        }

        return input;
    }

    function colourToHex(colour) {
        return '#' + zeroPad(Number(colour.red  ).toString(16), 2) 
                   + zeroPad(Number(colour.green).toString(16), 2) 
                   + zeroPad(Number(colour.blue ).toString(16), 2);
    }

    function hexToColour(hex) {
        var rgb = hex.match(/[\da-f]{2}/gi);

        return {
            red:   parseInt(rgb[0], 16),
            green: parseInt(rgb[1], 16),
            blue:  parseInt(rgb[2], 16),
        };
    }

    function getClosestColour(initialColour, colourOptions) {
        // ditch sprite colour as we're not using it atm
        delete colourOptions.sprite;

        _.each(palette, function(colour, name) {
            colourOptions[name].name = name;
            colourOptions[name].difference = colourDifference(initialColour, colour);
        });

        // lowest difference (closest) wins
        return _.first(_.sortBy(colourOptions, 'difference'));
    }

    function newTileName() {
        var tileNames = _.map(bitsyData.tiles, 'name');

        var i = 1; // start with 1 as 0 is an implicit tile

        while (tileNames.indexOf(i.toString(36)) > -1) {
            i++;
        }

        // base 36 = 0-9a-z
        return i.toString(36);
    }

    function handleBitsyGameData() {
        bitsyData = {};

        var input = $('#bitsy-data').val();

        // get palettes
        var palettes = input.match(/PAL (.*)\s(NAME (.*)\s)?([0-9,]*[\s]){3}/g);

        bitsyData.palettes = {};
        
        // do palettes always go 0..n?
        // will this cause problems if not?
        _.each(palettes, function(palette, n) {
            var thisPalette = {};
            var name = "";

            if (palette.match(/NAME (.+)\n/)) {
                name = palette.match(/NAME (.+)\n/)[0].replace('NAME ', '');
            } else if (palette.match(/PAL (\d+)\n/)) {
                name = palette.match(/PAL (\d+)\n/)[0].replace("PAL", "palette");
            }

            var colours = palette.match(/\d+,\d+,\d+/g);

            colours = _.map(colours, function(colour) {
                var rgb = colour.split(',');

                return {red: rgb[0], green: rgb[1], blue: rgb[2]};
            });

            bitsyData.palettes[name] = {
                id:         n,
                background: colours[0],
                tile:       colours[1],
                sprite:     colours[2],
            }
        });

        // get tiles

        bitsyData.tiles = [];

        // tile 0 (background colour only) is implicit in bitsy rather than being stored in the game data
        // so, make our own version
        bitsyData.tiles.push({
            name: "0", 
            bitmap: _.chunk(_.times(64, _.constant(0)), 8),
            new: false // this could also be used to stop it from being added to the game data, wooo
        });
        
        // todo: handle animated tiles properly instead of discarding the second animation frame
        var tiles = input.match(/TIL (.*)\n([01]{8}\n){8}(>\n([01]{8}\n){8})?/g); // everything after > is an optional second animation frame

        _.each(tiles, function(tile, i) {
            var name = tile.match(/TIL .*/)[0].replace('TIL ', '');
            
            tile = tile.replace(/TIL .*\n/, '');

            var bitmap = _.map(tile.match(/[01]/g), _.toInteger);

            var newTile = {
                name: name,
                new: false
            };

            // todo make this agnostic? i.e. tile.frames = _.chunk(bitmap, 64)

            if (bitmap.length === 64) { // normal tile
                newTile.bitmap = _.chunk(bitmap, 8);
            } else if (bitmap.length === 128) { // animated tile
                newTile.bitmap               = _.chunk(_.take(     bitmap, 64), 8);
                newTile.secondAnimationFrame = _.chunk(_.takeRight(bitmap, 64), 8);
            }

            bitsyData.tiles.push(newTile);
        });

        if (_.find(bitsyData.palettes, {'id': palette.id})) {
            // user has already selected a palette, leave it be

            // in case this is the first run:
            palette = _.find(bitsyData.palettes, {'id': palette.id})

            // if we just set the palette to the newly imported palette with the same ID,
            // we will lose any changes the user has made to the palettes
            // is this a big issue considering that the palettes cannot be currently saved anyway?
        } else {
            // set palette to first imported palette and redraw
            palette = _.first(_.sortBy(bitsyData.palettes, 'id'));
        }

        renderDebounced();

        // update palette picker
        $('tr.palette').remove();

        _.each(bitsyData.palettes, function(palette, name) {
            $('#palette tbody').append(
                '<tr class="palette">'
                  + '<td>'
                      + '<input type="radio" name="palette" id="palette-' + name + '">'
                      + '<input type="hidden" name="id" value="' + palette.id + '">'
                  + '</td>'
                  + '<td><label for="palette-' + name + '">' + name + '</label></td>'
                  + '<td><input type="color" name="background" value="' + colourToHex(palette.background) + '"></td>'
                  + '<td><input type="color" name="tile"       value="' + colourToHex(palette.tile)       + '"></td>'
                  + '<td><input type="color" name="sprite"     value="' + colourToHex(palette.sprite)     + '" disabled></td>'
              + '</tr>'
            );
        });

        $('input[name="id"][value="' + palette.id + '"]').siblings(':radio').trigger('click');
    }

    function readFile(input) {
        if (input.files && input.files[0]) {
            var reader = new FileReader();
            
            reader.onload = function (e) {
                $croppie.croppie('bind', {
                    url: e.target.result,
                    zoom: 0
                });
            }
            
            reader.readAsDataURL(input.files[0]);
        }
    }

    function render() {
        $croppie.croppie('result', {
            type: 'rawcanvas',
            size: 'viewport'
        }).then(function (result) {
            var imageData  = result.getContext('2d').getImageData(0, 0, 128, 128);
            var rawData    = imageData.data;
            var monochrome = [];

            var brightnessAdjustment = parseFloat($('#brightness').val());

            // for each pixel
            for (var i = 0; i < rawData.length; i += 4) {
                // this brightness adjustment is pretty crude but whatever
                var pixel = {
                    red:   _.clamp(rawData[i    ] + brightnessAdjustment, 0, 255),
                    green: _.clamp(rawData[i + 1] + brightnessAdjustment, 0, 255),
                    blue:  _.clamp(rawData[i + 2] + brightnessAdjustment, 0, 255),
                };

                var targetColour = getClosestColour(pixel, palette);

                if (targetColour.name === "background") {
                    monochrome.push(0);
                } else { // tile
                    monochrome.push(1)
                }

                rawData[i    ] = targetColour.red;
                rawData[i + 1] = targetColour.green;
                rawData[i + 2] = targetColour.blue;
                rawData[i + 3] = 255; // alpha
            }

            // split monochrome bitmap into equal chunks for easier x:y access
            monochrome = _.chunk(monochrome, 128);

            document.getElementById('preview').getContext('2d').putImageData(imageData, 0, 0);

            // tiled output

            room = [];

            _.times(16, function(tileY) {
                _.times(16, function(tileX) {
                    // make pseudo-tile from monochrome bitmap
                    var pseudoTile = [];

                    _.times(8, function(y) {
                        pseudoTile.push(
                            _.slice(monochrome[(tileY * 8) + y], (tileX * 8), (tileX * 8) + 8)
                        );
                    });

                    // if we want to always create new tiles, don't bother trying to check matches
                    if (tileMatchThreshold === 64) {
                        // even if we want to "always create new tiles" we still don't want to create duplicates
                        var bestMatch = _.find(bitsyData.tiles, function(tile) {
                            return _.isEqual(tile.bitmap, pseudoTile);
                        });

                        if (bestMatch) {
                            bestMatch.match = 64;
                        }
                    } else {
                        _.each(bitsyData.tiles, function(tile) {
                            tile.match = 0;

                            _.each(tile.bitmap, function(row, y) {
                                _.each(row, function(pixel, x) {
                                    if (parseInt(pixel) === parseInt(pseudoTile[y][x])) {
                                        tile.match++;
                                    }
                                });
                            });

                            if (tile.secondAnimationFrame) {
                                _.each(tile.secondAnimationFrame, function(row, y) {
                                    _.each(row, function(pixel, x) {
                                        if (parseInt(pixel) === parseInt(pseudoTile[y][x])) {
                                            tile.match++;
                                        }
                                    });
                                });

                                tile.match /= 2;
                            }
                        });

                        // what if there are several equally good matches?
                        // find highest match amount and find all of them
                        var bestMatchAmount = _.last(_.sortBy(bitsyData.tiles, ['match'])).match;
                        var bestMatches     = _.filter(bitsyData.tiles, {'match': bestMatchAmount});

                        // sort by name in ascending order
                        // earlier names are preferable
                        var bestMatch = _.first(_.sortBy(bestMatches, 'name'));
                    }

                    if ( ! bestMatch || bestMatch.match < tileMatchThreshold) {
                        // turn pseudo-tile into a real tile and add it to the tile data

                        var name = newTileName();

                        bitsyData.tiles.push({
                            name: name,
                            bitmap: pseudoTile,
                            new: true
                        });

                        room.push(name);

                        // issue with this approach:
                        // what if a tile we add late in the loop is a better match for an earlier "good enough" match?
                        // this would also cause different results if the user were to add the same room several times
                        // we could keep iterating until the room no longer changes
                    } else {
                        room.push(bestMatch.name);
                    }
                });
            });

            room = _.chunk(room, 16);

            // write room to output

            imageData = document.getElementById("room-output").getContext('2d').getImageData(0, 0, 128, 128);
            rawData   = imageData.data;

            _.each(room, function(row, tileY) {
                _.each(row, function(tileName, tileX) {
                    var tile = _.find(bitsyData.tiles, {'name' : tileName});

                    _.each(tile.bitmap, function(row, y) {
                        _.each(row, function(pixel, x) {
                            var position = (((tileY * 8) + y) * 128) + ((tileX * 8) + x);

                            position *= 4; // 4 values (rgba) per pixel

                            var pixelColour = {};

                            switch(parseInt(pixel)) {
                                case 0: pixelColour = palette.background; break;
                                case 1: pixelColour = palette.tile; break;
                                default: console.log("error");
                            }

                            rawData[position    ] = pixelColour.red;
                            rawData[position + 1] = pixelColour.green;
                            rawData[position + 2] = pixelColour.blue;
                            rawData[position + 3] = 255;
                        });
                    });
                });
            });

            document.getElementById('room-output').getContext('2d').putImageData(imageData, 0, 0);
        });
    }

    var renderDebounced = _.debounce(render, 30);
    var renderThrottled = _.throttle(render, 30);

    $croppie.on('update', renderDebounced);

    $('#brightness').on('change', renderThrottled);

    $('#brightness').on('dblclick', function() {
        $(this).val(0);

        renderDebounced();
    });

    $('label[for="brightness"]').on('click touchdown', function() {
        $('#brightness').trigger('dblclick');
    });

    $('#bitsy-data').on('change blur keyup', handleBitsyGameData);

    handleBitsyGameData();

    $('#imageUpload').on('change', function () {
        readFile(this);
    });

    // these inputs get added and removed from the DOM so the event handler needs to be on the document
    $(document).on('change', '#palette input', function() {
        var id = parseInt($(this).closest('.palette').find('input[name="id"]').val());

        // if this is a colour input, update the palette
        if ($(this).attr('type') === 'color') {
            if (id === palette.id) {
                palette[$(this).attr('name')] = hexToColour($(this).val());
            }
        }

        // if this is a radio button, pick this palette
        if ($(this).attr('type') === 'radio') {
            palette.id         = id;
            palette.background = hexToColour($(this).closest('.palette').find('input[name="background"]').val());
            palette.tile       = hexToColour($(this).closest('.palette').find('input[name="tile"]'      ).val());
            // sprite colour is not currently used
        }

        renderDebounced();
    });

    $(document).on('change', '#threshold', function() {
        var newValue = parseInt($(this).val());

        if (newValue < tileMatchThreshold) {
            // set tiles back to default
            bitsyData.tiles = _.filter(bitsyData.tiles, ['new', false]);
        }

        tileMatchThreshold = newValue;

        renderThrottled();
    });

    $('#save').on('click touchend', function() {
        var newGameData = $('textarea').val();

        // handle rooms

        // need to import IDs so we don't give the new room a conflicting ID
        var roomNames = newGameData.match(/ROOM \d+/g);

        var newRoomId = parseInt(_.last(roomNames).replace(/[^\d]+/g, "")) + 1;

        var newRoomName = $('#roomName').val();
        // remove invalid chars? what's invalid? newlines? are those possible?

        var newRoom = "ROOM " + newRoomId + "\n";

        _.each(room, function(row) {
            newRoom += _.toString(row) + "\n";
        });

        if (newRoomName) {
            newRoom += "NAME " + newRoomName + "\n";
        }

        newRoom += "PAL " + palette.id + "\n";

        newGameData = newGameData.replace(/(ROOM .*\n(.*\n)*PAL .*)/g, '$1\n\n' + newRoom);

        // handle tiles

        var newTiles = _.filter(bitsyData.tiles, 'new');
        var tileText = "";

        _.each(newTiles, function(tile) {
            tileText += "TIL " + tile.name + "\n"; //again, rename tile name to id...

            _.each(tile.bitmap, function(row) {
                tileText += row.join('') + "\n";
            });

            // don't need to worry about animation right now

            tileText += "\n";
        });

        newGameData = newGameData.replace(/(TIL.*(.*\n)*)SPR/g, '$1\n\n' + tileText + '\nSPR');

        // write
        $('textarea').val(newGameData);

        // todo: give the user some nice "yay! it worked!" kinda feedback?
    });
});
