/********************************************************************************
 This file contains code related to the Pose Engine,
 as well as code related to displaying tables for selection and the main game.
 ********************************************************************************/

/* NOTE: These are basically the same as epilogue engine sprites.
 * There's a _lot_ of common code here that can probably be merged.
 */
function PoseSprite(id, src, onload, pose, args) {
    this.pose = pose;
    this.id = id;
    this.player = args.player;
    this.src = this.prevSrc = 'opponents/' + src;
    this.x = args.x || 0;
    this.y = args.y || 0;
    this.z = args.z || 'auto';
    this.scalex = args.scalex || 1;
    this.scaley = args.scaley || 1;
    this.skewx = args.skewx || 0;
    this.skewy = args.skewy || 0;
    this.rotation = args.rotation || 0;
    this.alpha = args.alpha;
    this.pivotx = args.pivotx;
    this.pivoty = args.pivoty;
    this.height = args.height || 0;
    this.width = args.width || 0;
    this.delay = args.delay || 0;
    this.elapsed = 0;
    this.parentId = args.parent;
    
    this.vehicle = document.createElement('div');
    this.vehicle.id = id;
    this.pivot = document.createElement('div');
    this.vehicle.appendChild(this.pivot);
    
    this.img = document.createElement('img');
    this.img.onload = this.img.onerror = function() {
        if (!this.height) this.height = this.img.naturalHeight;
        if (!this.width) this.width = this.img.naturalWidth;
        
        onload(this);
        this.draw();
    }.bind(this);
    this.img.src = this.src;
    
    this.pivot.appendChild(this.img);
    
    if (this.alpha === undefined) {
        this.alpha = 100;
    }
    
    if (this.pivotx || this.pivoty) {
        this.pivotx = this.pivotx || "center";
        this.pivoty = this.pivoty || "center";
        $(this.pivot).css("transform-origin", this.pivotx + " " + this.pivoty);
    }
    
    $(this.vehicle).css("z-index", this.z);
}

PoseSprite.prototype.linkParent = function () {
    if (this.parentId) {
        this.parent = this.pose.sprites[this.parentId];
        this.parent.pivot.appendChild(this.vehicle);
    }
}

PoseSprite.prototype.scaleToDisplay = function(x) {
    return x * this.pose.getHeightScaleFactor();
}

PoseSprite.prototype.update = function (dt) {
    if (this.elapsed < this.delay) {
        this.elapsed += dt;
        if (this.elapsed >= this.delay) {
            this.draw();
        }
    }
}

PoseSprite.prototype.draw = function() {
    var alpha = this.alpha / 100;
    if (this.elapsed < this.delay) {
        alpha = 0;
    }
    var properties = {
        "position": "absolute",
        "left": "50%",
        "top": "0",
        "transform": "translateX(-50%) translateX(" + this.scaleToDisplay(this.x) + "px) translateY(" + this.scaleToDisplay(this.y) + "px)",
        "transform-origin": "top left",
        "opacity": alpha,
        "height": '100%',
    };
    if (this.parent) {
        properties.left = 0;
        properties.transform = "translateX(" + this.scaleToDisplay(this.x) + "px) translateY(" + this.scaleToDisplay(this.y) + "px)";
    }
    $(this.vehicle).css(properties);
    
    if (this.prevSrc !== this.src) {

        this.img.src = this.prevSrc = this.src;

        this.height = this.img.naturalHeight;
        this.width = this.img.naturalWidth;  
    }


    $(this.pivot).css({
      "transform": "rotate(" + this.rotation + "deg) scale(" + this.scalex + ", " + this.scaley + ") skew(" + this.skewx + "deg, " + this.skewy + "deg)",
    });
    if (this.img) {
        $(this.img).css({
            'height': this.scaleToDisplay(this.height) + "px",
            'width': this.scaleToDisplay(this.width) + "px"
        });
    }
}


function PoseAnimation (targetSprite, pose, args) {
    this.pose = pose;
    this.target = targetSprite;
    this.elapsed = 0;
    this.looped = args.looped || false;
    this.keyframes = args.keyframes.sort(function (kf1, kf2) {
        if (kf1.time === kf2.time) return 0;
        return (kf1.time < kf2.time) ? -1 : 1;
    });
    
    var totalTime = 0;
    this.keyframes.forEach(function (kf) {
        kf.startTime = totalTime;
        totalTime = kf.time;
    });
    
    this.duration = this.keyframes[this.keyframes.length-1].time;
    this.delay = args.delay || 0;
    this.interpolation = args.interpolation || 'none';
    this.ease = args.ease || 'linear';
    this.clamp = args.clamp || 'wrap';
    this.iterations = parseInt(args.iterations, 10) || 0;
}

PoseAnimation.prototype.isComplete = function () {
    var life = this.elapsed - this.delay;
    if (this.looped) {
        return this.iterations > 0 ? life / this.duration >= this.iterations : false;
    }
    return life >= this.duration;
}

PoseAnimation.prototype.update = function (dt) {
    this.elapsed += dt;
    
    var t = (this.elapsed - this.delay);
    if (t < 0) return;
    if (this.duration === 0) {
        t = 1;
    }
    else {
        var easingFunction = this.ease;
        t /= this.duration;
        if (this.looped) {
            t = clampingFunctions[this.clamp](t);
            if (this.isComplete()) {
                t = 1;
            }
        }
        else {
            t = Math.min(1, t);
        }
        t = Animation.prototype.easingFunctions[easingFunction](t)
        t *= this.duration;  
    }
    
    // Find current keyframe pair and update
    for (var i=this.keyframes.length-1;i>=0;i--) {
        var frame = this.keyframes[i];
        if (t <= frame.startTime) continue;

        var lastFrame = (i > 0) ? this.keyframes[i-1] : frame;
        var progress = (t - frame.startTime) / (frame.time - frame.startTime);
        progress = (t <= 0) ? 0 : Math.min(1, Math.max(0, progress));
        
        this.updateSprite(lastFrame, frame, progress, i);
        return;
    }
}

