function cell2Polygons(cell, x, y, settings) {
  var polygons = [];

  cell.polygons.forEach(function(p) {
    p.forEach(function(pp) {
      pp[0] += x;
      pp[1] += y;
    });

    if (settings.linearRing)
      p.push(p[0]);

    polygons.push(p);
  });

  return polygons;
}

function entry_coordinate(x, y, mode, path) {
  if (mode === 0) { /* down */
    x += 1;
    y += path[0][1];
  } else if (mode === 1) { /* left */
    x += path[0][0];
  } else if (mode === 2) { /* up */
    y += path[0][1];
  } else if (mode === 3) { /* right */
    x += path[0][0];
    y += 1;
  }

  return [ x, y ];
}


function skip_coordinate(x, y, mode) {
  if (mode === 0) { /* down */
    x++;
  } else if (mode === 1) { /* left */
    /* do nothing */
  } else if (mode === 2) { /* up */
    y++;
  } else if (mode === 3) { /* right */
    x++;
    y++;
  }

  return [ x, y ];
}


function requireFrame(data, lowerBound, upperBound) {
  var frameRequired,
    cols,
    rows,
    i,
    j;

  frameRequired = true;
  cols          = data[0].length;
  rows          = data.length;

  for (j = 0; j < rows; j++) {
    if ((data[j][0] < lowerBound) ||
        (data[j][0] > upperBound) ||
        (data[j][cols - 1] < lowerBound) ||
        (data[j][cols - 1] > upperBound)) {
      frameRequired = false;
      break;
    }
  }

  if ((frameRequired) &&
      ((data[rows - 1][0] < lowerBound) ||
      (data[rows - 1][0] > upperBound) ||
      (data[rows - 1][cols - 1] < lowerBound) ||
      (data[rows - 1][cols - 1] > upperBound))) {
    frameRequired = false;
  }

  if (frameRequired)
    for (i = 0; i < cols - 1; i++) {
      if ((data[0][i] < lowerBound) ||
          (data[0][i] > upperBound) ||
          (data[rows - 1][i] < lowerBound) ||
          (data[rows - 1][i] > upperBound)) {
        frameRequired = false;
        break;
      }
    }


  return frameRequired;
}


function requireLineFrame(data, threshold) {
  var frameRequired,
    cols,
    rows,
    i,
    j;

  frameRequired = true;
  cols          = data[0].length;
  rows          = data.length;

  for (j = 0; j < rows; j++) {
    if ((data[j][0] >= threshold) ||
        (data[j][cols - 1] >= threshold)) {
      frameRequired = false;
      break;
    }
  }

  if ((frameRequired) &&
      ((data[rows - 1][0] >= threshold) ||
      (data[rows - 1][cols - 1] >= threshold))) {
    frameRequired = false;
  }

  if (frameRequired)
    for (i = 0; i < cols - 1; i++) {
      if ((data[0][i] >= threshold) ||
          (data[rows - 1][i] > threshold)) {
        frameRequired = false;
        break;
      }
    }

  return frameRequired;
}


