import 'dart:math' as math;

import '../../../../quill_delta.dart';
import '../../editor/embed/embed_editor_builder.dart';
import '../style.dart';
import 'embeddable.dart';
import 'line.dart';
import 'node.dart';

/// A leaf in Quill document tree.
abstract base class Leaf extends Node {
  /// Creates a new [Leaf] with specified [data].
  factory Leaf(Object data) {
    if (data is Embeddable) {
      return Embed(data);
    }
    final text = data as String;
    assert(text.isNotEmpty);
    return QuillText(text);
  }

  Leaf.val(Object val) : _value = val;

  /// Contents of this node, either a String if this is a [QuillText] or an
  /// [Embed] if this is an [BlockEmbed].
  Object get value => _value;

  set value(Object v) {
    _value = v;
    _length = null;
    clearOffsetCache();
  }

  Object _value;

  @override
  Line? get parent => super.parent as Line?;

  int? _length;

  @override
  int get length {
    if (_length != null) {
      return _length!;
    }
    if (_value is String) {
      _length = (_value as String).length;
    } else {
      // return 1 for embedded object
      _length = 1;
    }
    return _length!;
  }

  @override
  void clearLengthCache() {
    if (parent != null) {
      parent!.clearLengthCache();
    }
  }

  @override
  Delta toDelta() {
    final data =
        _value is Embeddable ? (_value as Embeddable).toJson() : _value;
    return Delta()..insert(data, style.toJson());
  }

  @override
  void insert(int index, Object data, Style? style) {
    final length = this.length;
    assert(index >= 0 && index <= length);
    final node = Leaf(data);
    if (index < length) {
      splitAt(index)!.insertBefore(node);
    } else {
      insertAfter(node);
    }
    node.format(style);
  }

  @override
  void retain(int index, int? len, Style? style) {
    if (style == null) {
      return;
    }

    final local = math.min(length - index, len!);
    final remain = len - local;
    final node = _isolate(index, local);

    if (remain > 0 && node.next != null) {
      node.next?.retain(0, remain, style);
    }
    node.format(style);
  }

  @override
  void delete(int index, int? len) {
    final length = this.length;
    assert(index < length);

    final local = math.min(length - index, len!);
    final target = _isolate(index, local);
    final prev = target.previous as Leaf?;
    final next = target.next as Leaf?;
    target.unlink();

    final remain = len - local;
    if (remain > 0 && next != null) {
      next.delete(0, remain);
    }

    if (prev != null) {
      prev.adjust();
    }
  }

  @override
  String toString() {
    final keys = style.keys.toList(growable: false)..sort();
    final styleKeys = keys.join();
    return '⟨$value⟩$styleKeys';
  }

  /// Adjust this text node by merging it with adjacent nodes if they share
  /// the same style.
  @override
  void adjust() {
    if (this is Embed) {
      // Embed nodes cannot be merged with text nor other embeds (in fact,
      // there could be no two adjacent embeds on the same line since an
      // embed occupies an entire line).
      return;
    }

    // This is a text node and it can only be merged with other text nodes.
    var node = this as QuillText;

    // Merging it with previous node if style is the same.
    final prev = node.previous;
    if (!node.isFirst && prev is QuillText && prev.style == node.style) {
      prev.value = prev.value + node.value;
      node.unlink();
      node = prev;
    }

    // Merging it with next node if style is the same.
    final next = node.next;
    if (!node.isLast && next is QuillText && next.style == node.style) {
      node.value = node.value + next.value;
      next.unlink();
    }
  }

  /// Splits this leaf node at [index] and returns new node.
  ///import '../style.dart';

  /// If this is the last node in its list and [index] equals this node's
  /// length then this method returns `null` as there is nothing left to split.
  /// If there is another leaf node after this one and [index] equals this
  /// node's length then the next leaf node is returned.
  ///
  /// If [index] equals to `0` then this node itself is returned unchanged.
  ///
  /// In case a new node is actually split from this one, it inherits this
  /// node's style.
  Leaf? splitAt(int index) {
    assert(index >= 0 && index <= length);
    if (index == 0) {
      return this;
    }
    if (index == length) {
      return isLast ? null : next as Leaf?;
    }

    assert(this is QuillText);
    final text = _value as String;
    value = text.substring(0, index);
    final split = Leaf(text.substring(index))..applyStyle(style);
    insertAfter(split);
    return split;
  }

  /// Cuts a leaf from [index] to the end of this node and returns new node
  /// in detached state (e.g. [mounted] returns `false`).
  ///
  /// Splitting logic is identical to one described in [splitAt], meaning this
  /// method may return `null`.
  Leaf? cutAt(int index) {
    assert(index >= 0 && index <= length);
    final cut = splitAt(index);
    cut?.unlink();
    return cut;
  }

  /// Formats this node and optimizes it with adjacent leaf nodes if needed.
  void format(Style? style) {
    if (style != null && style.isNotEmpty) {
      applyStyle(style);
    }
    adjust();
  }

  /// Isolates a new leaf starting at [index] with specified [length].
  ///
  /// Splitting logic is identical to one described in [splitAt], with one
  /// exception that it is required for [index] to always be less than this
  /// node's length. As a result this method always returns a [LeafNode]
  /// instance. Returned node may still be the same as this node
  /// if provided [index] is `0`.
  Leaf _isolate(int index, int length) {
    assert(
        index >= 0 && index < this.length && (index + length <= this.length));
    final target = splitAt(index)!..splitAt(length);
    return target;
  }
}