// Borrowed heavily from spniEpilogue
PoseAnimation.prototype.interpolate = function (prop, last, next, t, idx) {
    var current = this.target[prop];
    var start = last[prop];
    var end = next[prop];
    
    if (typeof start === "undefined" || isNaN(start) || typeof end === "undefined" || isNaN(end)) {
      return;
    }
    
    var mode = this.interpolation;
    this.target[prop] = interpolationModes[mode](prop, start, end, t, this.keyframes, idx);
}

PoseAnimation.prototype.updateSprite = function (fromFrame, toFrame, t, idx) {
    if (toFrame.src && t >= 1) {
        this.target.src = toFrame.src;
    }
    else if (fromFrame.src) {
        this.target.src = fromFrame.src;
    }
    
    this.interpolate("x", fromFrame, toFrame, t, idx);
    this.interpolate("y", fromFrame, toFrame, t, idx);
    this.interpolate("rotation", fromFrame, toFrame, t, idx);
    this.interpolate("scalex", fromFrame, toFrame, t, idx);
    this.interpolate("scaley", fromFrame, toFrame, t, idx);
    this.interpolate("skewx", fromFrame, toFrame, t, idx);
    this.interpolate("skewy", fromFrame, toFrame, t, idx);
    this.interpolate("alpha", fromFrame, toFrame, t, idx);
    this.target.draw();
}


function Pose(poseDef, display) {
    this.id = poseDef.id;
    this.player = poseDef.player;
    this.display = display;
    this.sprites = {};
    this.totalSprites = 0;
    this.loaded_sprites = {};
    this.animations = [];
    this.loaded = false;
    this.onLoadComplete = null;
    this.lastUpdateTS = null;
    this.active = false;
    this.baseHeight = poseDef.baseHeight || 1400;
    
    var container = document.createElement('div');
    $(container).addClass("opponent-image custom-pose").css({
        "position": "relative"
    });
    this.container = container;
    
    poseDef.sprites.forEach(function (def) {
        if (def.marker && !checkMarker(def.marker, this.player)) {
            return;
        }
        var sprite = new PoseSprite(def.id, def.src, this.onSpriteLoaded.bind(this), this, def);
        this.sprites[def.id] = sprite
        this.totalSprites++;
        
        container.appendChild(sprite.vehicle);
    }.bind(this));

    for (var id in this.sprites) {
        if (this.sprites.hasOwnProperty(id)) {
            this.sprites[id].linkParent();
        }
    }
    
    poseDef.animations.forEach(function (def) {
        if (def.marker && !checkMarker(def.marker, this.player)) {
          return;
        }
        var target = this.sprites[def.id];
        if (!target) return;
        
        var anim = new PoseAnimation(target, this, def);
        this.animations.push(anim);
    }.bind(this));
}

Pose.prototype.getHeightScaleFactor = function() {
    return this.display.height() / this.baseHeight;
}

Pose.prototype.onSpriteLoaded = function(sprite) {
    if (this.loaded_sprites[sprite.id]) { return; }
    
    this.loaded_sprites[sprite.id] = true;
    var n_loaded = Object.keys(this.loaded_sprites).length;
    
    if (n_loaded >= this.totalSprites && !this.loaded) {
        this.loaded = true;
        if (this.onLoadComplete) { return this.onLoadComplete(); }
    }
}

Pose.prototype.update = function (timestamp) {    
    if (this.lastUpdateTS === null) { this.lastUpdateTS = timestamp; }
    var dt = timestamp - this.lastUpdateTS;

    for (var id in this.sprites) {
        if (this.sprites.hasOwnProperty(id)) {
            this.sprites[id].update(dt);
        }
    }

    for (var i=0;i<this.animations.length;i++) {
        this.animations[i].update(dt);
    }
    
    this.lastUpdateTS = timestamp;
}

Pose.prototype.draw = function() {
    for (key in this.sprites) {
        this.sprites[key].draw();
    }
}

Pose.prototype.needsAnimationLoop = function () {
    if (this.animations.some(function (a) { return a.looped || !a.isComplete(); })) {
        return true;
    }

    for (var id in this.sprites) {
        if (this.sprites.hasOwnProperty(id) && this.sprites[id].elapsed < this.sprites[id].delay) {
            return true;
        }
    }

    return false;
}

function xmlToObject($xml) {
    var targetObj = {};
    $.each($xml.attributes, function (i, attr) {
      var name = attr.name.toLowerCase();
      var value = attr.value;
      targetObj[name] = value;
    });
    
    return targetObj;
}


/* Common function for parsing sprite and directive definitions. */
function parseSpriteDefinition ($xml, player) {
    var targetObj = xmlToObject($xml);
  
    //properties needing special handling
    if (targetObj.alpha) { targetObj.alpha = parseFloat(targetObj.alpha, 10); }
    targetObj.zoom = parseFloat(targetObj.zoom, 10);
    targetObj.rotation = parseFloat(targetObj.rotation, 10);
    if (targetObj.scale) {
        targetObj.scalex = targetObj.scaley = targetObj.scale;
    } else {
        targetObj.scalex = parseFloat(targetObj.scalex, 10);
        targetObj.scaley = parseFloat(targetObj.scaley, 10);
    }
    
    targetObj.skewx = parseFloat(targetObj.skewx, 10);
    targetObj.skewy = parseFloat(targetObj.skewy, 10);
    targetObj.x = parseFloat(targetObj.x, 10);
    targetObj.y = parseFloat(targetObj.y, 10);
    targetObj.delay = parseFloat(targetObj.delay) * 1000 || 0;
    
    targetObj.player = player;
    
    return targetObj;
}

