document.observe("dom:loaded",function(){
  "image_links saving main_grid piece_selector editor grid_editor animation reset_button resume_button pause_button go_button selector_button select_done_button".split(/\s+/).each(function(id){
      TetrisUI[id] = $(id);
  });
  $("side_panel").setOpacity(0);

  tiles=get_tiles();
  $("tetris").select("td.tet").each(observe_tile);
  var cont = $("piece_category_small_pieces");
  $("piece_category_links").select(".tab").first().onclick();
  // Switches to piece_selector immediately
  // $("selector_button").onclick();
  TetrisUI.hide_persistence();

  build_default_piece_selection()
  if(!$("saved_grid"))select_tetrominoes();
  new Effect.Opacity("side_panel",{from:0.0, to:1.0, duration: 0.5});
  recompute_tiling_numbers();
  reassess();
  if($("saved_grid"))load_saved();
});

var sizex=8;
var sizey=8;
var wrapx=false;
var wrapy=false;
var active=60;
var visited;
var editing=true;
var paused;
var tiles;
var ds=[[-1,0],[0,-1],[0,1],[1,0]];
var tiling_numbers={};
var DEFAULT_COLORS = ["#225588",
                      "#C93C3C",
                      "#996633",
                      "#8899BB",
                      "#ECD959",
                      "#9944CC",
                      "#44CC44"];
var BORDER_COLOR = "#000000";


function observe_tile(tile){
  tile.observe("click", active_toggle);
  tile.observe("mouseover", function(ev){
      if(ev.shiftKey==1)active_toggle.bind(this)();
  });
}

function contracty(skip_post){
  if(sizey == 1)return;
  active-=$("tetris").select("tr")[sizey-1].remove().select("td").select(function(td){return ! td.hasClassName("blank")}).length;
  sizey--;
  if(!skip_post){
    tiles=get_tiles();
    reassess();
  }
}

function expandy(skip_post){
  active+=sizex;
  sizey++;
  var tet=$("tetris").select("tbody").first();
  var newRow = Builder.node("tr",$R(1,sizex).map(function(){return Builder.node("td",{className: "tet"},Builder.node("div",{className: "filler"}));}).each(observe_tile));
  tet.appendChild(newRow);
  if(!skip_post){
    tiles=get_tiles();
    reassess();
  }
}

function contractx(skip_post){
  if(sizex == 1)return;
  var rows = $("tetris").select("tr");
  var tds = rows.map(function(tr){return tr.select("td").last();});
  active-=tds.select(function(td){return ! td.hasClassName("blank")}).length;
  tds.each(Element.remove);
  sizex--;
  if(!skip_post){
    tiles=get_tiles();
    reassess();
  }
}

function expandx(skip_post){
  var new_square=function(){
    return td=Builder.node("td",{className: "tet"},Builder.node("div",{className:"filler"}));
  $("total").innerHTML=active;
  };
  active+=sizey;
  sizex++;
  var trs = $("tetris").select("tr");
  trs.each(function(tr){
      ted=new_square();
      tr.appendChild(ted);
      observe_tile(ted);
    });
  if(!skip_post){
    tiles=get_tiles();
    reassess();
  }
}

function dimensions_dialog(){
  var element = Builder.node("div",
      Builder.node("table",
       [Builder.node("tr",
         [Builder.node("td","Width:"),Builder.node("input",{style:"width:50px;",maxLength:"2"})]),
        Builder.node("tr",
         [Builder.node("td","Height:"),Builder.node("input",{style:"width:50px;",maxLength:"2"})]),
        Builder.node("tr",[Builder.node("td"),Builder.node("td",Builder.node("button","Resize"))])]));
  var closer = TetrisUI.popup("Input Dimensions",element);
  element.select("button")[0].observe("click",function(){
    var x = parseInt(element.select("input")[0].value);
    var y = parseInt(element.select("input")[1].value);
    closer();
    if(x < 1)x=1;if(y<1)y=1;
    var funx = [];
    if(sizex > x)$R(x+1,sizex).each(function(){funx.push(contractx)});
    if(sizex < x)$R(sizex+1,x).each(function(){funx.push(expandx)});
    if(sizey > y)$R(y+1,sizey).each(function(){funx.push(contracty)});
    if(sizey < y)$R(sizey+1,y).each(function(){funx.push(expandy)});
    funx = funx.sortBy(Math.random);
    var step = function(){
      if(funx.length > 0){
        funx.shift()(true);
        step.defer();
      }
      else {
        tiles = get_tiles();
        reassess();
      }
    }
    step.defer();
  });
}

function reassess(){
  reset_tiles("cval");
  var remaining = active;
  comps = [];
  while(remaining > 0){
    var cc = get_connected_component();
    comps.push(cc);
    remaining-=cc.length;
  }
  comps = comps.sortBy(function(a){return a.length}).reverse();
  $("connected").childElements().each(Element.remove);
  var i=0;
  var nodes = comps.map(function(c){
      return Builder.node("span", {className: "component "+(is_tileable(c.length) ? "good" : "bad"), id: "comp"+(i++)}, ""+c.length);
  });
  var n1=nodes.clone();
  var more_nodes = [nodes.shift()];
  while(nodes.length > 0){
    more_nodes.push(Builder.node("span", " | "));
    more_nodes.push(nodes.shift());
  }
  $("connected").appendChild(Builder.node("span", more_nodes));
  i=0;
  n1.each(function(n){
      n.num=i++;
      var f=function(ff){
        comps[n.num].each(function(el){ff(el,"highlighted")});
      };
      n.observe("mouseover",f.curry(Element.addClassName));
      n.observe("mouseout",f.curry(Element.removeClassName));
  });
  TetrisUI.show_persistence("grid");
  return comps.all(function(c){return is_tileable(c.length)});
}


function active_toggle(skip_reassess){
  if(!editing)return;
  this.toggleClassName("blank");
  if(this.hasClassName("blank")){
    active--;
    this.setStyle({backgroundColor:"#000"});
  }
  else{
    active++;
    this.setStyle({backgroundColor:"inherit"});
  }
  if(skip_reassess!=true)
    reassess();
}