function traceBandPaths(data, cellGrid, settings) {
  var nextedge,
    path,
    e,
    ee,
    s,
    ve,
    enter,
    x,
    y,
    finalized,
    origin,
    cc,
    dir,
    count,
    point,
    found_entry;

  var polygons = [];
  var rows = data.length - 1;
  var cols = data[0].length - 1;

  /*
   * directions for out-of-grid moves are:
   * 0 ... "down",
   * 1 ... "left",
   * 2 ... "up",
   * 3 ... "right"
   */
  var valid_entries = [ ['rt', 'rb'], /* down */
    ['br', 'bl'], /* left */
    ['lb', 'lt'], /* up */
    ['tl', 'tr']  /* right */
  ];
  var add_x         = [ 0, -1, 0, 1 ];
  var add_y         = [ -1, 0, 1, 0 ];
  var available_starts = [ 'bl', 'lb', 'lt', 'tl', 'tr', 'rt', 'rb', 'br' ];
  var entry_dir     =  {
    bl: 1, br: 1,
    lb: 2, lt: 2,
    tl: 3, tr: 3,
    rt: 0, rb: 0
  };

  if (requireFrame(data, settings.minV, settings.maxV)) {
    if (settings.linearRing)
      polygons.push([ [0, 0], [0, rows], [cols, rows], [cols, 0], [0, 0] ]);
    else
      polygons.push([ [0, 0], [0, rows], [cols, rows], [cols, 0] ]);
  }

  /* finally, start tracing back first polygon(s) */
  cellGrid.forEach(function(a, i) {
    a.forEach(function(cell, j) {
      nextedge = null;

      /* trace paths for all available edges that go through this cell */
      for (e = 0; e < 8; e++) {
        nextedge = available_starts[e];

        if (typeof cell.edges[nextedge] !== 'object')
          continue;

        /* start a new, full path */
        path              = [];
        ee                = cell.edges[nextedge];
        enter             = nextedge;
        x                 = i;
        y                 = j;
        finalized         = false;
        origin            = [ i + ee.path[0][0], j + ee.path[0][1] ];

        /* add start coordinate */
        path.push(origin);

        /* start traceback */
        while (!finalized) {
          cc = cellGrid[x][y];

          if (typeof cc.edges[enter] !== 'object')
            break;

          ee = cc.edges[enter];

          /* remove edge from cell */
          delete cc.edges[enter];

          /* add last point of edge to path arra, since we extend a polygon */
          point = ee.path[1];
          point[0] += x;
          point[1] += y;
          path.push(point);

          enter = ee.move.enter;
          x     = x + ee.move.x;
          y     = y + ee.move.y;

          /* handle out-of-grid moves */
          if ((typeof cellGrid[x] === 'undefined') ||
              (typeof cellGrid[x][y] === 'undefined')) {
            dir   = 0;
            count = 0;

            if (x === cols) {
              x--;
              dir = 0;  /* move downwards */
            } else if (x < 0) {
              x++;
              dir = 2;  /* move upwards */
            } else if (y === rows) {
              y--;
              dir = 3;  /* move right */
            } else if (y < 0) {
              y++;
              dir = 1;  /* move left */
            } else {
              throw new Error('Left the grid somewhere in the interior!');
            }

            if ((x === i) && (y === j) && (dir === entry_dir[nextedge])) {
              finalized = true;
              enter     = nextedge;
              break;
            }

            while (1) {
              found_entry = false;

              if (count > 4)
                throw new Error('Direction change counter overflow! This should never happen!');

              if (!((typeof cellGrid[x] === 'undefined') ||
                    (typeof cellGrid[x][y] === 'undefined'))) {
                cc = cellGrid[x][y];

                /* check for re-entry */
                for (s = 0; s < valid_entries[dir].length; s++) {
                  ve = valid_entries[dir][s];
                  if (typeof cc.edges[ve] === 'object') {
                    /* found re-entry */
                    ee = cc.edges[ve];
                    path.push(entry_coordinate(x, y, dir, ee.path));
                    enter = ve;
                    found_entry = true;
                    break;
                  }
                }
              }

              if (found_entry) {
                break;
              } else {
                path.push(skip_coordinate(x, y, dir));

                x += add_x[dir];
                y += add_y[dir];

                /* change direction if we'e moved out of grid again */
                if ((typeof cellGrid[x] === 'undefined') ||
                    (typeof cellGrid[x][y] === 'undefined')) {
                  if (((dir === 0) && (y < 0)) ||
                      ((dir === 1) && (x < 0)) ||
                      ((dir === 2) && (y === rows)) ||
                      ((dir === 3) && (x === cols))) {
                    x -= add_x[dir];
                    y -= add_y[dir];

                    dir = (dir + 1) % 4;
                    count++;
                  }
                }

                if ((x === i) && (y === j) && (dir === entry_dir[nextedge])) {
                /* we are back where we started off, so finalize the polygon */
                  finalized = true;
                  enter     = nextedge;
                  break;
                }
              }
            }
          }
        }

        if ((settings.linearRing) &&
          ((path[path.length - 1][0] !== origin[0]) ||
          (path[path.length - 1][1] !== origin[1])))
          path.push(origin);

        polygons.push(path);
      } /* end forall entry sites */
    }); /* end foreach i */
  }); /* end foreach j */

  return polygons;
}


