////////////////////////////////////////////////////
// Game state machine.
var State = new function() { this.name = null; this.current = null; this.module = null; };

State.go = function(name, context) {
  this.name = name;
  
  // Get the next module and state.
  var next_module = null;
  var next_state = null;
  
  var dot = name.indexOf('.');
  if (dot >= 0) {
    next_module = this[name.substring(0, dot)];
    next_state = next_module[name.substring(dot + 1)];
  } else {
    next_state = this[name];
  }
  
  // If state doesn't change, do nothing.
  if (this.current == next_state)
    return;
  
  // Exit current state.
  if (this.current && this.current.leave) 
    this.current.leave(context);

  // Switch module if necessary.  
  if (this.module != next_module) {
    if (this.module && this.module.leave) 
      this.module.leave(context);
    
    this.module = next_module;
  
    if (this.module && this.module.enter)
      this.module.enter(context);  
  }
  
  // Enter a new state.  
  this.current = next_state;
  if (this.current && this.current.enter)
    this.current.enter(context);
};

////////////////////////////////////////////////////
// Launcher.
State.launch = {
  enter: function(context) {
    Widgets.Transitions.hide($("#id_menu"));
    Widgets.Transitions.hide($("#id_scoreboard"));
    Widgets.Transitions.hide($("#id_lobby"));
    Widgets.Transitions.hide($("#id_end"));
    Widgets.Transitions.hide($("#id_help"));
    Widgets.Transitions.hide($("#id_comments"));
    
    Widgets.Transitions.fadeIn($("#frame_lobby"), function() {      
      State.go("intro");
    });
  }  
};

////////////////////////////////////////////////////
// Visual renderer.

var on_collision = null;

function Visual(width, height) {
  // Game state.
  this.GameState = { running: 0, ending: 1, ended: 2 };
  
  this._state = this.GameState.running;
  
  this._position = { pressed: false };
  this._input = null;
  
  this._ballRadius = 10;
  
  this._out = 0;
  this._lastOut = -1;  
  this._lastOutTime = new Date();
  
  this._endCountdown = -1;
  this._endSteps = Widgets.isApple ? 60 : 120;
  
  // Game rendering.
  this._width = width;
  this._height = height;    
   
  this._canvas = new Widgets.Canvas($("#id_main_canvas"), width, height);
  this._canvasRaw = this._canvas.get();
  
  this._canvas.fill('rgba(64, 68, 72, 1)', 0, 0, this._width, this._height);
  
  // Sparkles.
  this._sparks = [];
  
  // Scoring.
  this._scoreLabel = $("#id_score_label");
  this._scoreLabel.html("");
  
  this._lastScore = 0;
  this._score = 0;
  this._level = 0;
  
  // Timing.
  this._timer = null;
  
  // Collisions.
  var This = this;
  on_collision = function(a, b) { This.onCollision(a, b); };

  // Initialization.
  this.initializePhysics();
  
  this.seed();
  
  this.render(true);  
}

Visual.prototype.onCollision = function(a, b) {
  if (a.type == instance_type.ball && b.type == instance_type.ball && a.color != 0 && b.color != 0) {
    if (a.color == b.color) {
      if (this._score < 1500)
        this._lastOutTime = new Date(new Date() - 10000);
      
      a.done = true;
      b.done = true;
      
      this._score += 100;
    } else {
      this._score -= 10;
      if (this._score < 0)
        this._score = 0;
    }
    
    if (this._sparks.length < 25) {    
      var angle = Math.random() * Math.PI * 2;
      
      var x = (a.c.x + b.c.x) * 0.5;
      var y = (a.c.y + b.c.y) * 0.5;
      
      var v = 5;
      
      var a0 = 0 + Math.random() - 0.5;
      var a1 = 2 + Math.random() - 0.5;
      var a2 = 4 + Math.random() - 0.5;
      
      this._sparks.push({ x: x, y: y, dx: Math.cos(angle + a0 * Math.PI / 3) * v, dy: Math.sin(angle + a0 * Math.PI / 3) * v, r: 10 });
      this._sparks.push({ x: x, y: y, dx: Math.cos(angle + a1 * Math.PI / 3) * v, dy: Math.sin(angle + a1 * Math.PI / 3) * v, r: 10 });
      this._sparks.push({ x: x, y: y, dx: Math.cos(angle + a2 * Math.PI / 3) * v, dy: Math.sin(angle + a2 * Math.PI / 3) * v, r: 10 });
    }
  }
};

Visual.prototype.score = function() {
  return this._score;
}

Visual.prototype.initializePhysics = function() {
  reset_simulation();
  
  var s = 0;
  
  add_wall(s, s, this._width - s, s);
  add_wall(this._width - s, s, this._width - s, this._height - s);
  add_wall(this._width - s, this._height - s, s, this._height - s);
  add_wall(s, this._height - s, s, s);
  
  this._center = add_ball('c', v_(this._width * 0.5, this._height * 0.5), 20.0, 1.0);
  this._center.color = 0;
  this._center.dampening = -1000;
};

