import {bandwidthNRD} from 'vega-statistics';
import {array, constant, error, isFunction, one} from 'vega-util';
import {sum} from 'd3-array';

function radius(bw, data, f) {
  const v = bw >= 0 ? bw : bandwidthNRD(data, f);
  return Math.round((Math.sqrt(4 * v * v + 1) - 1) / 2);
}

function number(_) {
  return isFunction(_) ? _ : constant(+_);
}

// Implementation adapted from d3/d3-contour. Thanks!
export default function() {
  var x = d => d[0],
      y = d => d[1],
      weight = one,
      bandwidth = [-1, -1],
      dx = 960,
      dy = 500,
      k = 2; // log2(cellSize)

  function density(data, counts) {
    const rx = radius(bandwidth[0], data, x) >> k, // blur x-radius
          ry = radius(bandwidth[1], data, y) >> k, // blur y-radius
          ox = rx ? rx + 2 : 0, // x-offset padding for blur
          oy = ry ? ry + 2 : 0, // y-offset padding for blur
          n = 2 * ox + (dx >> k), // grid width
          m = 2 * oy + (dy >> k), // grid height
          values0 = new Float32Array(n * m),
          values1 = new Float32Array(n * m);

    let values = values0;

    data.forEach(d => {
      const xi = ox + (+x(d) >> k),
            yi = oy + (+y(d) >> k);

      if (xi >= 0 && xi < n && yi >= 0 && yi < m) {
        values0[xi + yi * n] += +weight(d);
      }
    });

    if (rx > 0 && ry > 0) {
      blurX(n, m, values0, values1, rx);
      blurY(n, m, values1, values0, ry);
      blurX(n, m, values0, values1, rx);
      blurY(n, m, values1, values0, ry);
      blurX(n, m, values0, values1, rx);
      blurY(n, m, values1, values0, ry);
    } else if (rx > 0) {
      blurX(n, m, values0, values1, rx);
      blurX(n, m, values1, values0, rx);
      blurX(n, m, values0, values1, rx);
      values = values1;
    } else if (ry > 0) {
      blurY(n, m, values0, values1, ry);
      blurY(n, m, values1, values0, ry);
      blurY(n, m, values0, values1, ry);
      values = values1;
    }

    // scale density estimates
    // density in points per square pixel or probability density
    const s = counts ? Math.pow(2, -2 * k) : 1 / sum(values);
    for (let i=0, sz=n*m; i<sz; ++i) values[i] *= s;

    return {
      values: values,
      scale: 1 << k,
      width: n,
      height: m,
      x1: ox,
      y1: oy,
      x2: ox + (dx >> k),
      y2: oy + (dy >> k)
    };
  }

  density.x = function(_) {
    return arguments.length ? (x = number(_), density) : x;
  };

  density.y = function(_) {
    return arguments.length ? (y = number(_), density) : y;
  };

  density.weight = function(_) {
    return arguments.length ? (weight = number(_), density) : weight;
  };

  density.size = function(_) {
    if (!arguments.length) return [dx, dy];
    var _0 = +_[0], _1 = +_[1];
    if (!(_0 >= 0 && _1 >= 0)) error('invalid size');
    return dx = _0, dy = _1, density;
  };

  density.cellSize = function(_) {
    if (!arguments.length) return 1 << k;
    if (!((_ = +_) >= 1)) error('invalid cell size');
    k = Math.floor(Math.log(_) / Math.LN2);
    return density;
  };

  density.bandwidth = function(_) {
    if (!arguments.length) return bandwidth;
    _ = array(_);
    if (_.length === 1) _ = [+_[0], +_[0]];
    if (_.length !== 2) error('invalid bandwidth');
    return bandwidth = _, density;
  };

  return density;
}

function blurX(n, m, source, target, r) {
  const w = (r << 1) + 1;
  for (let j = 0; j < m; ++j) {
    for (let i = 0, sr = 0; i < n + r; ++i) {
      if (i < n) {
        sr += source[i + j * n];
      }
      if (i >= r) {
        if (i >= w) {
          sr -= source[i - w + j * n];
        }
        target[i - r + j * n] = sr / Math.min(i + 1, n - 1 + w - i, w);
      }
    }
  }
}

function blurY(n, m, source, target, r) {
  const w = (r << 1) + 1;
  for (let i = 0; i < n; ++i) {
    for (let j = 0, sr = 0; j < m + r; ++j) {
      if (j < m) {
        sr += source[i + j * n];
      }
      if (j >= r) {
        if (j >= w) {
          sr -= source[i + (j - w) * n];
        }
        target[i + (j - r) * n] = sr / Math.min(j + 1, m - 1 + w - j, w);
      }
    }
  }
}
