import { ref } from '@strudel.cycles/core';
import { WidgetType, ViewPlugin, Decoration } from '@codemirror/view';
import { StateEffect, StateField } from '@codemirror/state';

export let sliderValues = {};
const getSliderID = (from) => `slider_${from}`;

export class SliderWidget extends WidgetType {
  constructor(value, min, max, from, to, view) {
    super();
    this.value = value;
    this.min = min;
    this.max = max;
    this.from = from;
    this.originalFrom = from;
    this.to = to;
    this.view = view;
  }

  eq() {
    return false;
  }

  toDOM() {
    let wrap = document.createElement('span');
    wrap.setAttribute('aria-hidden', 'true');
    wrap.className = 'cm-slider'; // inline-flex items-center
    let slider = wrap.appendChild(document.createElement('input'));
    slider.type = 'range';
    slider.min = this.min;
    slider.max = this.max;
    slider.step = (this.max - this.min) / 1000;
    slider.originalValue = this.value;
    // to make sure the code stays in sync, let's save the original value
    // becuase .value automatically clamps values so it'll desync with the code
    slider.value = slider.originalValue;
    slider.from = this.from;
    slider.originalFrom = this.originalFrom;
    slider.to = this.to;
    slider.className = 'w-16 translate-y-1';
    this.slider = slider;
    slider.addEventListener('input', (e) => {
      const next = e.target.value;
      let insert = next;
      //let insert = next.toFixed(2);
      const to = slider.from + slider.originalValue.length;
      let change = { from: slider.from, to, insert };
      slider.originalValue = insert;
      slider.value = insert;
      this.view.dispatch({ changes: change });
      const id = getSliderID(slider.originalFrom); // matches id generated in transpiler
      window.postMessage({ type: 'cm-slider', value: Number(next), id });
    });
    return wrap;
  }

  ignoreEvent(e) {
    return true;
  }
}

export const setWidgets = StateEffect.define();

export const updateWidgets = (view, widgets) => {
  view.dispatch({ effects: setWidgets.of(widgets) });
};

function getWidgets(widgetConfigs, view) {
  return widgetConfigs.map(({ from, to, value, min, max }) => {
    return Decoration.widget({
      widget: new SliderWidget(value, min, max, from, to, view),
      side: 0,
    }).range(from /* , to */);
  });
}

export const sliderPlugin = ViewPlugin.fromClass(
  class {
    decorations; //: DecorationSet

    constructor(view /* : EditorView */) {
      this.decorations = Decoration.set([]);
    }

    update(update /* : ViewUpdate */) {
      update.transactions.forEach((tr) => {
        if (tr.docChanged) {
          this.decorations = this.decorations.map(tr.changes);
          const iterator = this.decorations.iter();
          while (iterator.value) {
            // when the widgets are moved, we need to tell the dom node the current position
            // this is important because the updateSliderValue function has to work with the dom node
            iterator.value.widget.slider.from = iterator.from;
            iterator.value.widget.slider.to = iterator.to;
            iterator.next();
          }
        }
        for (let e of tr.effects) {
          if (e.is(setWidgets)) {
            this.decorations = Decoration.set(getWidgets(e.value, update.view));
          }
        }
      });
    }
  },
  {
    decorations: (v) => v.decorations,
  },
);

// user api
export let slider = (id, value, min, max) => {
  sliderValues[id] = value; // sync state at eval time (code -> state)
  return ref(() => sliderValues[id]); // use state at query time
};
// update state when sliders are moved
if (typeof window !== 'undefined') {
  window.addEventListener('message', (e) => {
    if (e.data.type === 'cm-slider') {
      if (sliderValues[e.data.id] !== undefined) {
        // update state when slider is moved
        sliderValues[e.data.id] = e.data.value;
      } else {
        console.warn(`slider with id "${e.data.id}" is not registered. Only ${Object.keys(sliderValues)}`);
      }
    }
  });
}