function get_tiles(){
  var trs=$A($("tetris").select("tr")).reverse();
  trs=trs.map(function(tr){return tr.select("td.tet");});
  /* invert */
  var t=[];
  while(trs.first().length > 0)t.push(trs.map(function(a){return a.shift()}));
  return t;
}

function reset_tiles(att){
  tiles.each(function(row){
      row.each(function(td){
        td[att] = td.hasClassName("blank") ? -1 : 0;
      });
  });
}

function get_zero(att){
  for(var j=0;j<sizey;j++){
    for(var i=0;i<sizex;i++){
      if(tiles[i][j][att]==0){
        return [i,j];
      }
    }
  }
}

function get_connected_component(){
  // Breadth first search
  var tmp=get_zero("cval");
  var x=tmp[0];
  var y=tmp[1];
  tiles[x][y].cval=1;
  var queue=[[x,y]];
  var rets = [tiles[x][y]];
  var now;
  while(queue.length > 0){
    now=queue.shift();
    ds.each(function(d){
        var i=now[0]+d[0];
        var j=now[1]+d[1];
        if(wrapx)i=(i+sizex)%sizex;
        if(wrapy)j=(j+sizey)%sizey;
        if(i>=0 && j>=0 && i<sizex && j<sizey && tiles[i][j].cval==0){
          tiles[i][j].cval=1;
          rets.push(tiles[i][j]);
          queue.push([i,j]);
        }
      });
  }
  return rets;
}

function pause(){
  paused=true;
  TetrisUI.pause();
}

function resume(){
  TetrisUI.resume();
  paused=false;
  going();
}

function start(){
  // TODO: This does not check the size of separated components
  if(!is_tileable(active)){
    TetrisUI.flash("Total square count is not tileable with current piece set!");
    return;
  }
  editing=false;
  TetrisUI.going_mode();
  TetrisUI.hide_persistence();
  reset_tiles("val");
  var available=new Hash();
  visited=0;
  paused=false;
  /* initialize available */
  set_tile_attributes();
  for(var i=0;i<sizex;i++){
    for(var j=0;j<sizey;j++){
      var t=tiles[i][j];
      if(t.val==0)
        available.set(t.tile_id,{tile:t, options:[]});
    }
  }
  compute_neighbors();
  active_options=[];
  component = new Component(available,complete.curry(true),complete.curry(false));
  get_all_options(available,going);
}

function set_tile_attributes(){
  var s=0;
  for(var j=0;j<sizey;j++){
    for(var i=0;i<sizex;i++){
      var t=tiles[i][j];
      t.posx=i;
      t.posy=j;
      t.tile_id=(s++);
    }
  }
}

function Component(squares,success,failure){
  this.stack=[];
  this.available=squares;
  this.taken=$H();
  this.success=success;
  this.failure=failure;
}

Component.registered_failures = $H();
Component.register_failure=function(c){
  var h = c.hash();
  if(!Component.registered_failures[h] && Component.registered_failures.keys().length > 500){
    var earliest = Component.registered_failures.keys().sortBy(function(k){
        return Component.registered_failures[k];
      }).first();
    Component.registered_failures.unset(earliest);
  }
  Component.registered_failures[c.hash()]=(new Date()).valueOf(); 
}

Component.prototype.unwind = function(){
  if(this.subs)this.subs.each(function(s){s.unwind()});
  while(this.stack.length > 0){
    var stack_item = this.stack.pop();
    this.unimplement(stack_item.options.first(),stack_item.removed);
  }
}

Component.prototype.sub_components=function(){
  /* Returns an array of Hashes that could be passed to a Component constructor;
     If this component is connected, array will have only one hash
   */
  var vs = this.available.values();
  var left = this.available.clone();
  comps = [];
  var ks,queue,item,id,hsh,x,y,t;
  while((ks=left.keys()).length > 0){
    hsh=$H();
    comps.push(hsh);
    queue=[id=ks.first()];
    hsh.set(id,left.unset(id));
    while(queue.length > 0){
     // t = hsh.get(queue.shift()).tile;
      neighbors[queue.shift()].each(function(nid){
        if(left.get(nid)){
          queue.push(nid);
          hsh.set(nid,left.unset(nid));
        }
      });
    }
  }
  return comps;
}