Visual.prototype.seed = function() {
  if (this._score == 0) {
    if (_balls.length == 1) {
      var c = Math.floor(Math.random() * 4) + 1;
      
      this.addBall(c, 0, 0);
      this.addBall(c, 0, 0);
      
      this._lastOutTime = new Date();
    }
  } else {
    var delay = 2500;
    if (this._score < 1000)
      delay = 6500;
    else if (this._score < 2000)
      delay = 6500;
    else if (this._score < 3000)
      delay = 5500;
    else if (this._score < 4000)
      delay = 4500;
    else if (this._score < 6500)
      delay = 3500;
    else if (this._score < 10000)
      delay = 2700;
    else
      delay = 2200;
      
    if (new Date() - this._lastOutTime > delay && _balls.length < 21) {
      if (this._score < 1000) {
        var c = Math.floor(Math.random() * 4) + 1;        
        this.addBall(c, 1, 2);
        this.addBall(c, 1, 2);
      } else if (this._score < 2000) {
        this.addBall(Math.floor(Math.random() * 6) + 1, 2, 3);
        this.addBall(Math.floor(Math.random() * 6) + 1, 2, 3);
      } else if (this._score < 4000) {
        this.addBall(Math.floor(Math.random() * 6) + 1, 2, 5);
        this.addBall(Math.floor(Math.random() * 6) + 1, 2, 5);
      } else if (this._score < 6000) {
        this.addBall(Math.floor(Math.random() * 6) + 1, 2, 7);
        this.addBall(Math.floor(Math.random() * 6) + 1, 2, 7);      
      } else {
        this.addBall(Math.floor(Math.random() * 7) + 1, 2, 7);
        this.addBall(Math.floor(Math.random() * 7) + 1, 2, 7);
      }
      this._lastOutTime = new Date();
    }
  }
};

Visual.prototype.addBall = function(c, variation_min, variation_max) {
  var oversize = 15;
  
  var size = Math.floor(Math.random() * 4);
  
  var x, y;
  if (size == 0) {
    x = - oversize;
    y = Math.random() * (this._height + oversize + oversize) - oversize;
  } else if (size == 1) {
    x = this._width + oversize;
    y = Math.random() * (this._height + oversize + oversize) - oversize;
  } else if (size == 2) {
    x = Math.random() * (this._width + oversize + oversize) - oversize;
    y = - oversize;
  } else {
    x = Math.random() * (this._width + oversize + oversize) - oversize;
    y = this._height + oversize;
  }
  
  var x_target = this._width * 0.5 + (Math.random() * 200 - 100);
  var y_target = this._height * 0.5 + (Math.random() * 200 - 100);
  
  var speed = 100 + Math.random() * 50 - 25;
  var v = v_mul(v_nor(v_sub(v_(x_target, y_target), v_(x, y))), speed);
  
  var f = Math.random();
  var m = variation_max + variation_min;
  var b =
    m == 0 ?
      add_ball(null, v_(x, y), this._ballRadius, 1.0) :
      add_ball(null, v_(x, y), this._ballRadius + f * m - variation_min, 1.0 + (f * m - variation_min) / m * 0.3)
  
  b.color = c;
  b.v = v;
  b.done = false;
  b.fresh = true;
  b.out = 0;
  
  ++this._out;
}

Visual.prototype.updateScore = function() {
  this._scoreLabel.html("Score:&nbsp;" + this._score + "&nbsp;Out:&nbsp;" + this._out);
}

function sign(x) {
  if (x < 0)
    return -1;
  if (x > 0)
    return 1;
  
  return 0;
}

Visual.prototype.circleBegin = function(cFill, cLine, w) {
  var canvas = this._canvasRaw;
  
  canvas.fillStyle = cFill;
  canvas.strokeStyle = cLine;
  canvas.lineWidth = w;
  
  this._canvasRaw.beginPath();
};

Visual.prototype.circle = function(x, y, r) {
  this._canvasRaw.moveTo(x + r, y);  
  this._canvasRaw.arc(x, y, r, 0, Math.PI * 2, false);
};

Visual.prototype.circleEnd = function() {
    this._canvasRaw.fill();
  this._canvasRaw.stroke();
};

function sort_balls(a, b) {
  var a_c = a.color;
  var b_c = b.color;
  
  if (a_c == b_c)
    return 0;
  
  if (a_c < b_c)
    return -1;
  
  return 1;
}

