var vectorUtils = {

distance: function(a, b){
    return Math.sqrt((b.x - a.x) * (b.x - a.x) + (b.y - a.y) * (b.y - a.y));
},
sqrDistance: function(a, b){
    return (b.x - a.x) * (b.x - a.x) + (b.y - a.y) * (b.y - a.y);
}

}

var drawingUtils = {

drawLine: function(ctx, from, to){
    ctx.beginPath();
    ctx.moveTo(from.x, from.y);
    ctx.lineTo(to.x, to.y);
    ctx.stroke();
},
renderArmature: function(context, armature){
    this.drawLine(context, armature.root, armature.bones[0].endpoint);
    for(var i = 0; i < armature.bones.length - 1; i++){
        this.drawLine(context, armature.bones[i].endpoint, armature.bones[i + 1].endpoint)
    }
}

}

class Armature{

constructor(bones, joints, root){
    this.bones = bones;
    this.joints = joints;
    this.root = root;
    this.recalculateEndpoints();
}
endPoint(){
    return this.bones[this.bones.length - 1].endpoint;
}
recalculateEndpoints(){
    var lastPosition = {x: this.root.x, y: this.root.y};
    var lastAngle = 0;
    for(var i = 0; i < this.bones.length; i++){
        this.bones[i].endpoint.x = lastPosition.x + 
            this.bones[i].length * Math.cos(lastAngle + this.joints[i].angle);
        this.bones[i].endpoint.y = lastPosition.y + 
            this.bones[i].length * Math.sin(lastAngle + this.joints[i].angle);
        lastAngle += this.joints[i].angle;
        lastPosition.x = this.bones[i].endpoint.x;
        lastPosition.y = this.bones[i].endpoint.y;
    }
}
applyInverseKinematics(targetPosition){
    var iterations = 0;
    while(vectorUtils.sqrDistance(this.bones[this.bones.length - 1].endpoint, targetPosition) > 0 && iterations < 10){
        this.approachTarget(targetPosition);
        iterations++;
    }
}
approachTarget(targetPosition){
    for(var i = this.joints.length - 1; i >= 0; i--){
        // j - e
        var jointToEndpoint = {x: 0, y: 0};
        if(i > 0){
            jointToEndpoint.x = this.bones[this.bones.length - 1].endpoint.x - this.bones[i - 1].endpoint.x;
            jointToEndpoint.y = this.bones[this.bones.length - 1].endpoint.y - this.bones[i - 1].endpoint.y;
        }
        else{
            jointToEndpoint.x = this.bones[this.bones.length - 1].endpoint.x - this.root.x;
            jointToEndpoint.y = this.bones[this.bones.length - 1].endpoint.y - this.root.y;
        }
        // j - t
        var jointToTarget = {x: 0, y: 0};
        if(i > 0){
            jointToTarget.x = targetPosition.x - this.bones[i - 1].endpoint.x;
            jointToTarget.y = targetPosition.y - this.bones[i - 1].endpoint.y;
        }
        else{
            jointToTarget.x = targetPosition.x - this.root.x;
            jointToTarget.y = targetPosition.y - this.root.y;
        }
        // (j - e) * (j - t)
        var dotProduct = jointToEndpoint.x * jointToTarget.x +
            jointToEndpoint.y * jointToTarget.y;
        // arccos((j - e) * (j - t))
        //this.joints[i + 1].angle = Math.acos(dotProduct);
        var angle1 = Math.atan2(jointToTarget.y, jointToTarget.x);
        var angle2 = Math.atan2(jointToEndpoint.y, jointToEndpoint.x);
        this.joints[i].angle += -angle2 + angle1;
        this.recalculateEndpoints();
    }
}

}

class Bone{

constructor(length){
    this.length = length;
    this.endpoint = {x: 0, y: 0}; 
}

}

class Joint{

constructor(minAngle, maxAngle){
    this._angle = 0;
    this.minAngle = minAngle;
    this.maxAngle = maxAngle;
}
set angle(newAngle){
    while(newAngle > Math.PI){
        newAngle -= 2*Math.PI;
    }
    while(newAngle < -Math.PI){
         newAngle += 2*Math.PI;
    }
    if(newAngle > this.maxAngle){
        this._angle = this.maxAngle
    }
    else if(newAngle < this.minAngle){
        this._angle = this.minAngle;
    }
    else{
        this._angle = newAngle;
    }
}
get angle() {
    return this._angle;
}

}

var drawingUtils = {

drawLine: function(ctx, from, to){
    ctx.lineWidth = 5;
    ctx.beginPath();
    ctx.moveTo(from.x, from.y);
    ctx.lineTo(to.x, to.y);
    ctx.stroke();
},
renderArmature: function(context, armature){
    this.drawLine(context, armature.root, armature.bones[0].endpoint);
    for(var i = 0; i < armature.bones.length - 1; i++){
        this.drawLine(context, armature.bones[i].endpoint, armature.bones[i + 1].endpoint)
    }
}

}