function parseKeyframeDefinition($xml) {
    var targetObj = parseSpriteDefinition($xml);
    targetObj.time = parseFloat(targetObj.time) * 1000;
    if (targetObj.src) {
        targetObj.src = "opponents/" + targetObj.src;
    }
    
    return targetObj;
}

function parseDirective ($xml) {
    var targetObj = xmlToObject($xml);
    
    if (targetObj.type === 'animation') {
        // Keyframe / interpolated animation
        targetObj.keyframes = [];
        targetObj.delay = parseFloat(targetObj.delay) * 1000 || 0;
        targetObj.looped = targetObj.looped || targetObj.loop;
        $($xml).find('keyframe').each(function (i, elem) {
            targetObj.keyframes.push(parseKeyframeDefinition(elem));
        });
    } else if (targetObj.type === 'sequence') {
        // Sequential frame sequence
        targetObj.frameTime = parseFloat(targetObj.frametime);
        targetObj.delay = parseFloat(targetObj.delay) || 0;
        targetObj.frames = [];
        $($xml).find('animFrame').each(function (i, elem) {
            targetObj.frames.push(xmlToObject(elem));
        });
    }
    
    return targetObj;
}


function PoseDefinition ($xml, player) {
    this.id = $xml.attr('id').trim();
    this.baseHeight = $xml.attr('baseHeight');
    
    this.sprites = [];
    $xml.find('sprite').each(function (i, elem) {
        this.sprites.push(parseSpriteDefinition(elem, player));
    }.bind(this));
    
    this.animations = [];
    $xml.find('directive').each(function (i, elem) {
        var directive = parseDirective(elem);
        if (directive.type === 'animation') {
            this.addAnimation(directive);
        } else if (directive.type === 'sequence') {
            // Convert the sequence into a set of Animation objects.
            var curDelay = directive.delay;
            var totalTime = directive.frameTime * directive.frames.length;
            
            directive.frames.forEach(function (frame) {
                this.animations.push({
                    type: 'animation',
                    id: frame.id,
                    looped: directive.looped || directive.loop,
                    interpolation: 'none',
                    delay: curDelay * 1000,
                    keyframes: [
                        {time: 0, alpha: 100},
                        {time: directive.frameTime*1000, alpha:0},
                        {time: totalTime*1000, alpha:0}
                    ]
                });
                
                curDelay += directive.frameTime;
            }.bind(this));
        }
    }.bind(this));
    
    this.player = player;
}

//This is pretty much the same thing as spniEpilogue's addDirectiveToScene
PoseDefinition.prototype.addAnimation = function (directive) {
    if (directive.keyframes.length > 1) {
        //first split the properties into buckets of frame indices where they appear
        var propertyMap = {};
        for (var i = 0; i < directive.keyframes.length; i++) {
            var frame = directive.keyframes[i];
            for (var j = 0; j < animatedProperties.length; j++) {
                var property = animatedProperties[j];
                if (frame.hasOwnProperty(property) && !Number.isNaN(frame[property])) {
                    if (!propertyMap[property]) {
                        propertyMap[property] = [];
                    }
                    propertyMap[property].push(i);
                }
            }
        }

        //next create directives for each combination of frames
        var directives = {};
        for (var prop in propertyMap) {
            var key = propertyMap[prop].join(',');
            var workingDirective = directives[key];
            if (!workingDirective) {
                //shallow copy the directive
                workingDirective = {};
                for (var srcProp in directive) {
                    if (directive.hasOwnProperty(srcProp)) {
                        workingDirective[srcProp] = directive[srcProp];
                    }
                }
                workingDirective.keyframes = [];
                directives[key] = workingDirective;
                this.animations.push(workingDirective);
            }
            var lastStart = 0;
            for (var i = 0; i < propertyMap[prop].length; i++) {
                var srcFrame = directive.keyframes[propertyMap[prop][i]];
                var targetFrame;
                if (workingDirective.keyframes.length <= i) {
                    //shallow copy the frame minus the animatable properties
                    targetFrame = {};
                    for (var srcProp in srcFrame) {
                        if (srcFrame.hasOwnProperty(srcProp)) {
                            targetFrame[srcProp] = srcFrame[srcProp];
                        }
                    }
                    for (var j = 0; j < animatedProperties.length; j++) {
                        var property = animatedProperties[j];
                        delete targetFrame[property];
                    }

                    targetFrame.startTime = lastStart;
                    workingDirective.keyframes.push(targetFrame);
                    lastStart = srcFrame.time;
                }
                else {
                    targetFrame = workingDirective.keyframes[i];
                }
                targetFrame[prop] = srcFrame[prop];
            }
        }
    }
    else {
        this.animations.push(directive);
    }
}

PoseDefinition.prototype.getUsedImages = function(stage) {
    var baseFolder = 'opponents/';
    var imageSet = {};
    
    this.sprites.forEach(function (sprite) {
        imageSet[baseFolder+sprite.src] = true;
    });
    this.animations.forEach(function (animation) {
        animation.keyframes.forEach(function (keyframe) {
            if (keyframe.src) {
                imageSet[keyframe.src] = true;
            }
        });
    });
    
    return Object.keys(imageSet);
}


function OpponentDisplay(slot, bubbleElem, dialogueElem, simpleImageElem, imageArea, labelElem) {
    this.slot = slot;
    
    this.bubble = bubbleElem;
    this.dialogue = dialogueElem;
    this.simpleImage = simpleImageElem;
    this.imageArea = imageArea;
    this.label = labelElem;
    this.animCallbackID = undefined;
    
    window.addEventListener('resize', this.onResize.bind(this));
}