Visual.prototype.render = function(first) {
  var This = this;
  
  // Simulate.
  step_simulation(0.05);  
  
  // Clear out dead stuff.
  for (var ball_index = 0; ball_index < _balls.length;) {
    var b = _balls[ball_index];
    if (!b.done) {
      if (b.c.x < 0 || b.c.y < 0 || b.c.x > this._width || b.c.y > this._height) {
        ++b.out;
        if (b.out > 20)
          b.done = true;
      }      
    }
    if (b.done) {
      --this._out;
      
      _balls[ball_index] = _balls[_balls.length - 1];
      _balls.pop();
      
    } else {
       ++ball_index    
    }
  }
  
  this.seed();  
  
  // Update score.
  if (this._lastScore != this._score || this._lastOut != this._out) {
    this._lastScore = this._score;
    this._lastOut = this._out;
    
    this.updateScore();
  }
  
  // Check end game conditions.
  var backgroundColor = 'rgba(64, 68, 72, 0.45)';
  
  if (_balls.length >= 21) {
    if (this._endCountdown == -1)
      this._endCountdown = this._endSteps;
    else {
      --this._endCountdown;
      if (this._endCountdown < 0)
        this._endCountdown = 0;
    }
    
    var f = this._endCountdown / this._endSteps;
    
    var r = 64 * f + 184 * (1 - f);
    var g = 68 * f + 88 * (1 - f);
    var b = 72 * f + 102 * (1 - f);
    
    backgroundColor = 'rgba(' + Math.floor(r) + ', ' + Math.floor(g) + ', ' + Math.floor(b) + ', 0.45)';
  } else {
    this._endCountdown = -1;
  }
  
  // Fade out.
  var canvas = this._canvas;  
  
  canvas.fill(backgroundColor, 0, 0, this._width, this._height);  
  
  if (this._input) {
    var diff = v_sub(this._input, this._center.c);
    var distance = v_len(diff);
    
    var speed = 350;
    
    if (distance > 25)
      this._center.v = v_mul(v_nor(diff), speed);
    else if (distance > 3)
      this._center.v = v_mul(v_nor(diff), speed - (20 - distance) * (speed / 20));
    else
      this._center.v = v_(0, 0);
  }
  
  _balls.sort(sort_balls);
  
  var last_color = -1;
  
  // Render out.
  // Draw sparks.
  for (var spark = 0; spark < this._sparks.length;) {
    var s = this._sparks[spark];
    
    s.r = s.r - 1;
    if (s.r == 0) {
      this._sparks[spark] = this._sparks[this._sparks.length - 1];
      this._sparks.pop();
    } else {
      s.x += s.dx;
      s.y += s.dy;
      spark++;
    }
  }
  
  if (this._sparks.length > 0) {  
    this._canvasRaw.fillStyle = 'rgb(255, 255, 255)';
    this._canvasRaw.beginPath();
    for (var spark = 0; spark < this._sparks.length; spark++) {
      var s = this._sparks[spark];
      
      this._canvasRaw.moveTo(s.x + s.r, s.y);  
      this._canvasRaw.arc(s.x, s.y, s.r, 0, Math.PI * 2, false);
    }
    this._canvasRaw.fill();
  }
  
  // Draw balls.
  for (var ball_index = 1; ball_index < _balls.length; ++ball_index) {
    var b = _balls[ball_index];
    if (!b.active)
      continue;
    
    if (b.color != last_color) {
      if (last_color != -1)
        this.circleEnd();
      
      var color;
      if (b.color == 1)
        color = 'rgb(152, 248, 73)';
      else if (b.color == 2)
        color = 'rgb(246, 83, 89)';
      else if (b.color == 3)
        color = 'rgb(87, 132, 248)';
      else if (b.color == 4)
        color = 'rgb(246, 235, 87)';
      else if (b.color == 5)
        color = 'rgb(246, 87, 255)';
      else if (b.color == 6)
        color = 'rgb(80, 242, 248)';
      else if (b.color == 7)
        color = 'rgb(50, 50, 50)';
        
      this.circleBegin(color, 'rgba(32, 32, 32, 0.5)', 1.8);
      
      last_color = b.color;
    }
  
    this.circle(b.c.x, b.c.y, b.r);    
  }
  
  if (last_color != -1)
    this.circleEnd();
  
  // Draw controller.
  this.circleBegin('rgba(245, 249, 252, 1)', 'rgba(32, 32, 32, 0.5)', 1.8);
  this.circle(this._center.c.x, this._center.c.y, this._center.r);
  this.circleEnd();
  
  if (this._endCountdown == 0) {
    State.go("game.end");
  } else {  
    this._timer = setTimeout(function() {
      This.render();
    }, Widgets.isApple ? 5: 50);
  }
};

Visual.prototype.inputStart = function(x, y) {
  if (this._state == this.GameState.running) {
    this._position.pressed = true;
    
    this._input = { x: x, y: y };
  }
};

Visual.prototype.inputMove = function(x, y) {
  if (this._input)
    this._input = { x: x, y: y };
};

Visual.prototype.inputEnd = function(x, y) {
  this._position.pressed = false;
  this._input = null;
};

Visual.prototype.dispose = function() {
  if (this._timer)
    clearTimeout(this._timer);
};

////////////////////////////////////////////////////
// Intro module.
State.intro = {
  enter: function() {
    Widgets.Transitions.fadeIn($("#id_menu"));
    
    if (navigator && navigator.userAgent) {      	
      if ((navigator.userAgent + "").toLowerCase().indexOf("419.3") >= 0)
        Widgets.MessageBox.notice("<div style='margin-top: -10px;'>Sorry, this game requires <span style='color: #faa;'>iPhone software version 2.0</span> or higher. It's a free update in your iTunes!<br/>Or you can play in Safari or Firefox on your desktop.</div>", null, true);
    }
  },
  leave: function() {
    Widgets.Transitions.fadeOut($("#id_menu"));
  }
};

////////////////////////////////////////////////////
// Help module.
State.help = {
  enter: function() {
    Widgets.Transitions.fadeIn($("#id_help"));
  },
  leave: function() {
    Widgets.Transitions.fadeOut($("#id_help"));
  }
};

////////////////////////////////////////////////////
// Comments module.
State.comments = {
  enter: function() {
    Widgets.Transitions.fadeIn($("#id_comments"));
    
    $("#id_comments-comment").val("");

    $.ajax({type: "POST", url: Utility.formatUrl("/comments/"), data: {},
      success: function(json) {
        var response = JSON.parse(json);
        if (response.errors) {          
        } else {
          if (response.comments) {
            var sb = new StringBuilder();

            for (var i = 0; i < response.comments.length; ++i) {
              var c = response.comments[i];
              
              sb.appendList('<div class="comment"><div class="name">', $.encode(c.n), ' said:</div><div class="value">', $.encode(c.c), '</div></div>');
            }

            $("#id_comments_last").html(sb.toString());
          }
        }
      }
    });
  },
  leave: function() {
    Widgets.Transitions.fadeOut($("#id_comments"));
  }
};