Component.prototype.get_bridges=function(){
  /*
   * Returns an array of [tile_id, weight] where tile_id represents a tile
   *   that is a gatekeeper between two otherwise disconnected components,
   *   and weight is the size of the smaller of the two (thus indicating the
   *   importance of that particular gatekeeper)
   */
  var left = this.available.clone();
  var modules = [];
  while(left.keys().length > 0){
    var module = $H();
    modules.push(module);
    var k1 = left.keys().first();
    module.set(k1,left.unset(k1));
    var conns=$R(1,modules.length-1).map(function(){return 0});
    module.set("connections",conns);
    var primers = neighbors[k1].select(function(k){return left.get(k)});
    if(primers.length==0){
      /* This tile is isolated */
      var val;
      var justone=0;
      neighbors[k1].each(function(k){
        $R(0,modules.length-2).each(function(mi){
          if(modules[mi].get(k))
          {
            conns[mi]++;
            justone++;
            val=mi;
          }
        });
      });
      if(justone==1){
        modules.pop();
        modules[val].set(k1,module.get(k1));
      }
      continue;
    }
    var k2=primers.first();
    var border = $H();
    var queue=[k1,k2];
    while(queue.length > 0){
      while(queue.length > 0){
        var node = queue.shift();
        neighbors[node].each(function(neighbor){
          if(left.get(neighbor))
            border.set(neighbor,left.unset(neighbor));
          else if(border.get(neighbor))
          {
            module.set(neighbor,border.unset(neighbor));
            queue.push(neighbor);
          }
          else{
            $R(0,modules.length-2).each(function(mi){
              if(modules[mi].get(neighbor))
                conns[mi]++;             
            });
          }
        });
      }
      var maybes=border.keys().concat(border.keys().map(function(k){return neighbors[k]})).flatten().sort();
      while(maybes.length > 1){
        if(maybes[0]!=maybes[1])maybes.shift();
        else
        {
          var k = maybes[0];
          while(maybes.first()==k)maybes.shift();
          queue.push(k);
          if(border.get(k))
            module.set(k,border.unset(k));
          else if(left.get(k))
            module.set(k,left.unset(k));
          else queue.pop();
        }
      }
    }
    border.keys().each(function(k){left.set(k,border.unset(k));});
  }
  var ob = merge_graph(modules.map(function(modu){return modu.unset("connections")}));
  var nodes = ob.assignments.uniq();
  if(nodes.length==1)return []; // only one module, thus no bridges
  for(var i=0;i<modules.length;i++){
    if(ob.assignments[i]!=i){
      var m=modules[ob.assignments[i]];
      modules[i].keys().each(function(k){
          m.set(k,modules[i].unset(k));
      });
      modules[i]=undefined;
    }
  }

  // Build simple tree structure
  var conn = ob.connections;
  var leaves = nodes.select(function(n){return conn.flatten().without(n).length == (2*conn.length-1)});
  var root = leaves.first();
  var conn2 = conn.clone();
  var build_tree=function(t){
    var childs = conn2.select(function(a){return a.include(t);});
    childs.each(function(c){conn2 = conn2.without(c);});
    childs = childs.map(function(a){return a.without(t).first();});
    return [t].concat(childs.map(function(c){return build_tree(c)}));
  }
  var tree=build_tree(root);
  var total = this.available.keys().length;
  var process_tree = function(t){
    t.slice(1).each(function(c){
      var s = c.flatten().map(function(m){return modules[m].keys().length}).inject(0,function(acc,n){return acc+n;});
      var sorted = [t.first(),c.first()].sort();
      for(var i = 0; i < conn.length;i++){
        if(conn[i].length==2){
          var sorted2=conn[i].sort();
          if(sorted[0]==sorted2[0] && sorted[1]==sorted2[1])
            conn[i].push([s,total-s].min());
        }
      }
      process_tree(c);
    });
  }
  process_tree(tree);

  conn = conn.sortBy(function(a){return -1*a[2]});

  var ret=conn.map(function(a){
    var mod1=modules[a[0]];
    var mod2=modules[a[1]];
    if(mod1.keys().length > mod2.keys().length){
      var t=mod1;
      mod1=mod2;
      mod2=t;
    }
    var candidates1 = mod1.keys().select(function(k){
      return neighbors[k].any(function(kk){return mod2.get(kk)})});
    if(candidates1.length==1)return [candidates1[0],a[2]];
    var candidates2 = mod2.keys().select(function(k){
      return neighbors[k].any(function(kk){return mod1.get(kk)})});
    if(candidates2.length==1)return [candidates2[0],a[2]];
  
    // We should never execute this, but just in case I think this command will be a more graceful
    //   failure that returning undefined
    return [(candidates1.concat(candidates2))[0],a[2]];
  });
  
  return ret;
}

function merge_graph(adjacency){
  /* Takes a 2-d adjacency matrix of a graph, and merges all cycles and all pairs with
     edge weight higher than 1; returns an array of equivalences. E.g., [0,1,2,3,1,5] means
     that 1 and 4 should be merged.*/
  var l=adjacency.length;
  var ret=$A($R(0,l-1));
  var total = adjacency.length;
  // Make a full matrix instead of half
  $R(0,l-1).each(function(i){
    $R(i+1,l-1).each(function(j){
      adjacency[i][j]=adjacency[j][i];
    });
    adjacency[i][i]=0;
  });
  var merge = function(i,j){
    // merges i into j
    for(var k=0;k<l;k++){
      if(k==i||k==j)continue;
      adjacency[k][j]+=adjacency[k][i];
      adjacency[k][i]=0;
      adjacency[j][k]+=adjacency[i][k];
    }
    adjacency[j][i]=0;
    for(var k=0;k<l;k++)adjacency[i][k]=0;
    var z = ret[i];
    for(var k=0;k<l;k++)if(ret[k]==z)ret[k]=ret[j];
    total--;
  }
  while(total > 1){
    var s=total;
    // First merge all double edges
    for(var i=0;i<l;i++)
      for(var j=i+1;j<l;j++)
        if(adjacency[i][j]>1)
          merge(i,j);

    // Then detect cycles
    var visited=[];
    var root = ret[0];
    var get_path_to=function(n,t){
      if(t.first()==n)return [];
      for(var i=1;i<t.length;i++)
        if(t[i].flatten().include(n))
          return [t.first()].concat(get_path_to(n,t[i]));
    }
    var visit = function(n,p){
      visited[n]=true;
      var childs = $R(0,l).select(function(i){return adjacency[n][i];}).without(n,p);
      var cycles = childs.select(function(c){return visited[c]});
      if(cycles.length > 0)
      {
        return ["CYCLE",n, cycles.first()];
      }
      var child_trees = [];
      for(var i = 0;i<childs.length;i++){
        var ct = visit(childs[i],n);
        if(ct=="BREAK")return ct;
        if(ct.first()=="CYCLE"){
          var mergers;
          if(ct.last()==n)
            mergers=ct.slice(1);
          else if(child_trees.flatten().include(ct.last()))
          {
            /* Then n is the common ancestor */
            mergers = ct.slice(1).concat([n]).concat(get_path_to(ct.last(),child_trees));
          }
          else
            return ["CYCLE",n].concat(ct.slice(1));
          while(mergers.length > 1){
            merge(mergers[0],mergers[1]);
            mergers.shift();
          }
          return "BREAK";
        }
        else
        {
          child_trees.push(ct);
        }
      }
      var r= [n].concat(child_trees);
      return r;
    }

    visit(root,undefined); 

    if(total==s)break;
  }
  var conn = [];
  var nodes = ret.uniq();
  for(var i=0;i<nodes.length-1;i++)
    for(var j=i+1;j<nodes.length;j++)
      if(adjacency[nodes[i]][nodes[j]])conn.push([nodes[i],nodes[j]]);
  return {assignments: ret, connections: conn};
}

Component.prototype.next_tile=function(){
  var ks = this.available.keys();
  if(ks.length==0)return undefined;
  var min=ks.first();
  var minv=this.available.get(min).options.length;
  var k,v;
  for(var i=1;i<ks.length;i++){
    k=ks[i];
    v=this.available.get(k).options.length;
    if(v < minv || (v == minv && k < min))
    {
      min=k;
      minv=v;
    }
  }
  if(minv>1){
    var bridges = this.get_bridges();
    if(bridges.length > 0 && bridges[0][1] > 5){
      return bridges[0][0];
    }
  }
  return min;
}