OpponentDisplay.prototype.height = function () {
    return this.imageArea.height();
}

OpponentDisplay.prototype.hideBubble = function () {
    this.dialogue.html("");
    this.bubble.hide();
}

OpponentDisplay.prototype.clearCustomPose = function () {
    this.imageArea.children('.custom-pose').remove();
    if (this.animCallbackID) {
        window.cancelAnimationFrame(this.animCallbackID);
        this.animCallbackID = undefined;
    }
}

OpponentDisplay.prototype.clearSimplePose = function () {
    this.simpleImage.hide();
}

OpponentDisplay.prototype.clearPose = function () {
    this.pose = null;
    this.clearCustomPose();
    this.clearSimplePose();
}

OpponentDisplay.prototype.drawPose = function (pose) {
    if (typeof(pose) === 'string') {
        // clear out previously shown custom poses if necessary
        if (this.pose instanceof Pose) {
            this.clearCustomPose(); 
        }
        this.simpleImage.attr('src', pose).show();
    } else if (pose instanceof Pose) {
        if (typeof(this.pose) === 'string') {
            // clear out previously shown simple poses
            this.clearSimplePose();
        } else if (this.pose instanceof Pose) {
            // Remove any previously shown custom poses too
            $(this.pose.container).remove();
        }
        
        this.imageArea.append(pose.container);
        pose.draw();
        if (pose.needsAnimationLoop()) {
            this.animCallbackID = window.requestAnimationFrame(this.loop.bind(this));
        }
    }
    
    this.pose = pose;
}

OpponentDisplay.prototype.onResize = function () {
    if (this.pose && (this.pose instanceof Pose)) {
        this.pose.draw();
    }
}

OpponentDisplay.prototype.updateText = function (player) {
    if (!player.chosenState.dialogue) {
        this.dialogue.empty();
        return;
    }

    var displayElems = parseStyleSpecifiers(player.chosenState.dialogue).map(function (comp) {
        /* {'text': 'foo', 'classes': 'cls1 cls2 cls3'} --> <span class="cls1 cls2 cls3">foo</span> */
        
        var wrapperSpan = document.createElement('span');
        wrapperSpan.innerHTML = fixupDialogue(comp.text);
        wrapperSpan.className = comp.classes;
        wrapperSpan.setAttribute('data-character', player.id);
        
        return wrapperSpan;
    });
    
    this.dialogue.empty().append(displayElems);
}

OpponentDisplay.prototype.updateImage = function(player) {
    var chosenState = player.chosenState;
    
    if (!chosenState.image) {
        this.clearPose();
    } else if (chosenState.image.startsWith('custom:')) {
        var key = chosenState.image.split(':', 2)[1];
        var poseDef = player.poses[key];
        if (poseDef) {
            this.drawPose(new Pose(poseDef, this));
        } else {
            this.clearPose();
        }
    } else {
        this.drawPose(player.folder + chosenState.image);
    }
}

OpponentDisplay.prototype.update = function(player) {
    if (!player) {
        this.hideBubble();
        this.clearPose();
        return;
    }
    
    if (!player.chosenState) {
        /* hide their dialogue bubble */
        this.hideBubble();
        return;
    }
    
    var chosenState = player.chosenState;
    
    /* update dialogue */
    this.updateText(player);
    
    /* update image */
    this.updateImage(player);    
    
    /* update label */
    this.label.html(player.label.initCap());

    /* check silence */
    if (!chosenState.dialogue) {
        this.hideBubble();
    } else {
        this.bubble.show();
        this.bubble.removeClass('arrow-down arrow-left arrow-right arrow-up').addClass('arrow-'+chosenState.direction);
        bubbleArrowOffsetRules[this.slot-1][0].style.left = chosenState.location;
        bubbleArrowOffsetRules[this.slot-1][1].style.top = chosenState.location;
    }
    
    /* Configure z-indices */
    this.imageArea.css('z-index', player.z_index);
    
    if (player.dialogue_layering === 'over') {
        this.bubble.css('z-index', player.z_index + 1);
    } else {
        this.bubble.css('z-index', player.z_index);
    }
    
    if (showDebug && !inRollback()) {
        appendRepeats(this.slot);
    }
}

OpponentDisplay.prototype.loop = function (timestamp) {
    if (!this.pose || !(this.pose instanceof Pose)) return;
    this.pose.update(timestamp);
    if (this.pose.needsAnimationLoop()) {
        this.animCallbackID = window.requestAnimationFrame(this.loop.bind(this));
    } else {
        this.animCallbackID = undefined;
    }
}


function GameScreenDisplay (slot) {
    OpponentDisplay.call(
        this,
        slot,
        $('#game-bubble-'+slot),
        $('#game-dialogue-'+slot),
        $('#game-image-'+slot),
        $('#game-image-area-'+slot),
        $('#game-name-label-'+slot)
    );
    
    this.opponentArea = $('#game-opponent-area-'+slot);
    this.collectibleIndicator = $('#collectible-button-'+slot);
    
    this.collectibleIndicator.click(this.onCollectibleIndicatorClick.bind(this));
    this.devModeController = new DevModeDialogueBox(this.bubble);
}
GameScreenDisplay.prototype = Object.create(OpponentDisplay.prototype);
GameScreenDisplay.prototype.constructor = GameScreenDisplay;