////////////////////////////////////////////////////
// Scoreboard.
State.scoreboard = {
  enter: function() {
    $("#id_scoreboard_all").html("");
    $("#id_scoreboard_today").html("");
    
    Widgets.Transitions.show($("#id_scoreboard_loading"));
    
    $.ajax({type: "POST", url: Utility.formatUrl("/scoreboard/"), data: {},
      success: function(json) {
        var response = JSON.parse(json);
        if (response.errors) {          
        }
        else {
          Widgets.Transitions.hide($("#id_scoreboard_loading"));
          
          State.scoreboard.processResults($("#id_scoreboard_all"), response.all);
          State.scoreboard.processResults($("#id_scoreboard_today"), response.today);
        }
      },
      error: function() {        
      }}
    );
    
    Widgets.Transitions.fadeIn($("#id_scoreboard"));    
  },
  leave: function() {
    Widgets.Transitions.fadeOut($("#id_scoreboard"));
  },
  processResults: function(element, data) {
    if ($.isArray(data)) {
      var sb = new StringBuilder();
      
      for (var i = 0; i < data.length; ++i) {
        var d = data[i];
        if (d == null)
          continue;
        
        sb.appendList('<div class="scoreLine"><div class="n">', $.encode(d.n), '</div><div class="s">', $.encode(d.s), '</div></div>');        
      }
      
      element.html(sb.toString());
    }
  }
};

////////////////////////////////////////////////////
// Game module.
State.game = { _visual: null,
  enter: function() {
    Widgets.Transitions.fadeIn($("#id_lobby"));
    
    this._visual = new Visual(320, 320);
  },
  leave: function() {
    Widgets.Transitions.fadeOut($("#id_lobby"));
    
    if (this._visual)
      this._visual.dispose();
  },
  mousedown: function(x, y) {
    if (this._visual)
      this._visual.inputStart(x, y);
  },
  mousemove: function(x, y) {
    if (this._visual)
      this._visual.inputMove(x, y);
  },
  mouseup: function(x, y) {
    if (this._visual)
      this._visual.inputEnd(x, y);
  },
  visual: function() {
    return this._visual;
  }
};

////////////////////////////////////////////////////
// Game main state.
State.game.game = {  
  enter: function() {
    
  },
  leave: function() {
    
  }
}

////////////////////////////////////////////////////
// Game end.
State.game.end = {  
  enter: function() {
    var v = State.game._visual;
    
    var message = "Game Over";
      
    $("#id_end_reason").html(message);
    
    Widgets.Transitions.fadeIn($("#id_end"));
  },
  leave: function() {
    Widgets.Transitions.fadeOut($("#id_end"));
  }
}

////////////////////////////////////////////////////
// Utilities.

var Utility = {
  formatUrl: function(requestUrl) {
    var location = window.location.protocol + "//" + window.location.host;
    
    return location + SITE_PATH + requestUrl.substring(1);
  },
  formatName: function(name, anonymizeGuest) {
    if (name == null)
      return "";
    if (anonymizeGuest)
      return name.startsWith("__Guest") ? "Guest" : name;
    else
      if (name.startsWith("__"))
        return name.substring(2);
        
    return name;
  },
  unpackMessage: function(message) {
    return message.replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&");
  }
};

////////////////////////////////////////////////////
// Touch events.

function touchHandler(event)
{
  var touches = event.changedTouches, first = touches[0], type = "";
  
  if (first.target.id != "id_main_canvas")
    return;
  
  switch(event.type)
  {
    case "touchstart":
      State.game.mousedown(first.clientX, first.clientY);
      break;
    case "touchmove":
      State.game.mousemove(first.clientX, first.clientY);
      break;
    case "touchend":
      State.game.mouseup(first.clientX, first.clientY);
      break;
    default: return;
  }
  
  event.preventDefault();
}

function initTouchHandlers()
{
    document.addEventListener("touchstart", touchHandler, true);
    document.addEventListener("touchmove", touchHandler, true);
    document.addEventListener("touchend", touchHandler, true);    
} 

////////////////////////////////////////////////////
// Initialization.