var component,active_options,neighbors;

function compute_neighbors(){
  /* This function is only called by start(); it updates the neighbors array */
  neighbors = [];
  $R(0,sizex-1).each(function(x){
    $R(0,sizey-1).each(function(y){
      if(tiles[x][y].val > -1){
        var id = tiles[x][y].tile_id;
        neighbors[id]=[];
        ds.each(function(d){
          var i = x+d[0];
          if(wrapx)i=(i+sizex)%sizex;
          if(tiles[i]){
            var j = y+d[1];
            if(wrapy)j=(j+sizey)%sizey;
            if(tiles[i][j] && tiles[i][j].val > -1)
              neighbors[id].push(tiles[i][j].tile_id);
          }
        });
      }
    });
  });
}

function going(){
  if(!paused)
    normal();
}

function normal(){
  var sc_hashes = component.sub_components();
  if(sc_hashes.length==0){
    component.success();
    return;
  }
  var solvable = sc_hashes.all(function(sc){
    return is_tileable(sc.keys().length);
  });
  if(solvable){
    if(sc_hashes.length==1){
      var next=component.next_tile();
      var options=component.available.get(next).options;
      if(options.length > 0){
        var removed=component.implement(options.first());
        component.stack.push({options:options.clone(), removed:removed});
      }
      else{
        backup();
        return;
      }
      going.defer();
    }
    else // Split
    {
      var parent_component = component;
      var components=sc_hashes.
        sortBy(function(h){return h.keys().length;}).
        map(function(h){return new Component(h);});
      if(components.any(function(c){
          return Component.registered_failures.get(c.hash());
        })){
        //At least one of these components has been tried before and failed
        backup();
        return
      }
      component.subs = components;
      var finished=0;
      var failure = function(){
        $R(0,finished-1).each(function(i){
            components[i].unwind();
        })
        component=parent_component;
        component.subs=undefined;
        backup();
      };
      var success=function(){
        finished++;
        if(finished==components.length){
          component=parent_component;
          parent_component.success();
        }
        else{
          component=components[finished];
          normal();
        }
      };
      components.each(function(c){
        c.success=success;
        c.failure=failure;
      });
      component=components.first();
      normal();
    }
  }
  else backup();
}

function backup(){
  while(component.stack.length > 0 && component.stack.last().options.length==1){
    var stack_item=component.stack.pop();
    component.unimplement(stack_item.options.first(),stack_item.removed);
  }
  if(component.stack.length==0)
  {
    Component.register_failure(component);
    component.failure();
    return;
  }
  var stack_item=component.stack.last();
  component.unimplement(stack_item.options.shift(),stack_item.removed);
  stack_item.removed=component.implement(stack_item.options.first());
  going.defer();
}

function complete(success){
  TetrisUI.finished_going();
  if(success){
    TetrisUI.show_persistence("tiling");
  }
  else{
    tiles.flatten().each(function(tile){
        $w(tile.className).select(function(cl){return cl.indexOf("piece") == 0}).each(function(cl){tile.removeClassName(cl);});
    });
    reset();
    TetrisUI.flash("No solution!");
  }
}

Component.prototype.hash=function(){
  return this.available.keys().sort().join(",");
}

Component.prototype.implement=function(option){
  var self=this;
  /* Does several things:
     -Adds appropriate CSS classes to the tiles
     -Moves the four tiles from available to taken
     -Collects all other options related to those four tiles and removes them from arrays in available
     -Returns the removed options
   */
  $("visited").innerHTML=""+(++visited);
  var removed=new Hash();
  option.tiles.each(function(tid){
    var tt = self.available.get(tid);
    var t=tt.tile;
    t.val=1;
    tt.options.each(function(opt){removed.set(opt.id,opt)});
    self.taken.set(tid,self.available.unset(tid));
  });
  removed=removed.values().sortBy(function(x){return x.id});
  var affected_tiles = new Hash();
  removed.each(function(r){
      r.tiles.each(function(t){
        var m=self.available.get(t);
        if(m){
          if(!m.about_to_remove)m.about_to_remove = [];
          m.about_to_remove.push(r)
          affected_tiles.set(t,m);
        }
      });
  });
  affected_tiles.values().each(function(tile){
      // May be able to do this faster by indexing
      var oz = tile.options;
      var atr = tile.about_to_remove;
      tile.about_to_remove = undefined;
      var new_options = [];
      var a = oz.shift();
      var b = atr.shift();
      while((oz.length > 0 || a) && (atr.length > 0 || b)){
        if(a.id < b.id){
          new_options.push(a);
          a = oz.shift();
        } else {
          // In this case they should be equal
          a = oz.shift();
          b = atr.shift();
        }
      }
      if(a)new_options.push(a);
      new_options = new_options.concat(tile.options);
      tile.options = new_options;
    });
  active_options.push(option);
  option.toggle();
  return removed;
}


Component.prototype.unimplement=function(option,removed){
  var self = this;
  /* Does several things:
      -Removes appropriate CSS classes from tiles
      -Adds options in the 'removed' array back into
        the arrays in available
      -Moves the four tiles from taken to available
   */

  // This part should be optimized as above, using a merge
  var affected_tiles = new Hash();
  removed.each(function(r){
      r.tiles.each(function(t){
        var m=self.available.get(t);
        if(m){
          m.options.push(r);
          affected_tiles.set(t, m);
        }
      });
  });
  affected_tiles.values().each(function(t){t.options = t.options.sortBy(function(x){return x.id});});

  option.toggle();

  option.tiles.each(function(tid){
      var t=self.taken.get(tid).tile;
      t.val=0;
      //t.removeClassName(option.shape);
      self.available.set(tid,self.taken.unset(tid));
  });
  if(active_options.last()==option)
    active_options.pop();
  else
    active_options=active_options.without(option);
}