GameScreenDisplay.prototype.reset = function (player) {
    clearHand(this.slot);
    
    /* Keep a reference to the player
     * (for handling collectible indicator clicks)
     */
    this.player = player;
    this.collectibleIndicator.hide();
    
    if (player) {
        this.opponentArea.show();
        this.imageArea.css({
            'height': player.scale + '%',
            'top': (100 - player.scale) + '%'
        }).show();
        this.label.removeClass("current loser tied");
    } else {
        this.opponentArea.hide();
        this.bubble.hide();
    }
}

GameScreenDisplay.prototype.update = function (player) {
    this.player = player;
    OpponentDisplay.prototype.update.call(this, player);
    
    if (devModeActive) this.devModeController.update(player);
    
    if (player && player.pendingCollectiblePopup) {
        this.collectibleIndicator.show();
    } else {
        this.collectibleIndicator.hide();
    }
}

GameScreenDisplay.prototype.onCollectibleIndicatorClick = function (ev) {
    if (!this.player || !this.player.pendingCollectiblePopup) return;
    
    var collectible = this.player.pendingCollectiblePopup;
    
    this.player.pendingCollectiblePopup = null;
    this.collectibleIndicator.hide();
    collectible.displayInfoModal();
}

/* Wraps logic for handling the Main Select screen displays. */
function MainSelectScreenDisplay (slot) {
    OpponentDisplay.call(this, 
        slot,
        $('#select-bubble-'+slot),
        $('#select-dialogue-'+slot),
        $('#select-image-'+slot),
        $('#select-image-area-'+slot),
        $('#select-name-label-'+slot)
    );
    
    this.selectButton = $("#select-slot-button-"+slot);
}

MainSelectScreenDisplay.prototype = Object.create(OpponentDisplay.prototype);
MainSelectScreenDisplay.prototype.constructor = MainSelectScreenDisplay;

MainSelectScreenDisplay.prototype.update = function (player) {
    if (!player) {
        this.hideBubble();
        this.clearPose();
        this.label.html("Opponent " + this.slot);
        
        /* change the button */
        this.selectButton.html("Select Opponent");
        this.selectButton.removeClass("smooth-button-red");
        this.selectButton.addClass("smooth-button-green");
        return;
    }
    
    if (!player.isLoaded()) {
        this.hideBubble();
        this.clearPose();
        
        this.label.html(player.label.initCap());
        this.selectButton.attr('disabled', true).html('Loading...');
    } else {
        OpponentDisplay.prototype.update.call(this, player);
        
        this.selectButton.attr('disabled', false).html("Remove Opponent");
        this.selectButton.removeClass("smooth-button-green");
        this.selectButton.addClass("smooth-button-red");
        
        if (!(this.pose instanceof Pose)) {
            this.simpleImage.one('load', function() {
                this.bubble.show();
                this.simpleImage.css('height', player.scale + '%').show();
            }.bind(this));
        } else {
            this.pose.onLoadComplete = function () {
                this.bubble.show();
                this.imageArea.css({
                    'height': player.scale + '%',
                    'top': (100 - player.scale) + '%'
                }).show();
            }.bind(this);
        }
    }
}

function createElementWithClass (elemType, className) {
    var elem = document.createElement(elemType);
    elem.className = className;
    
    return elem;
}


function OpponentSelectionCard (opponent) {
    this.opponent = opponent;
    
    this.mainElem = createElementWithClass('div', 'selection-card');
    
    var clipElem = this.mainElem.appendChild(createElementWithClass('div', 'selection-card-image-clip'));
    this.imageArea = clipElem.appendChild(createElementWithClass('div', 'selection-card-image-area'));
    this.simpleImage = $(this.imageArea.appendChild(createElementWithClass('img', 'selection-card-image-simple')));
    
    this.imageArea = $(this.imageArea);
    
    this.epilogueBadge = $(this.mainElem.appendChild(createElementWithClass('img', 'badge-icon')));
    
    var sidebarElem = this.mainElem.appendChild(createElementWithClass('div', 'selection-card-sidebar'));
    this.layerIcon = $(sidebarElem.appendChild(createElementWithClass('img', 'layer-icon')));
    this.genderIcon = $(sidebarElem.appendChild(createElementWithClass('img', 'gender-icon')));
    this.statusIcon = $(sidebarElem.appendChild(createElementWithClass('img', 'status-icon')));
    
    $(this.epilogueBadge).attr('src', "img/epilogue_icon.png");
    
    var footerElem = this.mainElem.appendChild(createElementWithClass('div', 'selection-card-footer'));
    this.label = $(footerElem.appendChild(createElementWithClass('div', 'selection-card-label selection-card-name')));
    this.source = $(footerElem.appendChild(createElementWithClass('div', 'selection-card-label selection-card-source')));
    
    this.update();
}

OpponentSelectionCard.prototype = Object.create(OpponentDisplay.prototype);
OpponentSelectionCard.prototype.constructor = OpponentSelectionCard;

OpponentSelectionCard.prototype.update = function () {    
    if (EPILOGUE_BADGES_ENABLED && this.opponent.ending) {
        this.epilogueBadge.show();
    } else {
        this.epilogueBadge.hide();
    }

    if (this.opponent.status) {
        var status_icon_img = 'img/testing-badge.png';
        var status_tooltip = TESTING_STATUS_TOOLTIP;
        
        if (this.opponent.status === 'offline') {
            status_icon_img = 'img/offline-badge.png';
            status_tooltip = OFFLINE_STATUS_TOOLTIP;
        } else if (this.opponent.status === 'incomplete') {
            status_icon_img = 'img/incomplete-badge.png';
            status_tooltip = INCOMPLETE_STATUS_TOOLTIP;
        }
    
        this.statusIcon.attr({
            'src': status_icon_img,
            'title': status_tooltip,
            'data-original-title': status_tooltip,
        }).show().tooltip({
            'placement': 'left'
        });
    } else {
        this.statusIcon.removeAttr('title').removeAttr('data-original-title').hide();
    }

    this.layerIcon.show().attr("src", "img/layers" + this.opponent.layers + ".png");
    this.genderIcon.show().attr("src", this.opponent.gender === 'male' ? 'img/male.png' : 'img/female.png');
    this.simpleImage.attr('src', this.opponent.selection_image).css('height', this.opponent.scale + '%').show();
    
    this.label.text(this.opponent.label);
    this.source.text(this.opponent.source);
    
    this.mainElem.addEventListener('click', this.handleClick.bind(this));
}

