import { UndoManager, createRelativePositionFromTypeIndex, createAbsolutePositionFromRelativePosition, createRelativePositionFromJSON } from '../../../../yjs@13.6.26/node_modules/yjs/dist/yjs.js';
import { EditorSelection, Facet, Annotation } from '../../../../@codemirror_state@6.5.2/node_modules/@codemirror/state/dist/index.js';
import { ViewPlugin } from '../../../../@codemirror_view@6.28.6/node_modules/@codemirror/view/dist/index.js';
import { YRange } from './y-range.js';

class YSyncConfig {
  constructor (ytext, awareness) {
    this.ytext = ytext;
    this.awareness = awareness;
    this.undoManager = new UndoManager(ytext);
  }

  /**
   * Helper function to transform an absolute index position to a Yjs-based relative position
   * (https://docs.yjs.dev/api/relative-positions).
   *
   * A relative position can be transformed back to an absolute position even after the document has changed. The position is
   * automatically adapted. This does not require any position transformations. Relative positions are computed based on
   * the internal Yjs document model. Peers that share content through Yjs are guaranteed that their positions will always
   * synced up when using relatve positions.
   *
   * ```js
   * import { ySyncFacet } from 'y-codemirror'
   *
   * ..
   * const ysync = view.state.facet(ySyncFacet)
   * // transform an absolute index position to a ypos
   * const ypos = ysync.getYPos(3)
   * // transform the ypos back to an absolute position
   * ysync.fromYPos(ypos) // => 3
   * ```
   *
   * It cannot be guaranteed that absolute index positions can be synced up between peers.
   * This might lead to undesired behavior when implementing features that require that all peers see the
   * same marked range (e.g. a comment plugin).
   *
   * @param {number} pos
   * @param {number} [assoc]
   */
  toYPos (pos, assoc = 0) {
    return createRelativePositionFromTypeIndex(this.ytext, pos, assoc)
  }

  /**
   * @param {Y.RelativePosition | Object} rpos
   */
  fromYPos (rpos) {
    const pos = createAbsolutePositionFromRelativePosition(createRelativePositionFromJSON(rpos), this.ytext.doc);
    if (pos == null || pos.type !== this.ytext) {
      throw new Error('[y-codemirror] The position you want to retrieve was created by a different document')
    }
    return {
      pos: pos.index,
      assoc: pos.assoc
    }
  }

  /**
   * @param {cmState.SelectionRange} range
   * @return {YRange}
   */
  toYRange (range) {
    const assoc = range.assoc;
    const yanchor = this.toYPos(range.anchor, assoc);
    const yhead = this.toYPos(range.head, assoc);
    return new YRange(yanchor, yhead)
  }

  /**
   * @param {YRange} yrange
   */
  fromYRange (yrange) {
    const anchor = this.fromYPos(yrange.yanchor);
    const head = this.fromYPos(yrange.yhead);
    if (anchor.pos === head.pos) {
      return EditorSelection.cursor(head.pos, head.assoc)
    }
    return EditorSelection.range(anchor.pos, head.pos)
  }
}

/**
 * @type {cmState.Facet<YSyncConfig, YSyncConfig>}
 */
const ySyncFacet = Facet.define({
  combine (inputs) {
    return inputs[inputs.length - 1]
  }
});

/**
 * @type {cmState.AnnotationType<YSyncConfig>}
 */
const ySyncAnnotation = Annotation.define();

/**
 * @extends {PluginValue}
 */
class YSyncPluginValue {
  /**
   * @param {cmView.EditorView} view
   */
  constructor (view) {
    this.view = view;
    this.conf = view.state.facet(ySyncFacet);
    this._observer = (event, tr) => {
      if (tr.origin !== this.conf) {
        const delta = event.delta;
        const changes = [];
        let pos = 0;
        for (let i = 0; i < delta.length; i++) {
          const d = delta[i];
          if (d.insert != null) {
            changes.push({ from: pos, to: pos, insert: d.insert });
          } else if (d.delete != null) {
            changes.push({ from: pos, to: pos + d.delete, insert: '' });
            pos += d.delete;
          } else {
            pos += d.retain;
          }
        }
        view.dispatch({ changes, annotations: [ySyncAnnotation.of(this.conf)] });
      }
    };
    this._ytext = this.conf.ytext;
    this._ytext.observe(this._observer);
  }

  /**
   * @param {cmView.ViewUpdate} update
   */
  update (update) {
    if (!update.docChanged || (update.transactions.length > 0 && update.transactions[0].annotation(ySyncAnnotation) === this.conf)) {
      return
    }
    const ytext = this.conf.ytext;
    ytext.doc.transact(() => {
      /**
       * This variable adjusts the fromA position to the current position in the Y.Text type.
       */
      let adj = 0;
      update.changes.iterChanges((fromA, toA, fromB, toB, insert) => {
        const insertText = insert.sliceString(0, insert.length, '\n');
        if (fromA !== toA) {
          ytext.delete(fromA + adj, toA - fromA);
        }
        if (insertText.length > 0) {
          ytext.insert(fromA + adj, insertText);
        }
        adj += insertText.length - (toA - fromA);
      });
    }, this.conf);
  }

  destroy () {
    this._ytext.unobserve(this._observer);
  }
}

const ySync = ViewPlugin.fromClass(YSyncPluginValue);

export { YSyncConfig, ySync, ySyncAnnotation, ySyncFacet };