function random(){
  TetrisUI.start_random();
  /* Set all to active */
  $("tetris").select("td.blank").each(function(td){active_toggle.bind(td,true)();});
  reassess();

  /* To do it animated, we create an array of functions to step through */
  var funx=[];
  var x=23+Math.floor(Math.random()*10);
  var y=23+Math.floor(Math.random()*10);
  if(y>x){
    x=x^y;
    y=x^y;
    x=x^y;
  }
  if(sizex > x)$R(x+1,sizex).each(function(){funx.push(contractx)});
  if(sizex < x)$R(sizex+1,x).each(function(){funx.push(expandx)});
  if(sizey > y)$R(y+1,sizey).each(function(){funx.push(contracty)});
  if(sizey < y)$R(sizey+1,y).each(function(){funx.push(expandy)});

  var tactive=Math.floor((0.75+0.15*Math.random())*x*y);
  while(!is_tileable(tactive))tactive--;
  var kids;
  var connected=$("connected");
  var reduce=function(){
    var taking = [12+(active-tactive)%4,active-tactive].min();
    while(!is_tileable(active - taking))taking++;
    while(true){
      var taken = kids.slice(0,taking);
      kids = kids.slice(taking);
      taken.each(function(k){active_toggle.bind(k)(true);});
      if(reassess())break;
      taken.each(function(k){active_toggle.bind(k)(true);});
      kids=kids.concat(taken.sortBy(Math.random));
    }
    if(active==tactive){
      TetrisUI.finish_random();
      $A($$("td.highlighted")).each(function(el){el.removeClassName("highlighted")});
    }
    else
      reduce.defer();
  }

  var step=function(){
    if(funx.length>0){
      funx.shift()();
      step.defer()
    }
    else{
      kids=tiles.flatten().sortBy(Math.random);
      reduce();
    }
  };
  step();
}

function recompute_tiling_numbers(){
  tiling_numbers.nums = 
    active_pieces().map(function(piece){return piece.points.length}).uniq().
      sortBy(function(x){return x});
  var nm = tiling_numbers.nums.first();
  tiling_numbers.gcd = tiling_numbers.nums.inject(nm,function(acc, n){return gcd(acc,n);});
  var c = tiling_numbers.cache=[false];
  var coprimes = tiling_numbers.nums.map(function(x){return x/tiling_numbers.gcd;});
  nm = coprimes.first();
  while(!(c.length > nm && c.slice(c.length-nm).all(function(x){return x}))){
    var next = c.length;
    if(next-nm<0)c.push(false);
    else
      if(coprimes.any(function(x){return x==next || (next-x > 0 && c[next-x])}))c.push(true);
      else
        c.push(false);
    if(c.length == 100){/*alert("dang");*/return}
  }
}

function get_all_options(tile_hash,callback){
  $("pause_button").disabled=true;
  var opts=[];
  var pn = -1;
  var loader = TetrisUI.loading("Computing possible piece positions...");
  var pieces_left = active_pieces();
  var step_count = pieces_left.length * sizex;
  var piece;
  var i = sizex;
  var stepi = 0;
  var loop = function(){
    if(i == sizex){
      if(pieces_left.length == 0){
        return true;
      }
      piece = pieces_left.shift();
      i = 0;
    }
    for(var j=0;j<sizey;j++){
      var mapped_points = piece.points.map(function(point){
          var x=point[0]+i;
          var y=point[1]+j;
          if(wrapx)x=(x+sizex)%sizex;
          if(wrapy)y=(y+sizey)%sizey;
          return [x,y];
        });
        
      if(mapped_points.all(function(point){
          var x=point[0];
          var y=point[1];
          return tiles[x] && tiles[x][y] &&
            tiles[x][y].val==0
        }) && (function(){
            for(var i=0;i<mapped_points.length-1;i++){
              for(var j=i+1;j<mapped_points.length;j++){
                if(mapped_points[i][0]==mapped_points[j][0] &&
                   mapped_points[i][1]==mapped_points[j][1])
                  return false;
              }
            }
            return true;
          })()){
        opts.push(new Option(piece.points.map(function(point){
              var x=point[0]+i;
              var y=point[1]+j;
              if(wrapx)x=(x+sizex)%sizex;
              if(wrapy)y=(y+sizey)%sizey;
              return tiles[x][y]}),piece));
      }
    }
    i++;
    stepi++;
    return false;
  }
  var step = function(){
    // k determines the chunk size used
    var k = 32;
    while(k > 0 && !loop())k--;
    loader(stepi/step_count);
    if(k > 0){
        loader(true);
        (function(){
          opts = opts.sortBy(Math.random);
          var oid = 0;
          opts.each(function(o){o.id=(oid++)});
          opts.each(function(opt){
            opt.tiles.each(function(tid){
              tile_hash.get(tid).options.push(opt);
            });
          });
          $("pause_button").disabled=false;
          callback();
        }).defer();
    }
    else step.defer();
  }
  step.defer();
}

function Option(elements, piece){
  this.piece = piece;
  this.tiles = elements.pluck("tile_id");
  var self = this;
  if(!piece.color)
    piece.color = DEFAULT_COLORS[Math.floor(Math.random()*DEFAULT_COLORS.length)];
  var color = piece.color;
  if(!piece)piece={};
  if(!piece.borders){
    piece.borders=[];
    var poss = piece.points;

    poss.each(function(pos){
        var pb = {backgroundColor: color};
        piece.borders.push(pb);
        if(!poss.any(function(p){
            return p[0]==pos[0]-1 && p[1]==pos[1];
          }))
          pb["borderLeftColor"] = BORDER_COLOR;
        else
          pb["borderLeftColor"] = color;
        if(!poss.any(function(p){
            return p[0]==pos[0]+1 && p[1]==pos[1];
          }))
          pb["borderRightColor"] = BORDER_COLOR;
        else
          pb["borderRightColor"] = color;
        if(!poss.any(function(p){
            return p[0]==pos[0] && p[1]==pos[1]+1;
          }))
          pb["borderTopColor"] = BORDER_COLOR;
        else
          pb["borderTopColor"] = color;
        if(!poss.any(function(p){
            return p[0]==pos[0] && p[1]==pos[1]-1;
          }))
          pb["borderBottomColor"] = BORDER_COLOR;
        else
          pb["borderBottomColor"] = color;
    }); 
  }
  this.on = false;
  this.toggle=function(){
    this.on = !this.on;
    if(this.on)
      elements.zip(piece.borders).each(function(a){
          a[0].setStyle(a[1]);
      });
    else
      elements.each(function(el){
          el.setStyle({borderColor: BORDER_COLOR, backgroundColor: "inherit"});
      });
  }
}