$(function() {  
  // Kick off preloader.
  Widgets.load(function() {
    // Map default buttons for states.
    Widgets.map_default_button("game.end", "#action_end_continue");
    Widgets.map_default_button("comments", "#action_comments_send");
    
    // Patch up images after preloading.
    Widgets.FormBuilder.setBackground($("#id_menu"), Widgets.backgrounds.main);
    Widgets.FormBuilder.setBackground($("#id_scoreboard"), Widgets.backgrounds.scores);
    Widgets.FormBuilder.setBackground($("#id_help"), Widgets.backgrounds.help);
    Widgets.FormBuilder.setBackground($("#id_comments"), Widgets.backgrounds.comments);
    
    ////////////////////////////////////////////////////
    // Main frame.
    (new Widgets.FormBuilder({x: 0, y: 0, width: 320, height: 356 }))
      .visible(false).container($("#frame_lobby")).sizeContainer()
    ;
    
    ////////////////////////////////////////////////////  
    // Main menu.
    (new Widgets.FormBuilder({x: 0, y: 0, width: 320, height: 356, container: $("#id_menu") }))
      .sizeContainer()
      .size(140, 28).position(90, 100).button($("#action_play"), function() {
        State.go("game.game");
      })
      .offset(0, 40).button($("#action_hiscores"), function() {
        State.go("scoreboard");
      })
      .offset(0, 40).button($("#action_help"), function() {
        State.go("help");
      })
      .offset(0, 40).button($("#action_comments"), function() {
        State.go("comments");
      })
      .offset(0, 50).button($("#action_more"), function() {
        window.location = "http://www.underclouds.com";
      })
      .size(300, 30).position(10, 315).multiline($("#id_menu-copyright"), "tiny", "center")
    ;
      
    ////////////////////////////////////////////////////  
    // Scoreboard.
    (new Widgets.FormBuilder({x: 0, y: 0, width: 320, height: 356, container: $("#id_scoreboard") }))
      .sizeContainer()
      .size(150, 60).position(5, 60).place($("#id_scoreboard_all"))
      .offset(160, 0).place($("#id_scoreboard_today"))
      .size(140, 28).position(90, 160).text($("#id_scoreboard_loading"), "main", "center")
      .size(140, 28).position(90, 320).button($("#action_scoreboard_continue"), function() {
        State.go("intro");
      })
    ;
    
    ////////////////////////////////////////////////////  
    // Help.
    (new Widgets.FormBuilder({x: 0, y: 0, width: 320, height: 356, container: $("#id_help") }))
      .sizeContainer()
      .size(205, 60).position(105, 65).multiline($("#id_help_start"))
      .size(205, 90).position(105, 140).multiline($("#id_help_draw"))
      .size(205, 90).position(105, 250).multiline($("#id_help_warning"))
      .size(140, 28).position(90, 320).button($("#action_help_continue"), function() {
        State.go("intro");
      })
    ;
    
    ////////////////////////////////////////////////////  
    // Comments.
    (new Widgets.FormBuilder({x: 0, y: 0, width: 320, height: 356, container: $("#id_comments") }))
      .sizeContainer()
      .size(300, 24).position(10, 43).multiline($("#id_comments_label"), "hint", "center")
      .size(100, 28).position(10, 220).text($("#id_comments-name_label"), "main", "left")
      .size(100, 28).position(5, 248).input($("#id_comments-name"))
      .size(205, 28).position(110, 220).text($("#id_comments-comment_label"), "main", "left")
      .size(205, 28).position(105, 248).input($("#id_comments-comment"))
      .size(140, 28).position(90, 285).button($("#action_comments_send"), function() {
        Widgets.MessageBox.loading("Sending...");
        
        $.ajax({type: "POST", url: Utility.formatUrl("/comment/"), data: { name: $("#id_comments-name").val(), comment: $("#id_comments-comment").val() },
          success: function(json) {
            var response = JSON.parse(json);
            if (response.errors)
              Widgets.MessageBox.error(response.errors);
            else {
              Widgets.MessageBox.hide();
              
              State.go("intro");
            }
          },
          error: function() {
            Widgets.MessageBox.error("Server is busy. Please try again.");            
          }}
        );
      })
      .size(140, 28).position(90, 320).button($("#action_comments_back"), function() {
        State.go("intro");
      })
      .size(310, 140).position(5, 77).place($("#id_comments_last"))
    ;     
    
    ////////////////////////////////////////////////////  
    // Main lobby.
    (new Widgets.FormBuilder({x: 0, y: 0, width: 320, height: 356, container: $("#id_lobby") }))
      .sizeContainer()
      .size(124, 24).position(160, 8).multiline($("#id_drag_label"), "hint", "right")
      .size(120, 24).position(32, 8).text($("#id_score_label"), "main")      
      .position(290, 8).image("drag")
      .position(4, 8).image("menu", function() {
        State.go("intro");
      })
      .position(0, 35).placePosition($("#id_main_canvas"))
    ;
      
    (new Widgets.FormBuilder({x: 0, y: 0, width: 320, height: 356, container: $("#id_end") }))
      .sizeContainer()
      .size(300, 50).position(10, 100).text($("#id_end_reason"), "large", "center")
      .size(250, 28).position(30, 170).text($("#id_name-name_label"), "main", "center")
      .size(200, 28).offset(30, 28).input($("#id_name-name"))
      .size(140, 28).offset(30, 40).button($("#action_end_continue"), function() {
        createCookie("name", $("#id_name-name").val(), 365);
        
        $("#id_comments-name").val($("#id_name-name").val());
        
        $.ajax({type: "POST", url: Utility.formatUrl("/score/"), data: { name: $("#id_name-name").val(), score: State.game.visual().score() },
          success: function(json) {
          },
          error: function() {            
          }}
        );
        
        State.go("intro");
      })
    ; 
      
    $("#id_main_canvas").mousedown(function(e) {
      var p = Widgets.mousePosition(this, e);
      
      State.game.mousedown(p.x, p.y);
    });
    $("#id_main_canvas").mousemove(function(e) {
      var p = Widgets.mousePosition(this, e);
      
      State.game.mousemove(p.x, p.y);
    });
    $("#id_main_canvas").mouseup(function(e) {
      var p = Widgets.mousePosition(this, e);
      
      State.game.mouseup(p.x, p.y);
    });
    
    // Setup touch events.
    if (Widgets.isApple)
      initTouchHandlers();
    
    // Begin game.
    State.go("launch");
    
    var name = readCookie("name");

    if (name && name.length > 0)
    {
      $("#id_name-name").val(name);
      $("#id_comments-name").val(name);    
    }
    else
      $("#id_name-name").val("");
      $("#id_comments-name").val("");
  });
});

