function _extends() { _extends = Object.assign ? Object.assign.bind() : function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }

function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; }

function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { _defineProperty(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; }

function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }

function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest(); }

function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }

function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); }

function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; }

function _iterableToArrayLimit(arr, i) { var _i = arr == null ? null : typeof Symbol !== "undefined" && arr[Symbol.iterator] || arr["@@iterator"]; if (_i == null) return; var _arr = []; var _n = true; var _d = false; var _s, _e; try { for (_i = _i.call(arr); !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"] != null) _i["return"](); } finally { if (_d) throw _e; } } return _arr; }

function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; }

/*
Copyright (c) Uber Technologies, Inc.

This source code is licensed under the MIT license found in the
LICENSE file in the root directory of this source tree.
*/
import * as React from 'react';
import { Button, SIZE } from '../button';
import { ButtonGroup, MODE } from '../button-group';
import { Input, SIZE as INPUT_SIZE } from '../input';
import { useStyletron } from '../styles';
import Column from './column';
import { COLUMNS, NUMERICAL_FORMATS, MAX_BIN_COUNT, HISTOGRAM_SIZE } from './constants';
import FilterShell from './filter-shell';
import { LocaleContext } from '../locale'; // @ts-ignore

import { bin, max as maxFunc, extent, scaleLinear, median, bisector } from 'd3';
import { Slider } from '../slider';

function roundToFixed(value, precision) {
  var k = Math.pow(10, precision);
  return Math.round(value * k) / k;
} // @ts-ignore


function format(value, options) {
  if (typeof options.format === 'function') {
    return options.format(value);
  }

  var formatted = value.toString();

  switch (options.format) {
    case NUMERICAL_FORMATS.ACCOUNTING:
      {
        var abs = Math.abs(value);

        if (value < 0) {
          formatted = "($".concat(roundToFixed(abs, options.precision), ")");
          break;
        }

        formatted = "$".concat(roundToFixed(abs, options.precision));
        break;
      }

    case NUMERICAL_FORMATS.PERCENTAGE:
      {
        formatted = "".concat(roundToFixed(value, options.precision), "%");
        break;
      }

    case NUMERICAL_FORMATS.DEFAULT:
    default:
      formatted = roundToFixed(value, options.precision);
      break;
  }

  return formatted;
} // @ts-ignore


function validateInput(input) {
  return Boolean(parseFloat(input)) || input === '' || input === '-';
} // @ts-ignore


var bisect = bisector(function (d) {
  return d.x0;
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
var Histogram = /*#__PURE__*/React.memo(function Histogram(_ref) {
  var data = _ref.data,
      lower = _ref.lower,
      upper = _ref.upper,
      isRange = _ref.isRange,
      exclude = _ref.exclude,
      precision = _ref.precision;

  var _useStyletron = useStyletron(),
      _useStyletron2 = _slicedToArray(_useStyletron, 2),
      css = _useStyletron2[0],
      theme = _useStyletron2[1];

  var _React$useMemo = React.useMemo(function () {
    var bins = bin().thresholds(Math.min(data.length, MAX_BIN_COUNT))(data);
    var xScale = scaleLinear().domain([bins[0].x0, bins[bins.length - 1].x1]).range([0, HISTOGRAM_SIZE.width]).clamp(true);
    var yScale = scaleLinear() // @ts-ignore
    .domain([0, maxFunc(bins, function (d) {
      return d.length;
    })]).nice().range([HISTOGRAM_SIZE.height, 0]);
    return {
      bins: bins,
      xScale: xScale,
      yScale: yScale
    };
  }, [data]),
      bins = _React$useMemo.bins,
      xScale = _React$useMemo.xScale,
      yScale = _React$useMemo.yScale; // We need to find the index of bar which is nearest to the given single value


  var singleIndexNearest = React.useMemo(function () {
    if (isRange) {
      return null;
    }

    return bisect.center(bins, lower);
  }, [isRange, data, lower, upper]);
  return /*#__PURE__*/React.createElement("div", {
    className: css({
      display: 'flex',
      marginTop: theme.sizing.scale600,
      marginLeft: theme.sizing.scale200,
      marginRight: 0,
      marginBottom: theme.sizing.scale400,
      justifyContent: 'space-between',
      overflow: 'visible'
    })
  }, /*#__PURE__*/React.createElement("svg", HISTOGRAM_SIZE, bins.map(function (d, index) {
    var x = xScale(d.x0) + 1;
    var y = yScale(d.length);
    var width = Math.max(0, xScale(d.x1) - xScale(d.x0) - 1);
    var height = yScale(0) - yScale(d.length);
    var included;

    if (singleIndexNearest != null) {
      included = index === singleIndexNearest;
    } else {
      var withinLower = d.x1 > lower;
      var withinUpper = d.x0 <= upper;
      included = withinLower && withinUpper;
    }

    if (exclude) {
      included = !included;
    }

    return /*#__PURE__*/React.createElement("rect", {
      key: "bar-".concat(index),
      fill: included ? theme.colors.backgroundInversePrimary : theme.colors.backgroundSecondary,
      x: x,
      y: y,
      width: width,
      height: height
    });
  })));
}); // @ts-ignore

function NumericalFilter(props) {
  var _useStyletron3 = useStyletron(),
      _useStyletron4 = _slicedToArray(_useStyletron3, 2),
      css = _useStyletron4[0],
      theme = _useStyletron4[1];

  var locale = React.useContext(LocaleContext);
  var precision = props.options.precision; // The state handling of this component could be refactored and cleaned up if we used useReducer.

  var initialState = React.useMemo(function () {
    return props.filterParams || {
      exclude: false,
      excludeKind: 'range',
      comparatorIndex: 0,
      lowerValue: null,
      upperValue: null
    };
  }, [props.filterParams]);

  var _React$useState = React.useState(initialState.exclude),
      _React$useState2 = _slicedToArray(_React$useState, 2),
      exclude = _React$useState2[0],
      setExclude = _React$useState2[1]; // the api of our ButtonGroup forces these numerical indexes...
  // TODO look into allowing semantic names, similar to the radio component. Tricky part would be backwards compat


  var _React$useState3 = React.useState(function () {
    switch (initialState.excludeKind) {
      case 'value':
        return 1;

      case 'range':
      default:
        // fallthrough
        return 0;
    }
  }),
      _React$useState4 = _slicedToArray(_React$useState3, 2),
      comparatorIndex = _React$useState4[0],
      setComparatorIndex = _React$useState4[1]; // We use the d3 function to get the extent as it's a little more robust to null, -Infinity, etc.


  var _React$useMemo2 = React.useMemo(function () {
    return extent(props.data);
  }, [props.data]),
      _React$useMemo3 = _slicedToArray(_React$useMemo2, 2),
      min = _React$useMemo3[0],
      max = _React$useMemo3[1];

  var _React$useState5 = React.useState(function () {
    return roundToFixed(initialState.lowerValue || min, precision);
  }),
      _React$useState6 = _slicedToArray(_React$useState5, 2),
      lv = _React$useState6[0],
      setLower = _React$useState6[1];

  var _React$useState7 = React.useState(function () {
    return roundToFixed(initialState.upperValue || max, precision);
  }),
      _React$useState8 = _slicedToArray(_React$useState7, 2),
      uv = _React$useState8[0],
      setUpper = _React$useState8[1]; // We keep a separate value for the single select, to give a user the ability to toggle between
  // the range and single values without losing their previous input.


  var _React$useState9 = React.useState(function () {
    return roundToFixed(initialState.lowerValue || median(props.data), precision);
  }),
      _React$useState10 = _slicedToArray(_React$useState9, 2),
      sv = _React$useState10[0],
      setSingle = _React$useState10[1]; // This is the only conditional which we want to use to determine
  // if we are in range or single value mode.
  // Don't derive it via something else, e.g. lowerValue === upperValue, etc.


  var isRange = comparatorIndex === 0;
  var excludeKind = isRange ? 'range' : 'value'; // while the user is inputting values, we take their input at face value,
  // if we don't do this, a user can't input partial numbers, e.g. "-", or "3."

  var _React$useState11 = React.useState(false),
      _React$useState12 = _slicedToArray(_React$useState11, 2),
      focused = _React$useState12[0],
      setFocus = _React$useState12[1];

  var _React$useMemo4 = React.useMemo(function () {
    if (focused) {
      return [isRange ? lv : sv, uv];
    } // once the user is done inputting.
    // we validate then format to the given precision


    var l = isRange ? lv : sv;
    l = validateInput(l) ? l : min;
    var h = validateInput(uv) ? uv : max;
    return [roundToFixed(l, precision), roundToFixed(h, precision)];
  }, [isRange, focused, sv, lv, uv, precision]),
      _React$useMemo5 = _slicedToArray(_React$useMemo4, 2),
      inputValueLower = _React$useMemo5[0],
      inputValueUpper = _React$useMemo5[1]; // We have our slider values range from 1 to the bin size, so we have a scale which
  // takes in the data driven range and maps it to values the scale can always handle


  var sliderScale = React.useMemo(function () {
    return scaleLinear().domain([min, max]).rangeRound([1, MAX_BIN_COUNT]) // We clamp the values within our min and max even if a user enters a huge number
    .clamp(true);
  }, [min, max]);
  var sliderValue = isRange ? [sliderScale(inputValueLower), sliderScale(inputValueUpper)] : [sliderScale(inputValueLower)]; // keep the slider happy by sorting the two values

  if (isRange && sliderValue[0] > sliderValue[1]) {
    sliderValue = [sliderValue[1], sliderValue[0]];
  }

  return /*#__PURE__*/React.createElement(FilterShell, {
    exclude: exclude,
    onExcludeChange: function onExcludeChange() {
      return setExclude(!exclude);
    },
    excludeKind: excludeKind,
    onApply: function onApply() {
      if (isRange) {
        // @ts-expect-error todo(flow->ts)
        var lowerValue = parseFloat(inputValueLower); // @ts-expect-error todo(flow->ts)

        var upperValue = parseFloat(inputValueUpper);
        props.setFilter({
          description: "\u2265 ".concat(lowerValue, " and \u2264 ").concat(upperValue),
          exclude: exclude,
          lowerValue: lowerValue,
          upperValue: upperValue,
          excludeKind: excludeKind
        });
      } else {
        // @ts-expect-error todo(flow->ts)
        var _value = parseFloat(inputValueLower);

        props.setFilter({
          description: "= ".concat(_value),
          exclude: exclude,
          lowerValue: inputValueLower,
          upperValue: inputValueLower,
          excludeKind: excludeKind
        });
      }

      props.close();
    }
  }, /*#__PURE__*/React.createElement(ButtonGroup, {
    size: SIZE.mini,
    mode: MODE.radio,
    selected: comparatorIndex,
    onClick: function onClick(_, index) {
      return setComparatorIndex(index);
    },
    overrides: {
      Root: {
        style: function style(_ref2) {
          var $theme = _ref2.$theme;
          return {
            marginBottom: $theme.sizing.scale300
          };
        }
      }
    }
  }, /*#__PURE__*/React.createElement(Button, {
    type: "button",
    overrides: {
      BaseButton: {
        style: {
          width: '100%'
        }
      }
    },
    "aria-label": locale.datatable.numericalFilterRange
  }, locale.datatable.numericalFilterRange), /*#__PURE__*/React.createElement(Button, {
    type: "button",
    overrides: {
      BaseButton: {
        style: {
          width: '100%'
        }
      }
    },
    "aria-label": locale.datatable.numericalFilterSingleValue
  }, locale.datatable.numericalFilterSingleValue)), /*#__PURE__*/React.createElement(Histogram, {
    data: props.data,
    lower: inputValueLower,
    upper: inputValueUpper,
    isRange: isRange,
    exclude: exclude,
    precision: props.options.precision
  }), /*#__PURE__*/React.createElement("div", {
    className: css({
      display: 'flex',
      justifyContent: 'space-between'
    })
  }, /*#__PURE__*/React.createElement(Slider // The slider throws errors when switching between single and two values
  // when it tries to read getThumbDistance on a thumb which is not there anymore
  // if we create a new instance these errors are prevented.
  , {
    key: isRange.toString(),
    min: 1,
    max: MAX_BIN_COUNT,
    value: sliderValue,
    onChange: function onChange(_ref3) {
      var value = _ref3.value;

      if (!value) {
        return;
      } // we convert back from the slider scale to the actual data's scale


      if (isRange) {
        var _value2 = _slicedToArray(value, 2),
            lowerValue = _value2[0],
            upperValue = _value2[1];

        setLower(sliderScale.invert(lowerValue));
        setUpper(sliderScale.invert(upperValue));
      } else {
        var _value3 = _slicedToArray(value, 1),
            singleValue = _value3[0];

        setSingle(sliderScale.invert(singleValue));
      }
    },
    overrides: {
      InnerThumb: function InnerThumb(_ref4) {
        var $value = _ref4.$value,
            $thumbIndex = _ref4.$thumbIndex;
        return /*#__PURE__*/React.createElement(React.Fragment, null, $value[$thumbIndex]);
      },
      // eslint-disable-next-line @typescript-eslint/no-unused-vars
      TickBar: function TickBar(_ref5) {
        var $min = _ref5.$min,
            $max = _ref5.$max;
        return null;
      },
      // we don't want the ticks
      ThumbValue: function ThumbValue() {
        return null;
      },
      Root: {
        style: function style() {
          return {
            // Aligns the center of the slider handles with the histogram bars
            width: 'calc(100% + 14px)',
            margin: '0 -7px'
          };
        }
      },
      InnerTrack: {
        style: function style() {
          if (!isRange) {
            return {
              // For range selection we use the color as is, but when selecting the single value,
              // we don't want the track standing out, so mute its color
              background: theme.colors.backgroundSecondary
            };
          }
        }
      },
      Thumb: {
        style: function style() {
          return {
            // Slider handles are small enough to visually be centered within each histogram bar
            height: '18px',
            width: '18px',
            fontSize: '0px'
          };
        }
      }
    }
  })), /*#__PURE__*/React.createElement("div", {
    className: css({
      display: 'flex',
      marginTop: theme.sizing.scale400,
      // This % gap is visually appealing given the filter box width
      gap: '30%',
      justifyContent: 'space-between'
    })
  }, /*#__PURE__*/React.createElement(Input, {
    min: min,
    max: max,
    size: INPUT_SIZE.mini,
    overrides: {
      Root: {
        style: {
          width: '100%'
        }
      }
    },
    value: inputValueLower,
    onChange: function onChange(event) {
      if (validateInput(event.target.value)) {
        isRange ? // @ts-expect-error - we know it is a number by now
        setLower(event.target.value) : // @ts-expect-error - we know it is a number by now
        setSingle(event.target.value);
      }
    },
    onFocus: function onFocus() {
      return setFocus(true);
    },
    onBlur: function onBlur() {
      return setFocus(false);
    }
  }), isRange && /*#__PURE__*/React.createElement(Input, {
    min: min,
    max: max,
    size: INPUT_SIZE.mini,
    overrides: {
      Input: {
        style: {
          textAlign: 'right'
        }
      },
      Root: {
        style: {
          width: '100%'
        }
      }
    },
    value: inputValueUpper,
    onChange: function onChange(event) {
      if (validateInput(event.target.value)) {
        // @ts-expect-error - we know it is a number by now
        setUpper(event.target.value);
      }
    },
    onFocus: function onFocus() {
      return setFocus(true);
    },
    onBlur: function onBlur() {
      return setFocus(false);
    }
  })));
} // @ts-ignore


function NumericalCell(props) {
  var _useStyletron5 = useStyletron(),
      _useStyletron6 = _slicedToArray(_useStyletron5, 2),
      css = _useStyletron6[0],
      theme = _useStyletron6[1];

  return /*#__PURE__*/React.createElement("div", {
    className: css(_objectSpread(_objectSpread({}, theme.typography.MonoParagraphXSmall), {}, {
      display: 'flex',
      justifyContent: theme.direction !== 'rtl' ? 'flex-end' : 'flex-start',
      // @ts-ignore
      color: props.highlight(props.value) ? theme.colors.contentNegative : null,
      width: '100%'
    }))
  }, format(props.value, {
    format: props.format,
    precision: props.precision
  }));
}

var defaultOptions = {
  title: '',
  sortable: true,
  filterable: true,
  format: NUMERICAL_FORMATS.DEFAULT,
  highlight: function () {
    return false;
  },
  precision: 0
};

function NumericalColumn(options) {
  var normalizedOptions = _objectSpread(_objectSpread({}, defaultOptions), options);

  if (normalizedOptions.format !== NUMERICAL_FORMATS.DEFAULT && (options.precision === null || options.precision === undefined)) {
    normalizedOptions.precision = 2;
  }

  if (normalizedOptions.format === NUMERICAL_FORMATS.ACCOUNTING && (options.highlight === null || options.highlight === undefined)) {
    normalizedOptions.highlight = function (n) {
      return n < 0;
    };
  }

  return Column({
    kind: COLUMNS.NUMERICAL,
    buildFilter: function buildFilter(params) {
      return function (data) {
        var value = roundToFixed(data, normalizedOptions.precision);
        var included = value >= params.lowerValue && value <= params.upperValue;
        return params.exclude ? !included : included;
      };
    },
    cellBlockAlign: options.cellBlockAlign,
    fillWidth: options.fillWidth,
    filterable: normalizedOptions.filterable,
    mapDataToValue: options.mapDataToValue,
    maxWidth: options.maxWidth,
    minWidth: options.minWidth,
    renderCell: function RenderNumericalCell(props) {
      return /*#__PURE__*/React.createElement(NumericalCell, _extends({}, props, {
        format: normalizedOptions.format,
        highlight: normalizedOptions.highlight,
        precision: normalizedOptions.precision
      }));
    },
    renderFilter: function RenderNumericalFilter(props) {
      return /*#__PURE__*/React.createElement(NumericalFilter, _extends({}, props, {
        options: normalizedOptions
      }));
    },
    sortable: normalizedOptions.sortable,
    sortFn: function sortFn(a, b) {
      return a - b;
    },
    title: normalizedOptions.title
  });
}

export default NumericalColumn;