OpponentSelectionCard.prototype.clear = function () {}

OpponentSelectionCard.prototype.handleClick = function (ev) {
    individualDetailDisplay.update(this.opponent);
}

OpponentDetailsDisplay = function () {
    this.displayContainer = $("#individual-select-screen .opponent-details-panel");
    
    this.mainView = $('#individual-select-screen .opponent-details-basic');
    
    this.epiloguesView = $('#individual-select-screen .opponent-details-epilogues');
    this.epiloguesContainer = $('#individual-select-screen .opponent-epilogues-container');
    
    this.collectiblesView = $('#individual-select-screen .opponent-details-collectibles');
    this.collectiblesContainer = $('#individual-select-screen .opponent-collectibles-container');
    
    this.epiloguesField = $('#individual-select-screen .opponent-epilogues-field');
    this.collectiblesField = $('#individual-select-screen .opponent-collectibles-field');
    
    this.nameLabel = $("#individual-select-screen .opponent-full-name");
    this.sourceLabel = $("#individual-select-screen .opponent-source");
    this.writerLabel = $("#individual-select-screen .opponent-writer");
    this.artistLabel = $("#individual-select-screen .opponent-artist");
    this.descriptionLabel = $("#individual-select-screen .opponent-details-description");
    this.linecountLabel = $("#individual-select-screen .opponent-linecount");
    this.posecountLabel = $("#individual-select-screen .opponent-posecount");
    this.costumeSelector = $("#individual-select-screen .opponent-costume-select");
    this.simpleImage = $("#individual-select-screen .opponent-details-simple-image");
    this.imageArea = $("#individual-select-screen .opponent-details-image-area");
    
    this.selectButton = $('#individual-select-screen .select-button');
    this.epiloguesNavButton = $('#individual-select-screen .opponent-epilogues');
    this.collectiblesNavButton = $('#individual-select-screen .opponent-collectibles');
    
    this.showMoreButton = $('#individual-select-screen .show-more-button');
    
    $('#individual-select-screen .opponent-nav-button').click(this.handlePanelNavigation.bind(this));
    
    this.costumeSelector.change(this.handleCostumeChange.bind(this));
    this.selectButton.click(this.handleSelected.bind(this));
    this.showMoreButton.click(function () {
        this.mainView.toggleClass('show-more');
    }.bind(this));
    
    this.epiloguesView.hide();
    this.collectiblesView.hide();
    
    var query = window.matchMedia('(min-aspect-ratio: 4/3)');
    if (query.matches) {
        this.mainView.addClass('show-more');
    } else {
        this.mainView.removeClass('show-more');
    }
}

OpponentDetailsDisplay.prototype = Object.create(OpponentDisplay.prototype);
OpponentDetailsDisplay.prototype.constructor = OpponentDetailsDisplay;

OpponentDetailsDisplay.prototype.handleSelected = function (ev) {
    if (!this.opponent) return;
    
    if (SENTRY_INITIALIZED) {
        Sentry.addBreadcrumb({
            category: 'select',
            message: 'Loading individual opponent ' + this.opponent.id,
            level: 'info'
        });
        Sentry.setTag("screen", "select-main");
    }

    players[selectedSlot] = this.opponent;
	players[selectedSlot].loadBehaviour(selectedSlot, true);
    updateSelectionVisuals();
	screenTransition($individualSelectScreen, $selectScreen);
    
    this.clear();
}

OpponentDetailsDisplay.prototype.handlePanelNavigation = function (ev) {
    var targetPanel = $(ev.target).attr('data-target');
    
    $('#individual-select-screen .opponent-details-view').hide();
    
    if (targetPanel === 'epilogues') {
        this.updateEpiloguesView();
        this.epiloguesView.show();
    } else if (targetPanel === 'collectibles') {
        this.updateCollectiblesView();
        this.collectiblesView.show();
    } else {
        this.mainView.show();
    }
}

OpponentDetailsDisplay.prototype.handleCostumeChange = function () {
    if (!this.opponent) return;
	var selectedCostume = this.costumeSelector.val();
	
	var costumeDesc = undefined;
	if (selectedCostume.length > 0) {
		for (let i=0;i<this.opponent.alternate_costumes.length;i++) {
			if (this.opponent.alternate_costumes[i].folder === selectedCostume) {
				costumeDesc = this.opponent.alternate_costumes[i];
				break;
			}
		}
	}
	
    this.opponent.selectAlternateCostume(costumeDesc);
    this.simpleImage.attr('src', this.opponent.selection_image);
}

OpponentDetailsDisplay.prototype.clear = function () {
    this.opponent = null;
    this.nameLabel.empty();
    this.sourceLabel.empty();
    this.writerLabel.empty();
    this.artistLabel.empty();
    this.descriptionLabel.empty();
    
    this.simpleImage.attr('src', null);
    this.selectButton.prop('disabled', true);
    this.epiloguesField.removeClass('has-epilogues');
    this.collectiblesField.removeClass('has-collectibles');
    this.costumeSelector.hide();
    
    this.displayContainer.hide();
}

