module JS = {
  [@bs.deriving abstract]
  type distJs = {
    xs: array(float),
    ys: array(float),
  };

  let jsToDist = (d: distJs): DistTypes.xyShape => {
    xs: xsGet(d),
    ys: ysGet(d),
  };

  [@bs.module "./KdeLibrary.js"]
  external samplesToContinuousPdf: (array(float), int, int) => distJs =
    "samplesToContinuousPdf";
};

module KDE = {
  let normalSampling = (samples, outputXYPoints, kernelWidth) => {
    samples
    |> JS.samplesToContinuousPdf(_, outputXYPoints, kernelWidth)
    |> JS.jsToDist;
  };

  // Note: This was an experiment, but it didn't actually work that well.
  let inGroups = (samples, outputXYPoints, kernelWidth, ~cuttoff=0.9, ()) => {
    let partitionAt =
      samples
      |> E.A.length
      |> float_of_int
      |> (e => e *. cuttoff)
      |> int_of_float;
    let part1XYPoints =
      outputXYPoints |> float_of_int |> (e => e *. cuttoff) |> int_of_float;
    let part2XYPoints = outputXYPoints - part1XYPoints |> Js.Math.max_int(30);
    let part1Data =
      samples |> Belt.Array.slice(_, ~offset=0, ~len=partitionAt);
    let part2DataLength = (samples |> E.A.length) - partitionAt;
    let part2Data =
      samples
      |> Belt.Array.slice(
           _,
           ~offset=(-1) * part2DataLength,
           ~len=part2DataLength,
         );
    let part1 =
      part1Data
      |> JS.samplesToContinuousPdf(_, part1XYPoints, kernelWidth)
      |> JS.jsToDist;
    let part2 =
      part2Data
      |> JS.samplesToContinuousPdf(_, part2XYPoints, 3)
      |> JS.jsToDist;
    let opp = 1.0 -. cuttoff;
    part1;
  };
};

module T = {
  type t = array(float);

  let splitContinuousAndDiscrete = (sortedArray: t) => {
    let continuous = [||];
    let discrete = E.FloatFloatMap.empty();
    Belt.Array.forEachWithIndex(
      sortedArray,
      (index, element) => {
        let maxIndex = (sortedArray |> Array.length) - 1;
        let possiblySimilarElements =
          (
            switch (index) {
            | 0 => [|index + 1|]
            | n when n == maxIndex => [|index - 1|]
            | _ => [|index - 1, index + 1|]
            }
          )
          |> Belt.Array.map(_, r => sortedArray[r]);
        let hasSimilarElement =
          Belt.Array.some(possiblySimilarElements, r => r == element);
        hasSimilarElement
          ? E.FloatFloatMap.increment(element, discrete)
          : {
            let _ = Js.Array.push(element, continuous);
            ();
          };
        ();
      },
    );
    (continuous, discrete);
  };

  let xWidthToUnitWidth = (samples, outputXYPoints, xWidth) => {
    let xyPointRange = E.A.Sorted.range(samples) |> E.O.default(0.0);
    let xyPointWidth = xyPointRange /. float_of_int(outputXYPoints);
    xWidth /. xyPointWidth;
  };

  let formatUnitWidth = w => Jstat.max([|w, 1.0|]) |> int_of_float;

  let suggestedUnitWidth = (samples, outputXYPoints) => {
    let suggestedXWidth = Bandwidth.nrd0(samples);
    xWidthToUnitWidth(samples, outputXYPoints, suggestedXWidth);
  };

  let kde = (~samples, ~outputXYPoints, width) => {
    KDE.normalSampling(samples, outputXYPoints, width);
  };

  let toShape =
      (
        ~samples: t,
        ~samplingInputs: RenderTypes.ShapeRenderer.Sampling.Inputs.fInputs,
        (),
      ) => {
    Array.fast_sort(compare, samples);
    let (continuousPart, discretePart) = E.A.Sorted.Floats.split(samples);
    let length = samples |> E.A.length |> float_of_int;
    let discrete: DistTypes.discreteShape =
      discretePart
      |> E.FloatFloatMap.fmap(r => r /. length)
      |> E.FloatFloatMap.toArray
      |> XYShape.T.fromZippedArray
      |> Distributions.Discrete.make(_, None);

    let pdf =
      continuousPart |> E.A.length > 5
        ? {
          let _suggestedXWidth = Bandwidth.nrd0(continuousPart);
          let _suggestedUnitWidth =
            suggestedUnitWidth(continuousPart, samplingInputs.outputXYPoints);
          let usedWidth =
            samplingInputs.kernelWidth |> E.O.default(_suggestedXWidth);
          let usedUnitWidth =
            xWidthToUnitWidth(
              samples,
              samplingInputs.outputXYPoints,
              usedWidth,
            );
          let foo: RenderTypes.ShapeRenderer.Sampling.samplingStats = {
            sampleCount: samplingInputs.sampleCount,
            outputXYPoints: samplingInputs.outputXYPoints,
            bandwidthXSuggested: _suggestedXWidth,
            bandwidthUnitSuggested: _suggestedUnitWidth,
            bandwidthXImplemented: usedWidth,
            bandwidthUnitImplemented: usedUnitWidth,
          };
          continuousPart
          |> kde(
               ~samples=_,
               ~outputXYPoints=samplingInputs.outputXYPoints,
               formatUnitWidth(usedUnitWidth),
             )
          |> Distributions.Continuous.make(`Linear, _, None)
          |> (r => Some((r, foo)));
        }
        : None;
    let shape =
      MixedShapeBuilder.buildSimple(
        ~continuous=pdf |> E.O.fmap(fst),
        ~discrete=Some(discrete),
      );
    let samplesParse: RenderTypes.ShapeRenderer.Sampling.outputs = {
      continuousParseParams: pdf |> E.O.fmap(snd),
      shape,
    };
    samplesParse;
  };

  let fromSamples =
      (
        ~samplingInputs=RenderTypes.ShapeRenderer.Sampling.Inputs.empty,
        samples,
      ) => {
    let samplingInputs =
      RenderTypes.ShapeRenderer.Sampling.Inputs.toF(samplingInputs);
    toShape(~samples, ~samplingInputs, ());
  };

  let fromGuesstimatorString =
      (
        ~guesstimatorString,
        ~samplingInputs=RenderTypes.ShapeRenderer.Sampling.Inputs.empty,
        (),
      ) => {
    let hasValidSamples =
      Guesstimator.stringToSamples(guesstimatorString, 10) |> E.A.length > 0;
    let _samplingInputs =
      RenderTypes.ShapeRenderer.Sampling.Inputs.toF(samplingInputs);
    switch (hasValidSamples) {
    | false => None
    | true =>
      let samples =
        Guesstimator.stringToSamples(
          guesstimatorString,
          _samplingInputs.sampleCount,
        );
      Some(fromSamples(~samplingInputs, samples));
    };
  };
};