/// A span of formatted text within a line in a Quill document.
///
/// Text is a leaf node of a document tree.
///
/// Parent of a text node is always a [Line], and as a consequence text
/// node's [value] cannot contain any line-break characters.
///
/// See also:
///
///   * [Embed], a leaf node representing an embeddable object.
///   * [Line], a node representing a line of text.
///
/// Update:
/// The reason we are renamed quill Text to [QuillText] so it doesn't
/// conflict with the one from the widgets, material or cupertino library
///
base class QuillText extends Leaf {
  QuillText([String super.text = ''])
      : assert(!text.contains('\n')),
        super.val();

  @override
  Node newInstance() => QuillText(value);

  @override
  String get value => _value as String;

  @override
  String toPlainText([
    Iterable<EmbedBuilder>? embedBuilders,
    EmbedBuilder? unknownEmbedBuilder,
  ]) =>
      value;
}

/// An embed node inside of a line in a Quill document.
///
/// Embed node is a leaf node similar to [QuillText]. It represents an arbitrary
/// piece of non-textual content embedded into a document, such as, image,
/// horizontal rule, video, or any other object with defined structure,
/// like a tweet, for instance.
///
/// Embed node's length is always `1` character and it is represented with
/// unicode object replacement character in the document text.
///
/// Any inline style can be applied to an embed, however this does not
/// necessarily mean the embed will look according to that style. For instance,
/// applying "bold" style to an image gives no effect, while adding a "link" to
/// an image actually makes the image react to user's action.
base class Embed extends Leaf {
  Embed(Embeddable super.data) : super.val();

  // Refer to https://www.fileformat.info/info/unicode/char/fffc/index.htm
  static const kObjectReplacementCharacter = '\uFFFC';
  static const kObjectReplacementInt = 65532;

  @override
  Node newInstance() => throw UnimplementedError();

  @override
  Embeddable get value => super.value as Embeddable;

  // Embed nodes are represented as unicode object replacement character in
  // plain text.
  @override
  String toPlainText([
    Iterable<EmbedBuilder>? embedBuilders,
    EmbedBuilder? unknownEmbedBuilder,
  ]) {
    final builders = embedBuilders;

    if (builders != null) {
      for (final builder in builders) {
        if (builder.key == value.type) {
          return builder.toPlainText(this);
        }
      }
    }

    if (unknownEmbedBuilder != null) {
      return unknownEmbedBuilder.toPlainText(this);
    }

    return Embed.kObjectReplacementCharacter;
  }

  @override
  String toString() => '${super.toString()} ${value.type}';
}