/*
 * @Description: Code highlight controller
 * @Author: ekibun
 * @Date: 2020-08-01 17:42:06
 * @LastEditors: ekibun
 * @LastEditTime: 2020-08-02 12:39:26
 */
import 'dart:math';
import 'dart:ui';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_highlight/themes/a11y-light.dart';
import 'package:highlight/highlight.dart';

Map<String, TextStyle> _theme = a11yLightTheme;
List<TextSpan> _convert(String code) {
  var nodes = highlight.parse(code, language: 'javascript').nodes;
  List<TextSpan> spans = [];
  var currentSpans = spans;
  List<List<TextSpan>> stack = [];

  _traverse(Node node) {
    if (node.value != null) {
      currentSpans.add(node.className == null
          ? TextSpan(text: node.value)
          : TextSpan(text: node.value, style: _theme[node.className]));
    } else if (node.children != null) {
      List<TextSpan> tmp = [];
      currentSpans.add(TextSpan(children: tmp, style: _theme[node.className]));
      stack.add(currentSpans);
      currentSpans = tmp;

      node.children.forEach((n) {
        _traverse(n);
        if (n == node.children.last) {
          currentSpans = stack.isEmpty ? spans : stack.removeLast();
        }
      });
    }
  }

  for (var node in nodes) {
    _traverse(node);
  }

  return spans;
}

class CodeInputController extends TextEditingController {
  CodeInputController({String text}) : super(text: text);

  TextSpan oldSpan = TextSpan();
  Future<void> spanCall;

  @override
  TextSpan buildTextSpan({TextStyle style, bool withComposing}) {
    String oldText = oldSpan.toPlainText();
    String newText = value.text;
    if (oldText == newText) return oldSpan;
    (spanCall?.timeout(Duration.zero) ?? Future.delayed(Duration.zero))
        .then((_) => spanCall = compute(_convert, value.text).then((lsSpan) {
              TextSpan newSpan = TextSpan(style: style, children: lsSpan);
              if (newSpan.toPlainText() == value.text) oldSpan = newSpan;
              notifyListeners();
            }))
        .catchError((_) => {});

    List<TextSpan> beforeSpans = [];
    int splitAt = value.selection.start;
    if (splitAt < 0) splitAt = newText.length ~/ 2;
    int start = 0;
    InlineSpan leftSpan;
    oldSpan.children?.indexWhere((element) {
      String elementText = element.toPlainText();
      if (start + elementText.length > splitAt || !newText.startsWith(elementText, start)) {
        leftSpan = element;
        return true;
      }
      beforeSpans.add(element);
      start += elementText.length;
      return false;
    });
    List<TextSpan> endSpans = [];
    int end = 0;
    InlineSpan rightSpan;
    oldSpan.children?.sublist(beforeSpans.length)?.lastIndexWhere((element) {
      String elementText = element.toPlainText();
      if (splitAt + end + elementText.length >= newText.length ||
          !newText.substring(start, newText.length - end).endsWith(elementText)) {
        rightSpan = element;
        return true;
      }
      endSpans.add(element);
      end += elementText.length;
      return false;
    });

    return TextSpan(style: style, children: [
      ...beforeSpans,
      TextSpan(
          style: leftSpan != null && leftSpan == rightSpan ? leftSpan.style : style,
          text: newText.substring(start, max(start, newText.length - end))),
      ...endSpans.reversed
    ]);
  }
}