function reset(){
  TetrisUI.not_going_mode();
  TetrisUI.show_persistence("grid");
  $("visited").innerHTML="";
  active_options.each(function(opt){opt.toggle();});
  active_options=undefined;
  editing=true;
}

function clear_all(){
  $("tetris").select("td.blank").each(function(el){active_toggle.bind(el)(true)});
  reassess();
}

function invert(){
  $("tetris").select("td.tet").each(function(el){active_toggle.bind(el)(true)});
  reassess();
}

function do_debug(msg){
  $("debug").innerHTML+=msg+"<hr>";
}

function is_tileable(n){
  if(n<tiling_numbers.nums[0])return false;
  if(n%tiling_numbers.gcd > 0)return false;
  var m = n/tiling_numbers.gcd;
  if(m>=tiling_numbers.cache.length)return true;
  return tiling_numbers.cache[m];
}

function gcd(a,b){
  return b==0 ? a : gcd(b, a%b);
}


function get_saving_data(){
  obj = new Hash();
  // Grid state
  var grid = new Hash();
  grid.set("width", sizex);
  grid.set("height", sizey);
  grid.set("wrapx",$("wrapx").checked);
  grid.set("wrapy",$("wrapy").checked);
  var arr = [];
  // This determines whether we will note the active squares
  // or the inactive ones
  var test = 2*active > sizex*sizey;
  $R(0,sizey-1).each(function(y){
      $R(0,sizex-1).each(function(x){
          if(tiles[x][y].hasClassName("blank") == test)
            arr.push(sizex*y+x);
      });
  });
  grid.set(test ? "off" : "on",arr);
  obj.set("grid",grid);
  obj.set("colors",get_active_colors());
  obj.set("version",2);
  var pieces = [];

  $("piece_selector").select(".unattached_piece_holder").each(function(piece_holder){
    var ob = export(piece_holder);
    if(ob.keys().length > 2 || 
        ob.get("mode") != "off" || 
        piece_holder.ancestors().invoke("identify").include("piece_category_custom"))
          pieces.push(ob);
  });
  obj.set("pieces",pieces);

  if(active_options)
    obj.set("instances",active_options.map(function(ao){
          return new Hash({piece:ao.piece.points, tiles: ao.tiles, color: ao.piece.color})
    }));
  
  return obj.toJSON();
}

function saving(){
  $("saving_link").setStyle({visibility:"hidden"});
  var p = $H();
  p.set("data", get_saving_data());
  p.set("authenticity_token", $("image_form").select("input").first().value);
  new Ajax.Request(window.location.toString(),
      {
        parameters: p,
        method: 'post',
        onSuccess: function(t){
          var sr = $("saving_result");
          var div = Builder.node("div","")
          div.innerHTML = t.responseText;
          TetrisUI.popup("Link to saved version", div, function(){
            // This might technically be a weakness in the UI,
            // as a person could start a tiling, close the window,
            // and click the Persistence link in the middle of a tiling.
            $("saving_link").setStyle({visibility: "visible"});
          });
        },
        onFailure: function(t){
          $("saving_link").setStyle({visibility: "visible"});
          TetrisUI.flash("Failed for some reason");
        }
      });
  return false;
}