OpponentDetailsDisplay.prototype.createEpilogueCard = function (title, gender, unlockHint) {
    // Add the opponent-epilogue-* classes for future extensibility and also
    // to minimize disruptions with caching
    var container = createElementWithClass('div', 'bordered opponent-subview-card opponent-epilogue-card');
    
    var titleElem = container.appendChild(createElementWithClass('div', 'opponent-subview-title opponent-epilogue-title'));
    $(titleElem).html(title);
    
    var genderElem = container.appendChild(createElementWithClass('div', 'bordered left-cap opponent-subview-row opponent-epilogue-row opponent-epilogue-gender'));
    var genderLabel = genderElem.appendChild(createElementWithClass('div', 'left-cap opponent-subview-label opponent-epilogue-label'));
    var genderValue = genderElem.appendChild(createElementWithClass('div', 'opponent-subview-value opponent-epilogue-value'));
    
    $(genderValue).html(gender);
    $(genderLabel).text("For");
    
    if (unlockHint) {
        var unlockHintElem = container.appendChild(createElementWithClass('div', 'bordered left-cap opponent-subview-row opponent-epilogue-row opponent-epilogue-unlock'));
        var unlockHintLabel = unlockHintElem.appendChild(createElementWithClass('div', 'left-cap opponent-subview-label opponent-epilogue-label'));
        var unlockHintValue = unlockHintElem.appendChild(createElementWithClass('div', 'opponent-subview-value opponent-epilogue-value'));
        $(unlockHintLabel).text("To Unlock");
        $(unlockHintValue).html(unlockHint);
    }
    
    return container;
}

function isEquivalentEpilogue(e1, e2) {
    if (e1.text() !== e2.text()) return false;
    
    return EPILOGUE_CONDITIONAL_ATTRIBUTES.every(function (condAttr) {
        return e1.attr(condAttr) == e2.attr(condAttr);
    });
}

OpponentDetailsDisplay.prototype.updateEpiloguesView = function () {
    if (!this.opponent.ending) return;

    // Group together any epilogues with a shared name and conditional attributes (but with different gender attributes).
    var groups = [];

    this.opponent.endings.each(function (idx, elem) {
        var $elem = $(elem);
        var title = $elem.text();
        
        if(!groups.some(function (group) {
            if (group.every(isEquivalentEpilogue.bind(null, $elem))) {
                // This group contains all equivalent epilogues to the current one, add the current epilogue 
                group.push(elem);
                return true;
            }
            return false;
        })) {
            // Add the current element as a new group
            groups.push([$elem]);
        }
    });
    
    var cards = groups.map(function (group) {
        var condGender = group[0].attr('gender');
        var genderText = '';
        
        if (group.length > 1) {
            genderText = 'All Genders';
        } else if (condGender === 'male') {
            genderText = 'Males';
        } else if (condGender === 'female') {
            genderText = 'Females';
        } else {
            genderText = 'All Genders';
        }
        
        return this.createEpilogueCard(
            group[0].text(), genderText, group[0].attr('hint')
        );
    }.bind(this));
    
    this.epiloguesContainer.empty().append(cards);
};

OpponentDetailsDisplay.prototype.createCollectibleCard = function (collectible) {
    var container = createElementWithClass('div', 'bordered opponent-subview-card');
    
    var titleElem = container.appendChild(createElementWithClass('div', 'opponent-subview-title'));
    var subtitleElem = container.appendChild(createElementWithClass('div', 'opponent-subview-subtitle'));
    
    if (!collectible.detailsHidden || collectible.isUnlocked()) {
        $(titleElem).html(collectible.title);
        $(subtitleElem).html(collectible.subtitle);
    } else {
        $(titleElem).html("[Locked]");
        $(subtitleElem).html("");
    }
    

    if (collectible.unlock_hint) {
        var unlockHintElem = container.appendChild(createElementWithClass('div', 'bordered left-cap opponent-subview-row opponent-collectible-unlock'));
        var unlockHintLabel = unlockHintElem.appendChild(createElementWithClass('div', 'left-cap opponent-subview-label'));
        var unlockHintValue = unlockHintElem.appendChild(createElementWithClass('div', 'opponent-subview-value'));
        $(unlockHintLabel).text("To Unlock");
        $(unlockHintValue).html(collectible.unlock_hint);
    }
    
    if (collectible.counter) {
        var counterElem = container.appendChild(createElementWithClass('div', 'bordered left-cap opponent-subview-row opponent-collectible-counter'));
        var counterLabel = counterElem.appendChild(createElementWithClass('div', 'left-cap opponent-subview-label'));
        var counterValue = counterElem.appendChild(createElementWithClass('div', 'opponent-subview-value'));
        
        var curCounter = collectible.getCounter()
        $(counterLabel).text("Progress");
        $(counterValue).html(curCounter + ' / ' + collectible.counter);
    }
    
    return container;
}

OpponentDetailsDisplay.prototype.updateCollectiblesView = function () {
    if (!COLLECTIBLES_ENABLED || !this.opponent.has_collectibles || !this.opponent.collectibles) return;
    
    var cards = this.opponent.collectibles.map(function (collectible) {
        if (collectible.hidden && !collectible.isUnlocked()) {
            return null;
        } else {
            return this.createCollectibleCard(collectible);
        }
    }.bind(this));
    this.collectiblesContainer.empty().append(cards);
}