////////////////////////////////////////////////////
// Physics.

function v_(x, y) { return {x:x, y:y}; }
function v_c(a) { return {x:a.x, y:a.y}; }
function v_add(a, b) { return {x:a.x + b.x, y:a.y + b.y}; }
function v_sub(a, b) { return {x:a.x - b.x, y:a.y - b.y}; }
function v_mul(a, b) { return {x:a.x * b, y:a.y * b}; }
function v_madd(a, b, c) { return {x:a.x + b.x * c, y: a.y + b.y * c}; }
function v_madd_ref(a, b, c) { a.x += b.x * c; a.y += b.y * c; }
function v_dot(a, b) { return a.x * b.x + a.y * b.y; }
function v_len(a) { return Math.sqrt(v_dot(a, a)); }
function v_nor(a) { var inv_len = 1.0 / Math.sqrt(a.x * a.x + a.y * a.y); return {x:a.x * inv_len, y:a.y * inv_len}; }
function v_nor_2(a, b) { var inv_len = 1.0 / Math.sqrt(a * a + b * b); return {x:a * inv_len, y:b * inv_len}; }
function p_(x, y, z) { return {x:x, y:y, z:z}; }
function p_dot(p, v) { return p.x * v.x + p.y * v.y + p.z; }
function v_crs(a) { return {x:-a.y, y:a.x}; }
function v_dist(a, b) { return Math.sqrt((a.x - b.x) * (a.x - b.x) + (a.y - b.y) * (a.y - b.y)); }

var instance_type = { ball: 1, wall: 2 };

function ph_plane_sphere(plane, begin, delta, radius) {
  var n_dot_d = plane.x * delta.x + plane.y * delta.y;
  var dist_b = plane.x * begin.x + plane.y * begin.y + plane.z;

  var ta, tb;
  
  if (dist_b < 0)
    return null;

  if (n_dot_d == 0.0) {
    if (Math.abs(dist_b) > radius)
      return null;

    ta = 0;
    tb = 1;
  } else {
    ta = (radius - dist_b) / n_dot_d;
    tb = (-radius - dist_b) / n_dot_d;

    if (ta > tb) { var tc = ta; ta = tb; tb = tc; }      
    if (ta > 1 || tb < 0) return null;
    if (ta < 0) ta = 0;
    if (tb > 1) tb = 1;
  }  

  return v_(ta, tb);
}

function ph_1d_intersect(line_begin, line_end, center, radius_squared) {  
  if (center > line_begin && center < line_end)
    return center;

  var d_begin = (center - line_begin) * (center - line_begin);
  if (d_begin < radius_squared)
    return line_begin;

  var d_end = (center - line_end) * (center - line_end);
  if (d_end < radius_squared)
    return line_end;

  return null
}		

function root(a, b, c, bound) {
  var determinant = b * b - 4.0 * a * c;
  if (determinant < 0.0)
    return null;

  var q = -0.5 * (b + (b < 0.0 ? -1.0 : 1.0) * Math.sqrt(determinant));

  var x1 = q / a;
  var x2 = c / q;

  if (x2 < x1) { var x3 = x1; x1 = x2; x2 = x3; }

  if (x1 >= 0.0 && x1 <= bound)
    return x1;
  if (x2 >= 0.0 && x2 <= bound)
    return x2;

  return null;
}

function ph_1d_swept_intersect(line_begin, line_end, begin, delta, a, b, c) {
  var upper_bound = 1.0;
  var collision = false;
  var point = null;

  var b_to_v = line_begin - begin;
  var a1 = a - delta * delta;
  var b1 = b + 2.0 * delta * b_to_v;
  var c1 = c - b_to_v * b_to_v;

  var t = root(a1, b1, c1, upper_bound);
  if (t) {
    collision = true;
    upper_bound = t;
    point = line_begin;
  }

  b_to_v = line_end - begin;
  a1 = a - delta * delta;
  b1 = b + 2.0 * delta * b_to_v;
  c1 = c - b_to_v * b_to_v;

  var t = root(a1, b1, c1, upper_bound);
  if (t) {
    collision = true;
    upper_bound = t;
    point = line_end;
  }

  return collision ? v_(upper_bound, point) : null;
}

function ph_line_sphere(line_plane, line_begin, line_end, begin, delta, radius) {
  var t = ph_plane_sphere(line_plane, begin, delta, radius);
  if (t == null)
    return null;

  var n_dot_d = v_dot(line_plane, delta);
  var dist_b = p_dot(line_plane, begin);
  var a = -n_dot_d * n_dot_d;
  var b = -2 * n_dot_d * dist_b;
  var c = radius * radius - dist_b * dist_b;

  var u = v_crs(line_plane);
  var begin_plane = v_dot(u, begin);
  var delta_plane = v_dot(u, delta);

  var p = ph_1d_intersect(line_begin, line_end, begin_plane + delta_plane * t.x, a * t.x * t.x + b * t.x + c);
  if (p) {
    return v_(t.x, 
      v_(u.x * p - line_plane.x * line_plane.z, u.y * p - line_plane.y * line_plane.z));
  }

  var p = ph_1d_swept_intersect(line_begin, line_end, begin_plane, delta_plane, a, b, c);
  if (p) {
    return v_(p.x, 
      v_(u.x * p.y - line_plane.x * line_plane.z, u.y * p.y - line_plane.y * line_plane.z));
  }

  return null;
}

