text: 'use strict';
  
  /** @typedef {string} */
  var pointId;
  
  /*+ @return {pointId} */
  function mapId(x, y) {
    var id = angular.toJson([x, y]);
    return id;
  }
  
  /**
   * a 2-d hash.
   * @constructor
   */
  var Map = function() {
    /*  {Object<pointId, Object>} */
    this.data = {};
  };
  
  /** returns an element for the passed x,y position. */
  Map.prototype.get = function(x, y) {
    var id = mapId(x, y);
    return this.data[id];
  };
  
  /** returns an element for the passed x,y position, or create one if it doesnt exist. */
  Map.prototype.getOrCreate = function(x, y, cb) {
    var id = mapId(x, y);
    if (!(id in this.data)) {
      var map = cb(id, x, y);
      this.data[id] = map;
    }
    return this.data[id];
  };
  
  /**
   * A wrapper for a single cell; created by Layer.
   * @constructor
   * @param {Grid} grid
   */
  var Cell = function(grid, index) {
    if (!grid || angular.isUndefined(grid.raw)) {
      throw new Error("invalid grid");
    }
    this.grid = grid;
    this._index = index;
  };
  
  /** linear index of this cell in its grid. */
  Cell.prototype.cellIndex = function() {
    return this._index;
  };
  
  /** coordinate of this cell within its grid. */
  Cell.prototype.cellOffset = function() {
    return this.grid.cellOffsetFromIndex(this._index);
  };
  
  /** pixel position of the upper-left corner of this cell. */
  Cell.prototype.cellPos = function() {
    var gridPos = this.grid.gridPos();
    var myPos = pt_mul(this.cellOffset(), this.grid.layer.cellSize);
    return pt_add(gridPos, myPos);
  };
  
  /** tile sheet id. */
  Cell.prototype.tileId = function() {
    return this.grid.raw[this._index][0];
  };
  
  /** linear index of a sprite within its tile sheet. */
  Cell.prototype.tileIndex = function() {
    return this.grid.raw[this._index][1];
  };
  
  /** rotation (0..3) in multiples of 90 degrees for displaying this cell. */
  Cell.prototype.tileRot = function() {
    return this.grid.raw[this._index][2];
  };
  
  /**
   * wraps the array of grid data; created by the Layer.
   * @constructor
   */
  var Grid = function(layer, ofs, raw) {
    if (!layer) {
      throw new Error("invalid layer");
    }
    this.layer = layer; // owner layer.
    this.ofs = ofs; // offset of the upper left corner of the grid (in units of whole grids).
    this.raw = raw; // array of raw grid cells.
  };
  
  /** Pixel position of the upper-left corner of this grid. */
  Grid.prototype.gridPos = function() {
    var pixels = pt_mul(this.layer.numCells, this.layer.cellSize);
    return pt_mul(pixels, this.ofs);
  };
  
  /** expand a linear cell index into a 2d cell coordinate. */
  Grid.prototype.cellOffsetFromIndex = function(index) {
    var cellsWide = this.layer.numCells.x;
    var cy = Math.floor(index / cellsWide);
    var cx = index % cellsWide;
    return pt(cx, cy);
  };
  
  /** return cell x,y within a grid. */
  Grid.prototype.cellOffsetFromPos = function(pix) {
    var pixels = pt_mul(this.layer.numCells, this.layer.cellSize);
    // "wrap" the passed pixel into this grid
    var pt = pt_mod(pix, pixels);
    // find the position in cells.
    var cd = pt_divFloor(pt, this.layer.cellSize);
    // normalize negative coordinates into positive measurements from the upper-left edge.
    if (cd.x < 0) {
      cd.x = this.layer.numCells.x + cd.x;
    }
    if (cd.y < 0) {
      cd.y = this.layer.numCells.y + cd.y;
    }
    return cd;
  };
  
  /**
   * A map of grids.
   * all grids in the layer have width*height pixels.
   * @constructor
   */
  var Layer = function(broad, id, numCells, cellSize) {
    this.id = id;
    this.numCells = numCells; // number of cells wide,high
    this.cellSize = cellSize; // size in pixel of each cell
    this.grids = new Map();
    this.broad = broad;
  };
  
  /* return grid based on position. */
  Layer.prototype.grid = function(pix) {
    var pixels = pt_mul(this.numCells, this.cellSize);
    var gofs = pt_divFloor(pix, pixels);
    var layer = this; // pin "this" for use in callback.
    return layer.grids.getOrCreate(gofs.x, gofs.y, function() {
      return new Grid(layer, gofs, []);
    });
  };
  /* return a new cell object for the passed pixel position. */
  Layer.prototype.cellByPixel = function(pix) {
    var grid = this.grid(pix);
    var cell = grid.cellOffsetFromPos(pix);
    var index = cell.x + (cell.y * this.numCells.x);
    return new Cell(grid, index);
  };
  /** 
   * add the tile to the world at the passed pixel position with the passed rotation.
   * @param {Array<string,int,int>} data (tileId, tileIdx, rot) 
  */
  Layer.prototype.place = function(pix, data) {
    // find the cell index on that grid.
    var cell = this.cellByPixel(pix);
    // store the data; FIX: sure is nice and object oriented :(
    cell.grid.raw[cell._index] = data;
    // let everyone known the map has changed.
    this.broad.$broadcast('tilePlaced', cell);
    // return
    return cell;
  };
  
  /**
   * @fileoverview MapService
   */
  angular.module('mosaic')
    .factory('MapService',
      function($log, $rootScope) {
        var layers = new Map();
        var layerSize = pt(512); // pixels of each layer.
        
        var mapService = {
          layerSize: layerSize,
  
          // return a layer which uses the passed cell size.
          // note: doesn't validate that its part of grid sizes
          getBestLayer: function(cellSize) {
            return layers.getOrCreate(cellSize.x, cellSize.y, function(id) {
              var numCells = pt_divFloor(layerSize, cellSize);
              return new Layer($rootScope, id, numCells, cellSize);
            });
          },
  
          // return array of json data
          mapData: function() {
            var rawMap = []; // an array of layers
            for (var id in layers.data) {
              var layer = layers.data[id];
              var rawLayer = {
                x: layer.cellSize.x,
                y: layer.cellSize.y,
                grids: []
              };
              rawMap.push(rawLayer);
              var layerData = layer.grids.data; // tricky :(
              for (var k in layerData) {
                var grid = layerData[k];
                var rawGrid = {
                  x: grid.ofs.x,
                  y: grid.ofs.y,
                  data: grid.raw
                };
                rawLayer.grids.push(rawGrid);
              } // end of layer data loop
            } // end of all layers loop
            return rawMap;
          }, // end of mapData.
        }; // end of map service
        return mapService;
      });
  