$(function() {

  var NUM_GENES = 18;
  var NUM_ENEMIES = 20;
  var MUTATE_PROB = 0.1;
  var MUTATE_AMOUNT = 1.0;
  var MATING_RANGE = 80;
  var MATING_TIME = 200;
  var NUM_CHILDREN = 3;

  var body = $("body");
  var paused = false;
  var playing = false;

  var errors = false;
  function showError(err) {
    $("#errors").show().append("<li>" + err + "</li>");
    if (!errors) {
      $("#errors").append("<li>This game has been tested in recent versions of <a href='http://getfirefox.com'>Firefox</a>, <a href='http://www.google.com/chrome'>Chrome</a> and <a href='http://www.apple.com/safari'>Safari</a>.</li>");
    }
    errors = true;
  }

  // SOUND INIT ////////////////////////////////////////////////////////////////

  var soundEnabled = true;

  if (Audio === undefined) {
    showError("Your browser does not support HTML5 audio. This game will be silent.");
  }

  function Sound(id) {
    var src = null;
    var audio = null;

    function createAudio() {
      audio = new Audio(src);
      audio.preload = "auto";
      audio.volume = 0.3;
    }

    $.ajax({
      url: id + ".wav.base64",
      dataType: "text",
      success: function(data) {
        src = "data:audio/wav;base64," + data;
        createAudio();
      },
      error: function(jqXHR, textStatus, errorThrown) {
        console.error("Failed to load " + jqXHR.url + ": " + textStatus + " " + errorThrown);
      },
    });

    this.play = function() {
      if (!soundEnabled) return;
      if (!audio) return;
      audio.play();
      createAudio();
    }
  }

  var sounds = {};

  function loadSounds(ids) {
    for (var i = 0; i < ids.length; ++i) {
      var id = ids[i];
      sounds[id] = new Sound(id);
    }
  }

  loadSounds(["shoot", "kill", "bunker", "bomb", "bullet_bomb", "die", "mate", "offspring", "blip"]);

  // MUSIC /////////////////////////////////////////////////////////////////////

  var musicEnabled = true;
  var music = document.getElementById("music");
  music.onended = function() {
    // loop doesn't always work...
    music.currentTime = 0;
    music.play();
  }

  // GRAPHICS INIT /////////////////////////////////////////////////////////////

  try {
    var gla = new Gladder({
      canvas: "canvas",
      // debug: true,
      errorCallback: function(err) { console.error(err); },
      // callCallback: function(msg) { console.info(msg); },
    });
  } catch (e) {
    showError("Your browser does not support WebGL.");
    return;
  }

  gla.enable(gla.Capability.BLEND);
  gla.additiveBlendFunc();

  var program = new gla.Program({
    vertexShader: "vertex-shader",
    fragmentShader: "fragment-shader",
    uniforms: { transform: "mat4", alpha: "float" },
    attributes: { position: "vec2", color: "vec3" },
  });

  var program3d = new gla.Program({
    vertexShader: "vertex-shader-3d",
    fragmentShader: "fragment-shader",
    uniforms: { transform: "mat4", alpha: "float" },
    attributes: { position: "vec3", color: "vec3" },
  });

  var projectionMatrix = mat4.create();
  mat4.ortho(0, gla.canvas.width, 0, gla.canvas.height, -1, 1, projectionMatrix);
  mat4.translate(projectionMatrix, [0.5, 0.5, 0.0]); // whole numbers are pixel centres

  var backgroundTransform = mat4.create();
  var NEAR = 20;
  var FAR = NEAR + 5;
  mat4.frustum(-10, 10, -10, 10, NEAR, FAR, backgroundTransform);

  var modelMatrix = mat4.create();
  var transform = mat4.create();

  function makeBackgroundData() {
    var data = [];
    var i = 0;
    function p(x, y, z) {
      data[i++] = x;
      data[i++] = y;
      data[i++] = z;
      var c = 0.1 - 0.03 * (y + 10) / 10;
      data[i++] = c;
      data[i++] = c;
      data[i++] = c;
    }
    for (var x = -10; x <= 10; ++x) {
      p(x, -10, -FAR);
      p(x, 20, -FAR);
      if (x != -10 && x != 10) {
        p(x, -10, -NEAR);
        p(x, -10, -FAR);
      }
    }
    for (var y = -10; y <= 20; ++y) {
      p(-10, y, -NEAR);
      p(-10, y, -FAR);
      p(10, y, -NEAR);
      p(10, y, -FAR);
      p(-10, y, -FAR);
      p(10, y, -FAR);
    }
    for (var z = NEAR; z < FAR; ++z) {
      p(-10, -10, -z);
      p(-10, 20, -z);
      p(10, -10, -z);
      p(10, 20, -z);
      p(-10, -10, -z);
      p(10, -10, -z);
    }
    return new Float32Array(data);
  }

  var backgroundBuffer = new gla.Buffer({
    data: makeBackgroundData(),
    views: {
      position: { size: 3, stride: 6 * 4 },
      color: { size: 3, stride: 6 * 4, offset: 3 * 4 },
    },
  });

  // HELPERS ///////////////////////////////////////////////////////////////////

  Math.sign = function(x) {
    if (x > 0) return 1;
    if (x < 0) return -1;
    return 0;
  }

  function makeData(points, color) {
    var data = new Float32Array(points.length / 2 * 5);
    var i = 0;
    for (var j = 0; j < points.length;) {
      data[i++] = Math.round(points[j++]);
      data[i++] = Math.round(points[j++]);
      data[i++] = color[0];
      data[i++] = color[1];
      data[i++] = color[2];
    }
    return data;
  }

  function makeBuffer(data) {
    return new gla.Buffer({
      data: data,
      views: {
        position: { size: 2, stride: 5 * 4 },
        color: { size: 3, stride: 5 * 4, offset: 2 * 4 },
      },
    });
  }

  function sqrDist(a, b) {
    var dx = a.x - b.x;
    var dy = a.y - b.y;
    return dx*dx + dy*dy;
  }

  // GAME OBJECTS //////////////////////////////////////////////////////////////

  var Particles = function(x, y, width, height, n, vmax, g, c) {
    this.x = x;
    this.y = y;
    this.alpha = 1.0;
    this.lineWidth = 2.5;

    var data = new Float32Array(n * 2 * 5);
    var v = new Float32Array(n * 2);
    var di = 0;
    var vi = 0;
    for (var i = 0; i < n; ++i) {
      var x = (2 * Math.random() - 1) * width;
      var y = (2 * Math.random() - 1) * height;
      data[di++] = x;
      data[di++] = y;
      data[di++] = 0;
      data[di++] = 0;
      data[di++] = 0;
      data[di++] = x;
      data[di++] = y;
      data[di++] = c[0];
      data[di++] = c[1];
      data[di++] = c[2];
      var vs = Math.random() * vmax;
      var va = Math.atan2(y, x)
      v[vi++] = vs * Math.cos(va);
      v[vi++] = vs * Math.sin(va);
    }

    this.buffer = new gla.Buffer({
      data: data,
      usage: gla.Buffer.Usage.DYNAMIC_DRAW,
      views: {
        position: { size: 2, stride: 5 * 4 },
        color: { size: 3, stride: 5 * 4, offset: 2 * 4 },
      }
    });
    this.mode = gla.DrawMode.LINES;

    this.die = function() {
      this.dead = true;
      this.buffer.del();
    };

    this.update = function(delta) {
      for (var i = 0; i < n; ++i) {
        v[2*i + 1] -= g * delta;
        data[10*i] = data[10*i + 5];
        data[10*i + 1] = data[10*i + 6];
        data[10*i + 5] += delta * v[2*i];
        data[10*i + 6] += delta * v[2*i + 1];
      }
      this.buffer.set({ data: data });

      this.alpha -= 0.001 * delta;
      if (this.alpha <= 0) {
        this.die();
      }
    };
  };

  var bulletBuffer = makeBuffer(makeData([ 0, -5, 0, 5 ], [1, 1, 1]));

  var Bullet = function(x, y) {
    var self = this;

    this.x = x;
    this.y = y;
    this.width = 1;
    this.height = 5;
    this.r = Math.sqrt(1*1 + 5*5);
    this.buffer = bulletBuffer;
    this.mode = gla.DrawMode.LINES;

    this.update = function(delta) {
      this.y += 0.3 * delta;
      if (this.y > gla.canvas.height + this.height) {
        this.die();
      }
    };

    this.die = function() {
      self.dead = true;
    }

    this.collideWith = function(other) {
      if (other instanceof Enemy || other instanceof Bomb || other instanceof Bunker) {
        this.die();
      }
    };
  };

  var bombBuffer = makeBuffer(makeData([ 0, -10, 3, 10, -3, 10 ], [1, 0, 0]));

  var Bomb = function(x, y, enemy) {
    var self = this;

    this.x = x;
    this.y = y;
    this.enemy = enemy;
    this.width = 3;
    this.height = 10;
    this.r = Math.sqrt(3*3 + 10*10);
    this.buffer = bombBuffer;
    this.mode = gla.DrawMode.LINE_LOOP;

    this.update = function(delta) {
      this.y -= 0.3 * delta;
      if (this.y < 25) {
        this.dead = true;
      }
    };

    this.die = function() {
      self.dead = true;
    }

    this.collideWith = function(other) {
      if (other instanceof Player) {
        if (other.state == 0) {
          this.die();
        }
      } else if (other instanceof Bullet) {
        this.die();
        sounds.bullet_bomb.play();
        entities.push(new Particles(this.x, this.y, this.width, this.height, 32, 0.1, 0.0003, [1, 0, 0]));
      } else if (other instanceof Bunker) {
        this.die();
      }
    }
  };

  var playerBuffer = makeBuffer(makeData([
    -20, -10, 20, -10, 20, 0, 4, 3, 4, 10, -4, 10, -4, 3, -20, 0
  ], [1, 1, 1]));

  var Player = function() {
    var self = this;

    this.x = gla.canvas.width / 2;
    this.y = gla.canvas.height * 0.07;
    this.width = 20;
    this.height = 10;
    this.r = Math.sqrt(20*20 + 10*10);
    this.buffer = playerBuffer;
    this.mode = gla.DrawMode.LINE_LOOP;

    var cooldown = 0;
    this.state = 0; // 0: alive, 1: dead, 2: immortal
    var stateCountdown = 0;

    this.update = function(delta) {
      if (!playing) return;
      var moveDirection = (this.moveLeft ? -1 : 0) + (this.moveRight ? 1 : 0);
      this.vx = moveDirection * 0.3 * (this.state == 1 ? 0 : 1);
      this.x += this.vx * delta;
      this.x = Math.max(this.x, this.width);
      this.x = Math.min(this.x, gla.canvas.width - this.width - 1);

      if (this.shooting && this.state != 1 && playing) {
        cooldown -= delta;
        if (cooldown <= 0) {
          var bullet = new Bullet(this.x, this.y + this.height);
          entities.push(bullet);
          cooldown = 200;
          sounds.shoot.play();
        }
      } else {
        cooldown = 0;
      }

      if (stateCountdown > 0) {
        stateCountdown -= delta;
        if (stateCountdown <= 0) {
          if (this.state == 1) {
            --lives;
            if (lives < 0) {
              lose();
            } else {
              this.state = 2;
              stateCountdown = 3000;
            }
          } else if (this.state == 2) {
            this.state = 0;
          }
        }
      }
      switch (this.state) {
        case 0:
          this.alpha = 1;
          break;
        case 1:
          this.alpha = 0;
          break;
        case 2:
          this.alpha = 0.3 + 0.7 * Math.round(0.5 + 0.5 * Math.sin(stateCountdown * 0.03));
          break;
      }
    }

    this.die = function() {
      stateCountdown = 1000;
      this.state = 1;
      entities.push(new Particles(this.x, this.y, this.width, this.height, 512, 0.2, 0.0003, [1, 1, 1]));
      sounds.die.play();
    }

    this.collideWith = function(other) {
      if (!playing) return;
      if (other instanceof Bomb && this.state == 0) {
        this.die();
      }
    };
  };

  var Mating = function(a, b) {
    this.x = 0;
    this.y = 0;

    var color = [0.8, 0.8, 1.0];
    var N = 5;
    var B = 3;
    var data = new Float32Array(5 * N * B);
    function fillData() {
      var i = 0;
      for (var q = 0; q < B; ++q) {
        for (var p = 0; p < N; ++p) {
          var f = p / (N - 1);
          if (q % 2 == 1) f = 1-f;
          data[i++] = f * a.x + (1-f) * b.x + 20 * (Math.random() - 0.5);
          data[i++] = f * a.y + (1-f) * b.y + 20 * (Math.random() - 0.5);
          i += 3;
        }
      }
    }
    for (var p = 0; p < N * B; ++p) {
      data[5*p + 2] = color[0];
      data[5*p + 3] = color[1];
      data[5*p + 4] = color[2];
    }
    fillData();
    this.buffer = new gla.Buffer({
      data: data,
      usage: gla.Buffer.Usage.DYNAMIC_DRAW,
      views: {
        position: { size: 2, stride: 5 * 4 },
        color: { size: 3, stride: 5 * 4, offset: 2 * 4 },
      },
    });
    this.mode = gla.DrawMode.LINE_STRIP;

    this.die = function() {
      a.mating = null;
      b.mating = null;
      this.dead = true;
      // this.buffer.del(); // bugs on Firefox??
    };

    var time = 0;

    this.update = function(delta) {
      if (sqrDist(a, b) > MATING_RANGE*MATING_RANGE || a.dead || b.dead) {
        this.die();
      } else {
        time += delta;
        if (time >= MATING_TIME) {
          sounds.offspring.play();
          for (var i = 0; i < NUM_CHILDREN; ++i) {
            var dna = crossbreed(a.dna, b.dna);
            newGen.push(dna);
            var f = (i + 1) / (NUM_CHILDREN + 1);
            entities.push(new Baby(f * a.x + (1-f) * b.x, f * a.y + (1-f) * b.y, dna));
          }
          this.die();
        }

        fillData();
        this.buffer.set({ data: data });
      }
    };
  };

  var Baby = function(x, y, dna) {
    this.x = x;
    this.y = y;
    this.buffer = makeBuffer(createEnemyData(dna));
    this.mode = gla.DrawMode.LINES;
    this.lineWidth = 0.5;
    this.scale = 0.5;
    var vy = 0;
    var vx = 2.0 * (Math.random() - 0.5);

    this.die = function() {
      this.dead = true;
    };

    this.update = function(delta) {
      vy += 0.01 * delta;
      this.x += vx;
      this.y += vy;
      if (this.y > gla.canvas.height + 20) {
        this.die();
      }
    };
  };

  function createEnemyData(dna) {
    var b = -17 + 3 * dna[GeneFor.WallForce]; // bottom
    var t = 17 - 3 * dna[GeneFor.WallDist]; // top
    var bw = 16 - 4 * dna[GeneFor.DodgeForce]; // bottom width
    var tw = 16 - 4 * dna[GeneFor.DodgeDist]; // top width
    var ex = 13 - 2 * dna[GeneFor.PlayerDist]; // eye x
    var ey = 4 + 2 * dna[GeneFor.PlayerForce]; // eye y
    var my = -8 + 4 * dna[GeneFor.Speed]; // mouth y
    var mw = 6 + 5 * dna[GeneFor.AttractionDist]; // mouth width
    var mh = -8 * dna[GeneFor.AttractionForce]; // mouth height
    var e1x = 7 * dna[GeneFor.BombAiming]; // eye point 1 x, etc
    var e1y = 7 * dna[GeneFor.BombPrecision];
    var e2x = 7 * dna[GeneFor.BombWillingness];
    var e2y = 7 * dna[GeneFor.Unused6];
    var e3x = 7 * dna[GeneFor.Unused7];
    var e3y = 7 * dna[GeneFor.Unused8];
    var color = [
      0.75 - 0.25 * dna[GeneFor.BombCooldown],
      0.75 + 0.25 * dna[GeneFor.AvoidanceForce],
      0.75 + 0.25 * dna[GeneFor.Flocking]
    ];
    return makeData([
      -bw, b, bw, b,
      bw, b, tw, t,
      tw, t, -tw, t,
      -tw, t, -bw, b,
      -mw, my, mw, my,
      -mw, my, 0, my + mh,
      mw, my, 0, my + mh,
      -ex - e1x, ey + e1y, -ex - e2x, ey + e2y,
      -ex - e2x, ey + e2y, -ex - e3x, ey + e3y,
      -ex - e3x, ey + e3y, -ex - e1x, ey + e1y,
      ex + e1x, ey + e1y, ex + e2x, ey + e2y,
      ex + e2x, ey + e2y, ex + e3x, ey + e3y,
      ex + e3x, ey + e3y, ex + e1x, ey + e1y,
    ], color);
  }

  var Genes = [
    "AttractionForce",
    "AttractionDist",
    "AvoidanceForce",
    "Speed",
    "DodgeForce",
    "DodgeDist",
    "WallForce",
    "WallDist",
    "PlayerForce",
    "PlayerDist",
    "Flocking",
    "BombCooldown",
    "BombAiming",
    "BombPrecision",
    "BombWillingness",
    "Unused6", "Unused7", "Unused8"
  ];
  var GeneFor = (function() {
    var gf = {};
    for (var i = 0; i < Genes.length; ++i) {
      gf[Genes[i]] = i;
    }
    return gf;
  })();

  var Enemy = function(x, y, dna) {
    var self = this;

    this.x = x;
    this.y = y;
    this.dna = dna;
    this.width = 20;
    this.height = 20;
    this.vx = 0;
    this.vy = 0;
    this.r = Math.sqrt(20*20 + 20*20);
    var data = createEnemyData(dna);
    this.buffer = makeBuffer(data);
    this.mode = gla.DrawMode.LINES;

    function inRange(type, range) {
      var a = [];
      for (var i = 0; i < entities.length; ++i) {
        var entity = entities[i];
        if (entity instanceof type && entity != self && sqrDist(entity, self) < range*range) {
          a.push(entity);
        }
      }
      return a;
    }

    function attractx(x, profile) {
      var d = x - self.x;
      var f = profile(d);
      self.vx += Math.sign(d) * f;
    }

    function attracty(y, profile) {
      var d = y - self.y;
      var f = profile(d);
      self.vy += Math.sign(d) * f;
    }

    function attract2D(other, profile) {
      var dx = other.x - self.x;
      var dy = other.y - self.y;
      var d = Math.sqrt(dx*dx + dy*dy);
      var f = profile(d);
      var a = Math.atan2(dy, dx);
      self.vx += Math.cos(a) * f;
      self.vy += Math.sin(a) * f;
    }

    function exponential(force, halfDist) {
      return function(d) {
        return force * Math.pow(2, -Math.abs(d) / (1 + halfDist));
      }
    }

    function step(force, distance) {
      return function(d) {
        return d > distance ? 0 : force;
      }
    }

    function smoothstep(force, a, b) {
      return function(d) {
        if (d < a) return force;
        if (d > b) return 0;
        return force * (1 - (d-a) / (b-a))
      }
    }

    var MATING_INTERVAL = 5000;

    var cooldown = 3000 + 2000 * Math.random();
    this.matingCooldown = 10000 + Math.random() * MATING_INTERVAL;
    this.mating = 0;

    this.update = function(delta) {
      //this.vx = 0;
      //this.vy = 0;
      this.matingCooldown -= delta;

      // Flocking and collision avoidance
      var enemiesInMatingRange = [];
      for (var i = 0; i < enemies.length; ++i) {
        var enemy = enemies[i];
        if (enemy == this) continue;

        // Attraction
        if (this.matingCooldown < 0) {
          attract2D(enemy, exponential(0.1 * dna[GeneFor.AttractionForce], 100 * (1 + dna[GeneFor.AttractionDist])));
        }

        // Avoidance
        attract2D(enemy, smoothstep(-0.3 * (1 + dna[GeneFor.AvoidanceForce]), 60, 80));

        // Flocking
        var f = dna[GeneFor.Flocking] * 100.0 / sqrDist(this, enemy);
        this.vx += enemy.vx * f;
        this.vy += enemy.vy * f;

        if (!enemy.mating && enemy.matingCooldown < 0 &&
            sqrDist(this, enemy) <= MATING_RANGE*MATING_RANGE) {
          enemiesInMatingRange.push(enemy);
        }
      }

      // Bullet dodging
      var bulletsInRange = inRange(Bullet, 0.2 * gla.canvas.width);
      for (var i = 0; i < bulletsInRange.length; ++i) {
        var bullet = bulletsInRange[i];
        attract2D(bullet, exponential(-0.3 * dna[GeneFor.DodgeForce], 50 * (1 + dna[GeneFor.DodgeDist])));
      }

      // Wall avoidance
      wallProfile = exponential(-0.1 * (1 + dna[GeneFor.WallForce]), 30 + 20 * (1 + dna[GeneFor.WallDist]));
      attractx(0, wallProfile);
      attractx(gla.canvas.width - 1, wallProfile);
      attracty(gla.canvas.height - 1, wallProfile);
      attracty(0.2 * gla.canvas.height, wallProfile);

      // Player confrontation
      if (player.state != 1) {
        attractx(player.x, exponential(-0.05 * dna[GeneFor.PlayerForce], 200 * (1 + dna[GeneFor.PlayerDist])));
      }

      // Physics
      var vmax = 0.15 + 0.1 * dna[GeneFor.Speed];
      var v = Math.sqrt(this.vx*this.vx + this.vy*this.vy);
      if (v > vmax) {
        var f = vmax / v;
        this.vx *= f;
        this.vy *= f;
      }
      this.x += delta * this.vx;
      this.y += delta * this.vy;
      this.x = Math.max(this.x, this.width);
      this.x = Math.min(this.x, gla.canvas.width - this.width - 1);
      this.y = Math.max(this.y, gla.canvas.height * 0.2 + this.height);
      this.y = Math.min(this.y, gla.canvas.height - this.height - 1);

      // Mating
      if (!this.mating && this.matingCooldown < 0 && enemiesInMatingRange.length > 0) {
        var mate = enemiesInMatingRange[Math.floor(Math.random() * enemiesInMatingRange.length)];
        sounds.mate.play();
        this.mating = mate.mating = new Mating(this, mate);
        entities.push(this.mating);
        this.matingCooldown = MATING_INTERVAL;
      }

      // Bombing
      var wantToDropBomb =
        Math.abs(player.x + 5 * dna[GeneFor.BombAiming] * this.y * player.vx - this.x) < 40 * (1 + dna[GeneFor.BombPrecision]) &&
        Math.random() > dna[GeneFor.BombWillingness] * delta * 0.01;
      cooldown -= delta;
      if (wantToDropBomb) {
        if (cooldown <= 0) {
          entities.push(new Bomb(this.x, this.y - this.height, this));
          sounds.bomb.play();
          cooldown = 3000 + 2000 * dna[GeneFor.BombCooldown];
        }
      }
    };

    this.die = function() {
      if (self.dead) return;
      self.dead = true;
      sounds.kill.play();
      entities.push(new Particles(this.x, this.y, this.width, this.height, 512, 0.2, 0.0003, [ data[2], data[3], data[4] ]));
      --enemiesLeft;
      if (enemiesLeft == 0) {
        window.setTimeout(function() {
          win();
        }, 2000);
      }
    }

    this.collideWith = function(other) {
      if (!playing) return;
      if (other instanceof Bullet && !other.dead) {
        this.die();
      } else if (other instanceof Enemy) {
        this.die();
      }
    };
  };

  var bunkerBuffer = makeBuffer(makeData([
    -40, -15, 40, -15, 40, 5, 20, 15, -20, 15, -40, 5
  ], [1, 1, 0]));

  var Bunker = function(x, y) {
    this.x = x;
    this.y = y;
    this.width = 40;
    this.height = 15;
    this.r = Math.sqrt(this.width*this.width + this.height*this.height);
    this.alpha = 1;
    this.buffer = bunkerBuffer;
    this.mode = gla.DrawMode.LINE_LOOP;

    this.update = function(delta) {
    };

    this.collideWith = function(other) {
      if (other instanceof Bomb || other instanceof Bullet) {
        this.alpha -= 0.1;
        sounds.bunker.play();
        if (this.alpha <= 0.001) {
          this.dead = true;
          entities.push(new Particles(this.x, this.y, this.width, this.height, 512, 0.2, 0.0003, [1, 1, 0]));
        } else {
          entities.push(new Particles(other.x, Math.min(Math.max(other.y, this.y - this.height), this.y + this.height), 3, 3, 64, 0.05, 0.0002, [1, 1, 0]));
        }
      }
    };
  };

  // COLLISION DETECTION ///////////////////////////////////////////////////////

  function overlap(amin, amax, bmin, bmax) {
    return amin <= bmax && bmin <= amax;
  }

  function collide(a, b) {
    if (!a.r || !b.r) return false;
    var dx = a.x - b.x;
    var dy = a.y - b.y;
    var sr = a.r + b.r;
    if (dx*dx + dy*dy > sr*sr) {
      return false;
    }
    return overlap(a.x - a.width, a.x + a.width, b.x - b.width, b.x + b.width) && overlap(a.y - a.height, a.y + a.height, b.y - b.height, b.y + b.height);
  }

  // INPUT /////////////////////////////////////////////////////////////////////

  function onKeyDown(e) {
		switch (e.keyCode) {
			case 37: player.moveLeft = true; break;
			case 39: player.moveRight = true; break;
      case 32: player.shooting = true; break;
      /*
      case 87:
        for (var i = 0; i < enemies.length; ++i) {
          enemies[i].die();
        }
        break;
      case 76:
        lives = 0;
        player.die();
        break;
      */
			default: return;
		}
    e.preventDefault();
  }

  function onKeyUp(e) {
		switch (e.keyCode) {
      case 37: player.moveLeft = false; break
      case 39: player.moveRight = false; break;
      case 32: player.shooting = false; break;
			default: return;
		}
    e.preventDefault();
  }

  function enableInput() {
    $(window).bind('keydown', onKeyDown);
    $(window).bind('keyup', onKeyUp);
  }
  function disableInput() {
    $(window).unbind('keydown', onKeyDown);
    $(window).unbind('keyup', onKeyUp);
  }

  // GENE POOL /////////////////////////////////////////////////////////////////

  var DNA_ENC = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";

  function crossbreed(a, b) {
    var c = [];
    for (var k = 0; k < NUM_GENES; ++k) {
      var x = Math.random();
      c[k] = x * a[k] + (1 - x) * b[k];
      if (Math.random() < MUTATE_PROB) {
        c[k] += MUTATE_AMOUNT * (2 * Math.random() - 1);
      }
      c[k] = Math.min(Math.max(c[k], -1), 1);
    }
    return c;
  }

  function randomDna() {
    var dna = [];
    for (var j = 0; j < NUM_GENES; ++j) {
      dna.push(2 * Math.random() - 1);
    }
    return dna;
  }

  function encodeDna(dna) {
    var part = "";
    for (var j = 0; j < dna.length; ++j) {
      var index = Math.floor(0.5 * (dna[j] + 1) * DNA_ENC.length);
      part += DNA_ENC[Math.min(Math.max(index, 0), DNA_ENC.length - 1)];
    }
    return part;
  }

  function decodeDna(part) {
    var dna = [];
    for (var j = 0; j < part.length; ++j) {
      dna.push(2 * DNA_ENC.indexOf(part[j]) / (DNA_ENC.length - 1) - 1);
    }
    return dna;
  }

  function encodeGenePool(genePool) {
    var hash = "";
    for (var i = 0; i < genePool.length; ++i) {
      if (hash != "") {
        hash += "_";
      }
      hash += encodeDna(genePool[i]);
    }
    return hash;
  }

  function decodeGenePool(hash) {
    var parts = hash.split("_");
    var genePool = [];
    for (var i = 0; i < parts.length; ++i) {
      genePool.push(decodeDna(parts[i]));
    }
    return genePool;
  }

  function randomGenePool() {
    var genePool = [];
    for (var i = 0; i < NUM_ENEMIES; ++i) {
      genePool.push(randomDna());
    }
    return genePool;
  }

  function logGenePool(prefix, genePool) {
    var line = prefix + ": " + genePool.length + " [ ";
    var mean = [];
    for (var j = 0; j < NUM_GENES; ++j) {
      mean[j] = 0;
      for (var i = 0; i < genePool.length; ++i) {
        mean[j] += genePool[i][j];
      }
      mean[j] /= genePool.length;
      line += Genes[j] + "=" + mean[j].toFixed(3) + " ";
    }
    line += "]";
    console.log(line);
  }

  // GAME STATE ////////////////////////////////////////////////////////////////

  var oldGen = [];
  var newGen = [];

  var entities = [];
  var player = {};
  var enemies = [];
  var lives = 0;
  var enemiesLeft = 0;

  // CHROME ////////////////////////////////////////////////////////////////////
  
  $("#sound").click(function(e) {
    soundEnabled = !soundEnabled;
    $("#sound").html(soundEnabled ? "Turn sound off" : "Turn sound on");
    e.preventDefault();
  });

  $("#mus").click(function(e) {
    musicEnabled = !musicEnabled;
    music.muted = !musicEnabled;
    $("#mus").html(musicEnabled ? "Turn music off" : "Turn music on");
    e.preventDefault();
  });

  function setHash(hash) {
    window.location.hash = hash;
    $("#bookmark")[0].href = window.location;
  }
  setHash(window.location.hash);

  $("#restart").click(function(e) {
    setHash("");
    window.location.reload(false);
    e.preventDefault();
  });

  function pressSpace(fun) {
    function wrap(e) {
      if (e.keyCode == 32) {
        fun();
        $(window).unbind("keydown", wrap);
        e.preventDefault();
      }
    }
    $(window).bind("keydown", wrap);
  }

  function intro() {
    body.addClass("intro");
    pressSpace(function() {
      body.removeClass("intro");
      var genePool = randomGenePool();
      setHash(encodeGenePool(genePool));
      window.setTimeout(function() {
        enemies = createEnemies(genePool);
        entities = [].concat(enemies);
        startGame();
      }, 1000);
    });
  }

  function lose() {
    playing = false;
    window.setTimeout(function() {
      body.addClass("lose1");

      pressSpace(function() {
        body.removeClass("lose1 lose2");
        resetGame();
        enemies = createEnemies(oldGen);
        entities = [].concat(enemies);
        startGame();
      });
    }, 1000);

    window.setTimeout(function() { body.addClass("lose2"); }, 2000);
  }

  function win() {
    playing = false;
    body.addClass("win1");
    window.setTimeout(function() { body.addClass("win2"); }, 1000);

    logGenePool("Old", oldGen);
    logGenePool("New", newGen);

    console.log("Created " + newGen.length + " offspring; adding " + Math.max(0, NUM_ENEMIES - newGen.length) + " random ones");

    var genePool = [];
    while (genePool.length < NUM_ENEMIES) {
      if (newGen.length > 0) {
        var i = Math.floor(Math.random() * newGen.length);
        genePool.push(newGen[i]);
        newGen.splice(i, 1);
      } else {
        genePool.push(randomDna());
      }
    }
    oldGen = genePool;
    newGen = [];
    setHash(encodeGenePool(genePool));

    function setUpdate(enemy, startx, starty, targetx, targety, start, duration) {
      var now = 0;
      enemy.x = startx;
      enemy.y = starty;
      enemy.alpha = 0;
      origUpdate = enemy.update;
      enemy.origUpdate = enemy.update;
      enemy.update = function(delta) {
        now += delta;
        var f = Math.sqrt((now - start) / duration);
        f = Math.min(Math.max(f, 0), 1);
        this.x = f * targetx + (1-f) * startx;
        this.y = f * targety + (1-f) * starty;
        this.alpha = Math.min(1, f * 5);
      };
      enemy.origCollideWith = enemy.collideWith;
      enemy.collideWith = null;
    }
    var tmp = [];
    for (var i = 0; i < genePool.length; ++i) {
      var enemy = new Enemy(0, 0, genePool[i]);
      setUpdate(enemy,
          gla.canvas.width / 2 + 100 * (i - NUM_ENEMIES / 2 + 0.5),
          2 * gla.canvas.height,
          gla.canvas.width / 2 + 80 * (Math.floor(i / 2) - NUM_ENEMIES / 4 + 0.5),
          gla.canvas.height * (0.7 - 0.12 * (i % 2)),
          2000 + 1000 * i / NUM_ENEMIES, 1000);
      entities.push(enemy);
      tmp.push(enemy);
    }

    window.setTimeout(function() { body.addClass("win3"); }, 5000);
    window.setTimeout(function() { body.addClass("win4"); }, 6000);

    window.setTimeout(function() {
      pressSpace(function() {
        body.removeClass("win1 win2 win3 win4");
        resetGame();
        enemies = [].concat(tmp);
        entities = [].concat(tmp);
        for (var i = 0; i < enemies.length; ++i) {
          var enemy = enemies[i];
          enemy.update = enemy.origUpdate;
          enemy.collideWith = enemy.origCollideWith;
        }
        startGame();
      });
    }, 4000);
  }

  function createEnemies(genePool) {
    var enemies = [];
    for (var i = 0; i < genePool.length; ++i) {
      var dna = genePool[i];
      var enemy = new Enemy(
          gla.canvas.width / 2 + 80 * (Math.floor(i / 2) - NUM_ENEMIES / 4 + 0.5),
          gla.canvas.height * (0.7 - 0.12 * (i % 2)),
          dna);
      enemies.push(enemy);
    }
    return enemies;
  }

  // GAME //////////////////////////////////////////////////////////////////////

  function resetGame() {
    entities = [];
    player = null;
    enemies = [];
    lives = 0;
    enemiesLeft = 0;
  }

  function startGame() {
    // assumes enemies have been created and are in enemies[] and entities[]

    newGen = [];

    lives = 3;

    player = new Player();
    entities.push(player);

    for (var i = 0; i < 5; ++i) {
      var bunker = new Bunker((0.5 + i) * gla.canvas.width / 5, gla.canvas.height * 0.15);
      entities.push(bunker);
    }

    enemiesLeft = enemies.length;

    body.addClass("getready");

    for (var i = 500; i < 2000; i += 500) {
      window.setTimeout(function() { sounds.blip.play(); }, i);
    }
    for (var i = 2000; i <= 3000; i += 125) {
      window.setTimeout(function() { sounds.blip.play(); }, i);
    }
    window.setTimeout(function() {
      body.addClass("reallygetready");
    }, 2000);
    window.setTimeout(function() {
      body.removeClass("getready");
    }, 2500);
    window.setTimeout(function() {
      body.removeClass("reallygetready");
      playing = true;
    }, 3000);
  }

  // ACTUALLY DO STUFF /////////////////////////////////////////////////////////

  window.onblur = function() {
    paused = true;
    body.addClass("paused");
    music.pause();
  };

  window.onfocus = function() {
    paused = false;
    body.removeClass("paused");
    music.play();
  };

  enableInput();

  if (window.location.hash == "") {
    intro();
  } else {
    oldGen = decodeGenePool(window.location.hash);
    enemies = createEnemies(oldGen);
    entities = [].concat(enemies);
    startGame();
  }

  gla.mainLoop(function(delta) {

    if (delta > 100) {
      delta = 100;
    }

    // UPDATE //////////////////////////////////////////////////////////////////

    if (!paused) {
      // Updates
      for (var i = 0; i < entities.length; ++i) {
        var entity = entities[i];
        entity.update(delta);
      }

      // Collisions
      for (var i = 0; i < entities.length; ++i) {
        var a = entities[i];
        for (var j = 0; j < entities.length; ++j) {
          if (i == j) continue;
          var b = entities[j];
          if (collide(a, b) && !a.dead && !b.dead) {
            if (a.collideWith) a.collideWith(b);
            if (b.collideWith) b.collideWith(a);
          }
        }
      }

      // Deaths
      for (var i = 0; i < entities.length; ++i) {
        var entity = entities[i];
        if (entity.dead) {
          entities.splice(i, 1);
          --i;
        }
      }
    }

    // RENDER //////////////////////////////////////////////////////////////////

    gla.clear({ color: [0, 0, 0] });

    gla.draw({
      program: program3d,
      uniforms: {
        transform: backgroundTransform,
        alpha: 1.0,
      },
      attributes: {
        position: backgroundBuffer.views.position,
        color: backgroundBuffer.views.color,
      },
      mode: gla.DrawMode.LINES,
      count: backgroundBuffer.views.position.numItems(),
      lineWidth: 2.0,
    });

    for (var i = 0; i < lives; ++i) {
      mat4.identity(modelMatrix);
      mat4.translate(modelMatrix, [24 + 50*i, 14, 0]);

      mat4.identity(transform);
      mat4.multiply(transform, projectionMatrix);
      mat4.multiply(transform, modelMatrix);

      gla.draw({
        program: program,
        uniforms: {
          transform: transform,
          alpha: 0.3,
        },
        attributes: {
          position: playerBuffer.views.position,
          color: playerBuffer.views.color,
        },
        mode: gla.DrawMode.LINE_LOOP,
        count: playerBuffer.views.position.numItems(),
        lineWidth: 2.0,
      });
    }

    for (var i = 0; i < entities.length; ++i) {
      var entity = entities[i];

      mat4.identity(modelMatrix);
      mat4.translate(modelMatrix, [Math.floor(entity.x), Math.floor(entity.y), 0]);
      if (entity.scale) {
        mat4.scale(modelMatrix, [ entity.scale, entity.scale, entity.scale ]);
      }

      mat4.identity(transform);
      mat4.multiply(transform, projectionMatrix);
      mat4.multiply(transform, modelMatrix);

      gla.draw({
        program: program,
        uniforms: {
          transform: transform,
          alpha: entity.alpha !== undefined ? entity.alpha : 1.0,
        },
        attributes: {
          position: entity.buffer.views.position,
          color: entity.buffer.views.color,
        },
        mode: entity.mode,
        count: entity.buffer.views.position.numItems(),
        lineWidth: entity.lineWidth !== undefined ? entity.lineWidth : 2.5,
      });
    }

    //gla.exitMainLoop();
  });
});