function ph_ray_sphere(ray_start, ray_direction, length, center, radius) {
  var offset_x = center.x - ray_start.x;
  var offset_y = center.y - ray_start.y;
  var ray_distance = ray_direction.x * offset_x + ray_direction.y * offset_y;
  if (ray_distance <= 0 || (ray_distance - length) > radius)
    return null;

  var offset_squared = offset_x * offset_x + offset_y * offset_y;
  var radius_squared = radius * radius;
  if (offset_squared <= radius_squared)
    return v_(0.0, ray_start);
  var d = radius_squared - (offset_squared - ray_distance * ray_distance);
  if (d < 0)
    return null;
  var fraction = ray_distance - Math.sqrt(d);
  if (fraction > length)
    return null;

  return v_(fraction / length, v_madd(ray_start, ray_direction, fraction));
}

function ph_sphere_sphere(static_center, static_radius, begin, delta, radius)	{
  var length = v_len(delta);

  if (length == 0.0)
    return null;

  var t = ph_ray_sphere(begin, v_mul(delta, 1.0 / length), length, static_center, static_radius + radius);
  if (t) {
    var intersection_point = v_madd(begin, delta, t.x);
    var d = v_sub(static_center, intersection_point);

    return v_(t.x, v_madd(intersection_point, d, radius));
  } else 
    return null;
}

function ph_moving_spheres(begin_a, delta_a, radius_a, begin_b, delta_b, radius_b) {
  var a_to_b_x = begin_b.x - begin_a.x;
  var a_to_b_y = begin_b.y - begin_a.y;
  var delta_ab_x = delta_b.x - delta_a.x;
  var delta_ab_y = delta_b.y - delta_a.y;
  
  var radius = radius_a + radius_b;

  var c = a_to_b_x * a_to_b_x + a_to_b_y * a_to_b_y - radius * radius;

  if (c <= 0.0) {
    var t = radius_a / radius;
    return v_(0.0, v_(begin_a.x * (1.0 - t) + begin_b.x * t, begin_a.y * (1.0 - t) + begin_b.y * t));
  }

  var b = 2.0 * (delta_ab_x * a_to_b_x + delta_ab_y * a_to_b_y);
  if (b >= 0.0)
    return null;

  var a = delta_ab_x * delta_ab_x + delta_ab_y * delta_ab_y;

  var t = root(a, b, c, 1.0);	
  if (t) {
    var begin_a_time_x = begin_a.x + delta_a.x * t;
    var begin_a_time_y = begin_a.y + delta_a.y * t;
    var begin_b_time_x = begin_b.x + delta_b.x * t;
    var begin_b_time_y = begin_b.y + delta_b.y * t;

    var f = radius_a / radius;

    return v_(t, v_(begin_a_time_x * (1.0 - f) + begin_b_time_x * f, begin_a_time_y * (1.0 - f) + begin_b_time_y * f));
  }

  return null;
}

var _walls = [];

function add_wall(x1, y1, x2, y2) {
  var a = v_(x1, y1);
  var b = v_(x2, y2);
  var n = v_crs(v_nor(v_sub(b, a)));
  var d = -v_dot(n, a);

  var u = v_crs(n);

  var ua = v_dot(u, a);
  var ub = v_dot(u, b);

  if (ua > ub) { var uc = ua; ua = ub; ub = uc; }

  _walls.push({type: instance_type.wall, normal:v_(n.x, n.y), p:p_(n.x, n.y, d), b:ua, e: ub, v: v_(0.0, 0.0), m: 0.0, f: v_(0.0, 0.0), center: v_((x1 + x2) * 0.5, (y1 + y2) * 0.5) });
}

var _balls = [];

function add_ball(id, center, radius, mass) {
  var b = {type: instance_type.ball, c:center, r:radius, v:v_(0, 0), f:v_(0, 0), m:1.0 / mass, id: id, active: true, dampening: -50 };

  _balls.push(b);

  return b;
}

var _time;

function reset_simulation() {
  _time = 0.0;
  _walls = [];
  _balls = [];
}

var _pairs = [];