var forwardKinematicsDisplay = {

canvas: document.getElementById("forward-kinematics-display-canvas"),
context: document.getElementById("forward-kinematics-display-canvas").getContext('2d'),
element: document.getElementById("forward-kinematics-display"),
angleSliders: [
                document.getElementById("forward-kinematics-display-joint-slider-1"),
                document.getElementById("forward-kinematics-display-joint-slider-2"),
                document.getElementById("forward-kinematics-display-joint-slider-3")
            ],
armature: null,
loop: function (){
    this.armature.joints[0].angle = (this.angleSliders[0].value / 100 * Math.PI / 2) + Math.PI *2;
    for(var i = 1; i < this.angleSliders.length; i++){
        this.armature.joints[i].angle = this.angleSliders[i].value / 100 * 2 * Math.PI;
    }
    this.context.save(); // save the default state
    this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
    this.context.fillStyle = "#6bff6b";
    if(vectorUtils.distance({x:180, y:180}, this.armature.endPoint()) < 10){
        this.context.fillRect(0, 0, this.canvas.width, this.canvas.height);
    }
    // draw the circle
    this.context.beginPath();
    this.context.arc(180, 180, 10, 0, 2 * Math.PI, false);
    this.context.fill();
    this.context.stroke();
    this.armature.recalculateEndpoints();
    drawingUtils.renderArmature(this.context, this.armature);
    this.context.restore();
},
initialize: function(){
    var bones = [new Bone(200), new Bone(100), new Bone(50)];
    var joints = [new Joint(-Math.PI, Math.PI), new Joint(-Math.PI, Math.PI), new Joint(-Math.PI, Math.PI)];
    var root = {x: 0, y: 0};
    this.armature = new Armature(bones, joints, root);
},
deinitialize: function(){}

}

var inverseKinematicsDisplay = {

canvas: document.getElementById("inverse-kinematics-display-canvas"),
context: document.getElementById("inverse-kinematics-display-canvas").getContext('2d'),
element: document.getElementById("inverse-kinematics-display"),
armature: null,
boneSliders: [
            document.getElementById("inverse-kinematics-bone-length-slider-1"),
            document.getElementById("inverse-kinematics-bone-length-slider-2"),
            document.getElementById("inverse-kinematics-bone-length-slider-3"),
            document.getElementById("inverse-kinematics-bone-length-slider-4")
        ],
mousePosition: {x: 100, y: 100},
updateMousePosition: function(mouseEvent){
    var display = inverseKinematicsDisplay;
    var rect = display.canvas.getBoundingClientRect();
    display.mousePosition.x = mouseEvent.clientX - rect.left;
    display.mousePosition.y = mouseEvent.clientY - rect.top;
},
loop: function (){
    for(var i = 0; i < this.boneSliders.length; i++){
        this.armature.bones[i].length = this.boneSliders[i].value * 2;
    }
    this.armature.recalculateEndpoints();
    this.armature.applyInverseKinematics(this.mousePosition);
    this.context.save(); // save the default state
    this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
    drawingUtils.renderArmature(this.context, this.armature);
    this.context.restore();
},
initialize: function(){
    var bones = [new Bone(200), new Bone(100), new Bone(50), new Bone(50)];
    var joints = [new Joint(-4*Math.PI, 4*Math.PI), new Joint(-4*Math.PI, 4*Math.PI), new Joint(-4*Math.PI, 4*Math.PI), new Joint(-4*Math.PI, 4*Math.PI)];
    var root = {x: 0, y: 0};
    this.armature = new Armature(bones, joints, root);
    this.canvas.addEventListener('mousemove', this.updateMousePosition);
},
deinitialize: function(){
    this.canvas.removeEventListener('mousemove', this.updateMousePosition);
}

} var inverseKinematicsAngleDisplay = {

canvas: document.getElementById("inverse-kinematics-angle-display-canvas"),
context: document.getElementById("inverse-kinematics-angle-display-canvas").getContext('2d'),
element: document.getElementById("inverse-kinematics-angle-display"),
armature: null,
mousePosition: {x: 100, y: 100},
updateMousePosition: function(mouseEvent){
    var display = inverseKinematicsAngleDisplay;
    var rect = display.canvas.getBoundingClientRect();
    display.mousePosition.x = mouseEvent.clientX - rect.left;
    display.mousePosition.y = mouseEvent.clientY - rect.top;
},
loop: function (){
    this.armature.applyInverseKinematics(this.mousePosition);
    this.context.save(); // save the default state
    this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
    drawingUtils.renderArmature(this.context, this.armature);
    this.context.restore();
},
initialize: function(){
    var bones = [new Bone(50), new Bone(50), new Bone(50), new Bone(50), new Bone(50), new Bone(50), new Bone(50), new Bone(50), new Bone(50)];
    var joints = [new Joint(-Math.PI, Math.PI), new Joint(-Math.PI/4, Math.PI/4), new Joint(-Math.PI/4, Math.PI/4), new Joint(-Math.PI/4, Math.PI/4), new Joint(-Math.PI/4, Math.PI/4), new Joint(-Math.PI/4, Math.PI/4), new Joint(-Math.PI/4, Math.PI/4), new Joint(-Math.PI/4, Math.PI/4), new Joint(-Math.PI/4, Math.PI/4)];
    var root = {x: 0, y: 300};
    this.armature = new Armature(bones, joints, root);
    this.canvas.addEventListener('mousemove', this.updateMousePosition);
},
deinitialize: function(){
    this.canvas.removeEventListener('mousemove', this.updateMousePosition);
}

} codeDisplays.push(forwardKinematicsDisplay); codeDisplays.push(inverseKinematicsDisplay); codeDisplays.push(inverseKinematicsAngleDisplay);