function load_saved(){
  var data = eval("("+$("saved_grid").innerHTML+")");
  var grid = data.grid;
  var height = grid.height;
  var width = grid.width;
  clear_all();
  while(sizex < width)expandx(true);
  while(sizex > width)contractx(true);
  while(sizey > height)contracty(true);
  while(sizey < height)expandy(true);
  tiles=get_tiles();
  reassess();
  var marked, aswhat;
  if(grid.on){
    marked=grid.on.clone();
    aswhat = false;
  }
  else{
    marked=grid.off.clone();
    aswhat = true;
  }
  var k=0;
  for(var j=0;j<height;j++){
    for(var i=0;i<width;i++){
      var t = tiles[i][j];
      if((k++) == marked.first()){
        marked.shift();
        if(aswhat)active_toggle.bind(t)();
      }
      else
        if(!aswhat)active_toggle.bind(t)();
    }
  }
  
  if(grid.wrapx)$("wrapx").checked=wrapx=true;
  if(grid.wrapy)$("wrapy").checked=wrapy=true;

  if(data.version == 1){
    if(data.wrapx)$("wrapx").checked=wrapx=true;
    if(data.wrapy)$("wrapy").checked=wrapy=true;
    var mapping = {"4-1": [[1, 0], [2, 0], [0, 0], [3, 0]], "4-2": [[0, 0], [0, 1], [1, 0], [1, 1]], "4-3": [[1, 0], [0, 0], [2, 0], [0, 1]], "4-4": [[1, 0], [0, 0], [2, 0], [1, 1]], "4-5": [[1, 0], [1, 1], [0, 0], [2, 1]], "2-1": [[0, 0], [1, 0]], "3-1": [[1, 0], [2, 0], [0, 0]], "3-2": [[0, 0], [0, 1], [1, 0]], "5-1": [[2, 0], [3, 0], [1, 0], [4, 0], [0, 0]], "5-2": [[1, 0], [1, 1], [0, 0], [0, 1], [2, 0]], "5-3": [[1, 0], [0, 0], [2, 0], [0, 1], [2, 1]], "5-4": [[1, 0], [2, 0], [0, 0], [3, 0], [0, 1]], "5-5": [[1, 0], [2, 0], [0, 0], [3, 0], [1, 1]], "5-6": [[2, 1], [1, 1], [3, 1], [1, 0], [0, 0]], "5-7": [[1, 0], [0, 0], [2, 0], [0, 1], [0, 2]], "5-8": [[1, 1], [0, 1], [2, 1], [0, 2], [0, 0]], "5-9": [[1, 1], [0, 1], [2, 1], [0, 0], [1, 2]], "5-10": [[1, 1], [1, 2], [0, 1], [2, 2], [0, 0]], "5-11": [[1, 1], [2, 1], [0, 1], [2, 2], [0, 0]], "5-12": [[1, 1], [0, 1], [2, 1], [1, 2], [1, 0]], "6-1": [[2, 0], [3, 0], [1, 0], [4, 0], [0, 0], [5, 0]], "6-2": [[1, 0], [1, 1], [0, 0], [0, 1], [2, 0], [2, 1]], "6-3": [[1, 0], [1, 1], [0, 0], [0, 1], [2, 0], [3, 0]], "6-4": [[1, 0], [2, 0], [0, 0], [2, 1], [0, 1], [3, 0]], "6-5": [[1, 0], [2, 0], [0, 0], [2, 1], [0, 1], [3, 1]], "6-6": [[1, 0], [2, 0], [0, 0], [3, 0], [0, 1], [3, 1]], "6-7": [[2, 0], [1, 0], [3, 0], [0, 0], [4, 0], [0, 1]], "6-8": [[2, 0], [2, 1], [1, 0], [1, 1], [3, 0], [0, 0]], "6-9": [[1, 0], [1, 1], [2, 0], [2, 1], [0, 0], [3, 1]], "6-10": [[2, 0], [1, 0], [3, 0], [0, 0], [4, 0], [1, 1]], "6-11": [[2, 1], [3, 1], [1, 1], [4, 1], [1, 0], [0, 0]], "6-12": [[2, 0], [1, 0], [3, 0], [0, 0], [4, 0], [2, 1]], "6-13": [[3, 1], [2, 1], [4, 1], [2, 0], [1, 0], [0, 0]], "6-14": [[1, 0], [1, 1], [0, 0], [0, 1], [2, 0], [0, 2]], "6-15": [[0, 1], [1, 1], [0, 0], [1, 0], [0, 2], [2, 1]], "6-16": [[0, 1], [0, 0], [0, 2], [1, 0], [1, 2], [2, 0]], "6-17": [[1, 0], [2, 0], [0, 0], [3, 0], [0, 1], [0, 2]], "6-18": [[1, 1], [0, 1], [2, 1], [0, 0], [2, 0], [0, 2]], "6-19": [[1, 1], [2, 1], [0, 1], [3, 1], [0, 2], [0, 0]], "6-20": [[1, 1], [0, 1], [1, 0], [0, 0], [1, 2], [2, 1]], "6-21": [[1, 1], [0, 1], [1, 0], [0, 0], [1, 2], [2, 2]], "6-22": [[1, 1], [2, 1], [0, 1], [2, 0], [0, 0], [1, 2]], "6-23": [[1, 1], [2, 1], [0, 1], [3, 1], [0, 0], [1, 2]], "6-24": [[2, 2], [1, 2], [3, 2], [1, 1], [0, 1], [0, 0]], "6-25": [[1, 1], [2, 1], [0, 1], [3, 1], [0, 0], [2, 2]], "6-26": [[1, 1], [2, 1], [0, 1], [2, 2], [3, 2], [0, 0]], "6-27": [[2, 1], [1, 1], [3, 1], [0, 1], [3, 2], [0, 0]], "6-28": [[1, 0], [2, 0], [0, 0], [3, 0], [1, 1], [1, 2]], "6-29": [[2, 1], [1, 1], [3, 1], [1, 0], [0, 0], [1, 2]], "6-30": [[1, 1], [1, 2], [1, 0], [2, 2], [3, 2], [0, 0]], "6-31": [[2, 1], [1, 1], [3, 1], [1, 0], [0, 0], [2, 2]], "6-32": [[2, 1], [1, 1], [2, 2], [1, 0], [3, 2], [0, 0]], "6-33": [[1, 0], [2, 0], [0, 0], [2, 1], [3, 1], [2, 2]], "6-34": [[1, 1], [2, 1], [0, 1], [3, 1], [1, 2], [1, 0]], "6-35": [[2, 1], [1, 1], [3, 1], [0, 1], [2, 2], [1, 0]]};
    var new_data = new Hash();
    var new_grid = new Hash(grid);
    new_grid.set("wrapx",data.wrapx);
    new_grid.set("wrapy",data.wrapy);
    new_data.set("grid",new_grid);
    new_data.set("colors",["225588", "C93C3C", "996633", "8899BB", "ECD959", "9944CC", "44CC44"]);
    new_data.set("pieces", data.pieces.map(function(piece){
          return new Hash({mode: "free", 
                           color: piece.color,
                           piece: mapping[piece.id.substring(6)]});
    }));
    new_data.set("version",2);
    new_data.set("version-1",data);
    if(data.pieces.any(function(piece){return piece.instances})){
      set_tile_attributes();
      new_data.set("instances",data.pieces.map(function(piece){
        if(!piece.instances)return [];
        var normalized_piece = new Piece(mapping[piece.id.substring(6)]);
        return piece.instances.map(function(instance){
          var anchor;
          var matches = function(a_piece, an_instance){
            var anchor_id = an_instance[0];
            var i = anchor_id % sizex;
            var j = Math.floor(anchor_id / sizex);
            var mapped_points = a_piece.points.map(function(point){
              var x = point[0]+i-a_piece.points[0][0];
              var y = point[1]+j-a_piece.points[0][1];
              if(wrapx)x=(x+2*sizex)%sizex;
              if(wrapy)y=(y+2*sizey)%sizey;
              return [x,y];
            });
            var on_board = mapped_points.all(function(point){
              var x = point[0];
              var y = point[1];
              return tiles[x] && tiles[x][y];
            });
            if(!on_board)return false;
            var on_piece = mapped_points.all(function(point){
              var x = point[0];
              var y = point[1];
              var id = x + sizex*y;
              return instance.include(id);
            });
            anchor = anchor_id;
            return on_piece;
          };
          var matches_any = function(a_piece, an_instance){
            return an_instance.any(function(t){
                return matches(a_piece,[t].concat(an_instance.without(t)))
            });
          }
          var real_piece = normalized_piece.variations().find(function(v){return matches_any(v,instance)});
          if(!real_piece)throw "no pieces";
          var anchor_point = real_piece.points[0];
          var i = anchor % sizex - anchor_point[0];;
          var j = Math.floor(anchor / sizex) - anchor_point[1];
          var new_instance = real_piece.points.map(function(point){
              var x = point[0] + i;
              var y = point[1] + j;
              if(wrapx)x = (x+sizex)%sizex;
              if(wrapy)y = (y+sizey)%sizey;
              return tiles[x][y].tile_id;
          });
          return new Hash({color: piece.color,
                           tiles: new_instance,
                           "piece": real_piece.points});
        });
      }).flatten());
    }
    var form = $("version_updater");
    form.version_update.value = new_data.toJSON();
    form.submit();
    return;
  }

  var pieces = data.pieces;
  pieces.each(import);
  recompute_tiling_numbers();
  reassess();
  TetrisUI.hide_persistence();

  $("colors").childElements().each(Element.remove);
  data.colors.each(create_color);


  if(data.instances){
    set_tile_attributes();

    // REFACTOR: This is gross
    var tiles_by_id = [];
    $$(".tet").each(function(t){tiles_by_id[t.tile_id]=t});
 
    active_options=[];
    data.instances.each(function(instance){
      var the_tiles = instance.tiles.map(function(tid){return tiles_by_id[tid]});
      var piece = new Piece(instance.piece);
      piece.color = instance.color;
      active_options.push(new Option(the_tiles,piece));
    });
    active_options.invoke("toggle");
    TetrisUI.going_mode();
    TetrisUI.finished_going();
    TetrisUI.disable(TetrisUI.saving);
  }
  $$(".tab").invoke("identify").invoke("substring",8).each(piece_window_update_count);
}