function step_simulation(step) {
  target = _time + step;

  _pairs.length = 0;
  
  while (_time < target) {
    var was_collision = false;  

    var obj_one = null;
    var obj_two = null;
  
    var seconds = target - _time;

    for (var i = 0; i < _balls.length; ++i) {
      _balls[i].delta = v_mul(_balls[i].v, seconds);
    }

    var collision_time = null;

    for (var current_ball = 0; current_ball < _balls.length; ++current_ball) {
      var ball = _balls[current_ball];      

      if (!ball.active)
        continue;

      var stopped = ball.v.x == 0.0 && ball.v.y == 0.0;

      var delta = ball.delta;
      
      var bcx = ball.c.x;
      var bcy = ball.c.y;
      var bvx = ball.v.x;
      var bvy = ball.v.y;
      
      if (!stopped) {
        for (var i = 0; i < _walls.length; ++i) {
          if (i == 0 && (bcy > 60 || bvy > 0))
            continue;
          if (i == 1 && (bcx < 240 || bvx < 0))
            continue;
          if (i == 2 && (bcy < 240 || bvy < 0))
            continue;
          if (i == 3 && (bcx > 60 || bvx > 0))
            continue;  
          
          var w = _walls[i];      
      
          var t = ph_line_sphere(w.p, w.b, w.e, ball.c, delta, ball.r);
          if (t) {
            var valid = false;
      
            var intersection_time = t.x * seconds;
            if (intersection_time == 0.0) {
              var normal_direction = v_sub(ball.c, t.y);
              if (v_dot(ball.v, normal_direction) < 0.0)
                valid = true;
            } else
              valid = true;
      
            if (valid) {
              if (!collision_time || intersection_time <= collision_time) {
                if (collision_time != intersection_time)
                  _pairs.length = 0;
      
                collision_time = intersection_time;
      
                _pairs.push({one: ball, two: w, v_one: v_c(ball.v), v_two: v_c(w.v), point: t.y, normal: v_nor_2(ball.c.x + ball.v.x * collision_time - t.y.x, ball.c.y + ball.v.y * collision_time - t.y.y) });
              }
            }
          }
        }
      }

      var zero = v_(0.0, 0.0);

      for (var i = current_ball + 1; i < _balls.length; ++i) {
        var another_ball = _balls[i];
        if (!another_ball.active)
          continue;

        if (stopped && another_ball.v.x == 0 && another_ball.v.y == 0)
          continue;

        var center_x = another_ball.c.x - ball.c.x;        
        var center_y = another_ball.c.y - ball.c.y;
        if (center_x * center_x + center_y * center_y > 1600)
          continue;

        var t = ph_moving_spheres(ball.c, delta, ball.r, another_ball.c, another_ball.delta, another_ball.r);
        if (t) {
          var valid = false;

          var intersection_time = t.x * seconds;
          if (intersection_time == 0.0) {
            var a_to_b_x = ball.c.x - another_ball.c.x;
            var a_to_b_y = ball.c.y - another_ball.c.y;
            var rel_v_x = ball.v.x - another_ball.v.x;
            var rel_v_y = ball.v.y - another_ball.v.y;           

            if ((a_to_b_x * rel_v_x + a_to_b_y * rel_v_y) < - 0.01)
              valid = true;
          } else
            valid = true;

          if (valid) {
            if (!collision_time || intersection_time <= collision_time) {
              if (collision_time != intersection_time)
                _pairs.length = 0;

              collision_time = intersection_time;

              _pairs.push({one: ball, two: another_ball, v_one: v_c(ball.v), v_two: v_c(another_ball.v), point: t.y, normal: v_nor_2(ball.c.x + ball.v.x * collision_time - another_ball.c.x - another_ball.v.x * collision_time, ball.c.y + ball.v.y * collision_time - another_ball.c.y - another_ball.v.y * collision_time) });
            }
          }
        }
      }      
    }

    if (collision_time != null)
      was_collision = true;

    // Integrate up to the moment of collision.
    var step_time = collision_time != null ? collision_time : seconds;

    for (var i = 0; i < _balls.length; ++i) {
      var ball = _balls[i];

      if (!ball.active)
        continue;

      v_madd_ref(ball.c, ball.v, step_time);
      v_madd_ref(ball.v, ball.f, step_time * ball.m);
    }

    if (collision_time != null && _pairs.length > 0) {
      // Check for identical points.
      for (var i = 0; i < _pairs.length; ++i) {
        var a = _pairs[i];
        for (var j = i + 1; j < _pairs.length; ++j) {
          var b = _pairs[j];

          if (b.skip)
            continue;
          if (a.point.x == b.point.x && a.point.y == b.point.y && a.normal.x == b.normal.x && a.normal.y == b.normal.y)
            b.skip = true;
        }
      }

      for (var i = 0; i < _pairs.length; ++i) {
        var v = _pairs[i];
        if (v.skip)
          continue;
           
        var obj_one = v.one;
        var obj_two = v.two;

        var c = -1.9;

        if (obj_two.type == instance_type.wall) 
          c = -1.7;
        
        var impulse = v_mul(v.normal, c * v_dot(v_sub(v.v_one, v.v_two), v.normal) / (obj_one.m + obj_two.m));

        v_madd_ref(obj_one.v, impulse, obj_one.m);
        v_madd_ref(obj_two.v, impulse, - obj_two.m);

        obj_one.f = v_(0.0, 0.0);
        obj_two.f = v_(0.0, 0.0);
        
        if (on_collision)
          on_collision(obj_one, obj_two);
      }
    }

    _time += step_time;
  }  

  var primary = _balls[0];

  for (var i = 0; i < _balls.length; ++i) {
    var ball = _balls[i];
    if (!ball.active)
      continue;

    ball.f = v_(0.0, 0.0);

    var velocity = ball.v;
    var vel = v_dot(velocity, velocity);
    if (vel > 3) {
      velocity = v_nor(velocity);
      ball.f.x += velocity.x * ball.dampening;
      ball.f.y += velocity.y * ball.dampening;
    } else {
      ball.v = v_(0.0, 0.0);
    }
  }
}