OpponentDetailsDisplay.prototype.update = function (opponent) {
    if (this.opponent === opponent) {
        // Interpret double-clicks as selection events.
        return this.handleSelected();
    }
    
    
    this.opponent = opponent;
    
    this.displayContainer.show();
    this.nameLabel.html(opponent.first + " " + opponent.last);
    this.sourceLabel.html(opponent.source);
    this.writerLabel.html(opponent.writer);
    this.artistLabel.html(opponent.artist);
    this.descriptionLabel.html(opponent.description);

    this.simpleImage.attr('src', opponent.selection_image).css('height', opponent.scale + '%').show();
    
    this.selectButton.prop('disabled', false);
    
    var query = window.matchMedia('(min-aspect-ratio: 4/3)');
    if (query.matches) {
        this.mainView.addClass('show-more');
    } else {
        this.mainView.removeClass('show-more');
    }
    
    if (!opponent.ending) {
        this.epiloguesField.removeClass('has-epilogues');
    } else {
        this.epiloguesField.addClass('has-epilogues');
        
        var endingGenders = {
            male: false,
            female: false
        };
        
        var hasConditionalEnding = false;
        var totalEndings = 0;
        var unlockedEndings = 0;
        
        opponent.endings.each(function (idx, elem) {
            var $elem = $(elem);
            
            totalEndings += 1;
            if (save.hasEnding(opponent.id, $elem.text())) {
                unlockedEndings += 1;
            }
            
            if(EPILOGUE_CONDITIONAL_ATTRIBUTES.some(function (attr) { return !!$elem.attr(attr); })) {
                hasConditionalEnding = true;
            }
            
            var gender = $elem.attr('gender');
            
            if (gender === 'male') {
                endingGenders.male = true;
            } else if (gender === 'female') {
                endingGenders.female = true;
            } else {
                endingGenders.male = true;
                endingGenders.female = true;                
            }
        });
        
        var epilogueAvailable = false;
        if (endingGenders.male && endingGenders.female) {
            epilogueAvailable = true;
        } else if (endingGenders.male) {
            epilogueAvailable = (humanPlayer.gender === 'male');
        } else if (endingGenders.female) {
            epilogueAvailable = (humanPlayer.gender === 'female');
        }
        
        if (epilogueAvailable) {
            this.epiloguesNavButton
                .text((hasConditionalEnding ? 'Conditionally ' : '') + 'Available' + ' (' + unlockedEndings +'/' + totalEndings + ' seen)')
                .removeClass('smooth-button-red')
                .addClass('smooth-button-blue');
        } else {
            this.epiloguesNavButton
                .text((endingGenders.male ? 'Males' : 'Females') + ' Only' + ' (' + unlockedEndings +'/' + totalEndings + ' seen)')
                .removeClass('smooth-button-blue')
                .addClass('smooth-button-red');
        }
    }

    if (COLLECTIBLES_ENABLED && opponent.has_collectibles) {        
        var updateCollectiblesBtn = function () {
            var counts = opponent.collectibles.reduce(function (acc, collectible) {
                acc.total += 1;
                if (collectible.isUnlocked()) acc.unlocked += 1;
                
                return acc;
            }, {unlocked:0, total:0});
            
            this.collectiblesNavButton.text("Available ("+counts.unlocked+"/"+counts.total+" unlocked)");
        }.bind(this);
        
        this.collectiblesField.addClass('has-collectibles');
        
        this.collectiblesNavButton
            .text("Available")
            .addClass('smooth-button-blue')
            .prop('disabled', false);
            
        if (!opponent.collectibles) {
            opponent.loadCollectibles(updateCollectiblesBtn);
        } else {
            updateCollectiblesBtn();
        }
    } else {
        this.collectiblesField.removeClass('has-collectibles');
    }

    if (ALT_COSTUMES_ENABLED && opponent.alternate_costumes.length > 0) {
        this.costumeSelector.empty().append($('<option>', {val: '', text: 'Default Skin'})).prop('disabled', false);
        
        opponent.alternate_costumes.forEach(function (alt) {
            this.costumeSelector.append($('<option>', {
                val: alt.folder,
                text: alt.label
            }));
        }.bind(this));
        
        /* Force-set and lock the selector if FORCE_ALT_COSTUME is set */
        opponent.alternate_costumes.some(function (alt) {
            if (alt.set === FORCE_ALT_COSTUME) {
                this.costumeSelector.val(alt.folder).prop('disabled', true);
                return true;
            }
        }.bind(this));
        
        this.costumeSelector.show();
    } else {
        this.costumeSelector.hide();
    }
    
    if (opponent.uniqueLineCount === undefined || opponent.posesImageCount === undefined) {
        // retrieve line and image counts
        if (DEBUG) {
            console.log("[LineImageCount] Fetching counts for " + opponent.label);
        }

        this.linecountLabel.text("Loading...");
        this.posecountLabel.text("Loading...");

        fetchCompressedURL(opponent.folder + 'behaviour.xml').then(countLinesImages).then(function(response) {
            opponent.uniqueLineCount = response.numUniqueLines;
            opponent.posesImageCount = response.numPoses;

            // show line and image counts
            if (DEBUG) {
                console.log("[LineImageCount] Loaded " + opponent.label + " from behaviour: " +
                  opponent.uniqueLineCount + " lines, " + opponent.posesImageCount + " images");
            }
            
            this.linecountLabel.text(opponent.uniqueLineCount);
            this.posecountLabel.text(opponent.posesImageCount);
        }.bind(this));
    }
    else {
        // this character's counts were previously loaded
        if (DEBUG) {
            console.log("[LineImageCount] Loaded previous count for " + opponent.label + ": " +
              opponent.uniqueLineCount + " lines, " + opponent.posesImageCount + " images)");
        }
        this.linecountLabel.text(opponent.uniqueLineCount);
        this.posecountLabel.text(opponent.posesImageCount);
    }
    
    this.epiloguesView.hide();
    this.collectiblesView.hide();
    this.mainView.show();
}