function wrap_change(){
  wrapx = $("wrapx").checked;
  wrapy = $("wrapy").checked;
  reassess();
}

var TetrisUI = {

  start_select_pieces: function(){
    TetrisUI.toggle("go_button selector_button select_done_button piece_selector main_grid");
    var up = new Effect.BlindUp("not_selecting",{sync:true});
    var down = new Effect.BlindDown("are_selecting",{sync:true});
    new Effect.Parallel([down,up],{duration:1.0});
  },

  done_select_pieces: function(){
    if(active_pieces().length == 0){
      TetrisUI.flash("Select a piece first"); 
      return;
    }
    TetrisUI.toggle("go_button selector_button select_done_button piece_selector main_grid");
    var up = new Effect.BlindUp("are_selecting",{sync:true});
    var down = new Effect.BlindDown("not_selecting",{sync:true});
    new Effect.Parallel([down,up],{duration:1.0});
    recompute_tiling_numbers();
    reassess();
  },

  start_random: function(){
    TetrisUI.disable_most();
  },

  finish_random: function(){
    TetrisUI.enable_most();
  },

  disable_most: function(){
    [TetrisUI.grid_editor, TetrisUI.image_links, TetrisUI.saving].each(TetrisUI.disable);
  },

  enable_most: function(){
    [TetrisUI.grid_editor, TetrisUI.image_links, TetrisUI.saving].each(TetrisUI.enable);
  },

  going_mode : function(){
    TetrisUI.disable_most();
    TetrisUI.toggle("go_button selector_button pause_button");
    TetrisUI.toggle_arrows("hidden");
  },

  pause: function(){
    TetrisUI.toggle("pause_button resume_button reset_button");
  },

  resume: function(){
    TetrisUI.toggle("pause_button resume_button reset_button");
  },

  finished_going : function(){
    TetrisUI.toggle("pause_button reset_button");
    TetrisUI.enable(TetrisUI.saving);
    TetrisUI.enable(TetrisUI.image_links);
  },

  not_going_mode : function(){
    TetrisUI.toggle("reset_button selector_button go_button");
    TetrisUI.resume_button.hide();
    TetrisUI.toggle_arrows("visible");
    TetrisUI.enable(TetrisUI.saving);
    TetrisUI.enable(TetrisUI.grid_editor);
  },

  disable : function(what){
    what.setOpacity(0.3);
    what.select("button, input").each(function(el){
        el.disabled=true;
      });
  },

  enable : function(what){
    what.setOpacity(1);
    what.select("button, input").each(function(el){
        el.disabled=false;
      });
  },

  toggle: function(ids){
    ids.split(/\s+/).each(function(id){
        if(!TetrisUI[id])alert("Why isn't there an \""+id+"\"? I am "+TetrisUI);
        TetrisUI[id].toggle()});
  },

  toggle_arrows: function(what){
    ["bup","bleft","bright","bdown"].each(function(id){$(id).setStyle({visibility: what});});
  },

  hide_persistence: function(){
    if(TetrisUI.persistence){
      TetrisUI.disable(TetrisUI.saving);
      TetrisUI.persistence=undefined;
    }
  },

  show_persistence: function(save_type){
    if(!(TetrisUI.persistence=="save_type")){
      TetrisUI.enable(TetrisUI.saving);
      TetrisUI.persistence=save_type;
      $("save_type").innerHTML=save_type;
    }
  },
  persistence: undefined,

  flash: function(str, t){
    if(!t)t=2.0;
    var el = Builder.node("div", {className: "flash"}, str);
    document.body.appendChild(el);
    (function(){Effect.Fade(el.identify(), {duration: t})}).delay(2.4);
  },

  popup: function(title, el, callback){
    var close_button = (callback == false ? 
        Builder.node("span","") : 
        Builder.node("div", {className: "close"}, "X"));
    var p = Builder.node("div", {className: "popup"},
        Builder.node("div", {className: "title_bar"},
          [close_button, Builder.node("span",title), Builder.node("div",{style: "clear:both;"})]));
    document.body.appendChild(p);
    p.appendChild(Builder.node("div", {className: "contents"}, el));
    var removal = Element.remove.curry(p);
    var closer = p.select(".close")[0];
    if(closer)closer.observe("click",function(){removal();if(callback)callback()});
    return removal;
  },

  loading: function(title){
    var element = Builder.node("div",{className:"loading_holder"},
        Builder.node("div",{className:"loading",style:"width:0%;"}));
    var closing = TetrisUI.popup(title, element, false);
    var closed = false;
    var update = function(f){
      if(closed)return;
      if(f==true){
        closing();
        closed = true;
      }
      else {
        f = Math.min(1,Math.max(parseFloat(f),0));
        element.select(".loading")[0].setStyle({width:""+parseInt(f*parseInt(element.getStyle("width")))+"px"});
      }
    };
    return update;
  }
}

function download_image(size){
  var json = get_saving_data();
  var f = document.forms.image_form;
  f.data.value = json;
  f.size.value = size;
  f.submit();
}