function traceLinePaths(data, cellGrid, settings) {
  var nextedge,
    e,
    ee,
    cc,
    path,
    enter,
    x,
    y,
    finalized,
    origin,
    point,
    dir,
    count,
    found_entry,
    ve;

  var polygons = [];
  var rows = data.length - 1;
  var cols = data[0].length - 1;

  /*
   * directions for out-of-grid moves are:
   * 0 ... "down",
   * 1 ... "left",
   * 2 ... "up",
   * 3 ... "right"
   */
  var valid_entries = [ 'right',  /* down */
    'bottom', /* left */
    'left',   /* up */
    'top'     /* right */
  ];
  var add_x         = [ 0, -1, 0, 1 ];
  var add_y         = [ -1, 0, 1, 0 ];
  var entry_dir     =  {
    bottom: 1,
    left: 2,
    top: 3,
    right: 0
  };

  /* first, detect whether we need any outer frame */
  if (!settings.noFrame)
    if (requireLineFrame(data, settings.threshold)) {
      if (settings.linearRing)
        polygons.push([ [0, 0], [0, rows], [cols, rows], [cols, 0], [0, 0] ]);
      else
        polygons.push([ [0, 0], [0, rows], [cols, rows], [cols, 0] ]);
    }

  /* finally, start tracing back first polygon(s) */

  cellGrid.forEach(function(a, i) {
    a.forEach(function(cell, j) {
      nextedge = null;

      /* trace paths for all available edges that go through this cell */
      for (e = 0; e < 4; e++) {
        nextedge = valid_entries[e];

        if (typeof cell.edges[nextedge] !== 'object')
          continue;

        /* start a new, full path */
        path              = [];
        ee                = cell.edges[nextedge];
        enter             = nextedge;
        x                 = i;
        y                 = j;
        finalized         = false;
        origin            = [ i + ee.path[0][0], j + ee.path[0][1] ];

        /* add start coordinate */
        path.push(origin);

        /* start traceback */
        while (!finalized) {
          cc = cellGrid[x][y];

          if (typeof cc.edges[enter] !== 'object')
            break;

          ee = cc.edges[enter];

          /* remove edge from cell */
          delete cc.edges[enter];

          /* add last point of edge to path arra, since we extend a polygon */
          point = ee.path[1];
          point[0] += x;
          point[1] += y;
          path.push(point);

          enter = ee.move.enter;
          x     = x + ee.move.x;
          y     = y + ee.move.y;

          /* handle out-of-grid moves */
          if ((typeof cellGrid[x] === 'undefined') ||
              (typeof cellGrid[x][y] === 'undefined')) {

            if (!settings.linearRing)
              break;

            dir   = 0;
            count = 0;

            if (x === cols) {
              x--;
              dir = 0;  /* move downwards */
            } else if (x < 0) {
              x++;
              dir = 2;  /* move upwards */
            } else if (y === rows) {
              y--;
              dir = 3;  /* move right */
            } else if (y < 0) {
              y++;
              dir = 1;  /* move left */
            }

            if ((x === i) && (y === j) && (dir === entry_dir[nextedge])) {
              finalized = true;
              enter     = nextedge;
              break;
            }

            while (1) {
              found_entry = false;

              if (count > 4)
                throw new Error('Direction change counter overflow! This should never happen!');

              if (!((typeof cellGrid[x] === 'undefined') ||
                    (typeof cellGrid[x][y] === 'undefined'))) {
                cc = cellGrid[x][y];

                /* check for re-entry */
                ve = valid_entries[dir];
                if (typeof cc.edges[ve] === 'object') {
                  /* found re-entry */
                  ee = cc.edges[ve];
                  path.push(entry_coordinate(x, y, dir, ee.path));
                  enter = ve;
                  found_entry = true;
                  break;
                }
              }

              if (found_entry) {
                break;
              } else {
                path.push(skip_coordinate(x, y, dir));

                x += add_x[dir];
                y += add_y[dir];

                /* change direction if we'e moved out of grid again */
                if ((typeof cellGrid[x] === 'undefined') ||
                  (typeof cellGrid[x][y] === 'undefined')) {
                  if (((dir === 0) && (y < 0)) ||
                      ((dir === 1) && (x < 0)) ||
                      ((dir === 2) && (y === rows)) ||
                      ((dir === 3) && (x === cols))) {
                    x -= add_x[dir];
                    y -= add_y[dir];

                    dir = (dir + 1) % 4;
                    count++;
                  }
                }

                if ((x === i) && (y === j) && (dir === entry_dir[nextedge])) {
                  /* we are back where we started off, so finalize the polygon */
                  finalized = true;
                  enter     = nextedge;
                  break;
                }
              }
            }
          }
        }

        if ((settings.linearRing) &&
            ((path[path.length - 1][0] !== origin[0]) ||
            (path[path.length - 1][1] !== origin[1])))
          path.push(origin);

        polygons.push(path);
      } /* end forall entry sites */
    }); /* end foreach i */
  }); /* end foreach j */

  return polygons;
}


export {cell2Polygons, traceBandPaths, traceLinePaths};