import 'package:flutter/material.dart';

/// BBCode parser
///
/// @author Pierre HUBERT

/// This callback return null if the text has to be left as is or a TextSpan
/// if it has been sub parsed...
typedef ParseCallBack = List<InlineSpan> Function(TextStyle, String);

class BBCodeParsedWidget extends StatelessWidget {
  final _Element _content;
  final ParseCallBack parseCallback;

  BBCodeParsedWidget({@required String text, this.parseCallback})
      : assert(text != null),
        _content = _parse(text);

  _printRecur(_Element el, {int pos = 0}) {
    String str;
    str = "".padLeft(pos, "*");
    if (el.text != null) print(str + el.text);
    el.children.forEach((f) => _printRecur(f, pos: pos + 1));
  }

  @override
  Widget build(BuildContext context) {
    _printRecur(_content);
    return RichText(
      text: _content.toTextSpan(context, parseCallback),
    );
  }

  /// Initialize parsing
  static _Element _parse(String text, {_ElementStyle style}) {
    try {
      return _parseRecur(
        text: text,
        style: _ElementStyle.empty(),
        pos: 0,
      ).el;
    } catch (e) {
      print("BBCode parse error: " + e.toString());
      print("Could not parse text!");
      return _Element(text: text, style: _ElementStyle.empty());
    }
  }

  /// Recursive parsing
  static _ElementRecur _parseRecur({
    @required String text,
    @required _ElementStyle style,
    @required int pos,
    String parentTag,
  }) {
    _Element el = _Element(style: style.clone());

    int lastBeginPos = pos;
    int childNumber = 0;
    bool stop = false;
    while (!stop && pos < text.length) {
      //Go to next stop
      while (!stop && pos < text.length) {
        if (text[pos] == '[') break;
        pos++;
      }

      //Check for text with default style to apply
      if (lastBeginPos != pos)
        el.children.add(_Element(
            style: style.clone(), text: text.substring(lastBeginPos, pos)));

      //Check if the [ tag is alone
      if (pos == text.length)
        break;
      else if (!text.contains("]", pos) ||
          (text.contains("[", pos + 1) &&
              text.indexOf("]", pos) > text.indexOf("[", pos + 1)))
        el.children.add(_Element(style: style.clone(), text: "["));

      //Check if we have to stop recursion
      else if (text[pos + 1] == "/") {
        pos = text.indexOf("]", pos);
        break;
      }

      //Check if we have to enter recursion
      else {
        // Prepare tag detection
        final closeBrace = text.indexOf("]", pos);
        String tag = text.substring(pos + 1, closeBrace);
        String arg;
        final newStyle = style.clone();

        //Check for argument
        if (tag.contains("=")) {
          final s = tag.split("=");
          tag = s[0];
          arg = s[1];
        }

        if (tag.length == 0) throw "This BBCode is invalid!";

        _preParseTag(tag, newStyle, arg);

        final subParse = _parseRecur(
          text: text,
          pos: closeBrace + 1,
          style: newStyle,
          parentTag: tag,
        );

        pos = subParse.finalPos;
        _postParseTag(tag, subParse.el,
            arg: arg, parentTag: parentTag, childNumber: childNumber);

        el.children.add(subParse.el);
      }

      pos++;
      lastBeginPos = pos;
      childNumber++;
    }

    return _ElementRecur(el: el, finalPos: pos);
  }

  /// Pre-parse tag
  static void _preParseTag(String tag, _ElementStyle style, [String arg]) {
    switch (tag) {
      // Bold
      case "b":
        style.fontWeight = FontWeight.bold;
        break;

      // Italic
      case "i":
        style.fontStyle = FontStyle.italic;
        break;

      // Underline
      case "u":
        style.decoration = TextDecoration.underline;
        break;

      // Strike through
      case "s":
        style.decoration = TextDecoration.lineThrough;
        break;

      // Color
      case "color":
        assert(arg != null);
        style.color = Color.fromARGB(
          255,
          int.tryParse(arg.substring(1, 3), radix: 16),
          int.tryParse(arg.substring(3, 5), radix: 16),
          int.tryParse(arg.substring(5, 7), radix: 16),
        );
        break;

      // Not found
      default:
        print("Tag " + tag + " not understood!");
        break;
    }
  }

  /// Post-parse tag
  static void _postParseTag(String tag, _Element el,
      {String arg, String parentTag, int childNumber}) {
    // List container
    if (tag == "ul" || tag == "ol")
      el.children.insert(0, _Element(style: el.style, text: "\n"));

    // List children
    if (tag == "li") {
      el.children.add(_Element(style: el.style, text: "\n"));
      if (parentTag == "ol")
        el.children.insert(
            0, _Element(style: el.style, text: " ${childNumber + 1}. "));
      else
        el.children.insert(0, _Element(style: el.style, text: " \u2022 "));
    }
  }
}

/// An element's style
class _ElementStyle {
  TextDecoration decoration;
  FontWeight fontWeight;
  FontStyle fontStyle;
  Color color;

  /// Generate an empty style
  _ElementStyle.empty();

  /// Construct an instance of this element
  _ElementStyle(
      {@required this.decoration,
      @required this.fontWeight,
      @required this.fontStyle,
      @required this.color});

  /// Clone this style
  _ElementStyle clone() {
    return _ElementStyle(
        decoration: decoration,
        fontWeight: fontWeight,
        fontStyle: fontStyle,
        color: color);
  }

  /// Generate corresponding TextStyle
  TextStyle toTextStyle(BuildContext context) {
    return Theme.of(context).textTheme.body1.copyWith(
        decoration: decoration,
        fontWeight: fontWeight,
        fontStyle: fontStyle,
        color: color);
  }
}

/// An element
class _Element {
  /// Note : if text is not null, children must be empty !!!
  String text;
  final _ElementStyle style;
  final List<_Element> children = List();

  _Element({@required this.style, this.text});

  /// Turn this element into a TextSpan
  TextSpan toTextSpan(BuildContext context, ParseCallBack parseCallback) {
    assert(text == null || children.length == 0);

    final generatedStyle = this.style.toTextStyle(context);

    if (parseCallback != null && text != null) {
      final parsed = parseCallback(generatedStyle, text);

      if (parsed != null && parsed.length > 0)
        return TextSpan(
          style: generatedStyle,
          children: parsed,
        );
    }

    return TextSpan(
      text: text,
      style: generatedStyle,
      children:
          children.map((f) => f.toTextSpan(context, parseCallback)).toList(),
    );
  }
}

class _ElementRecur {
  final _Element el;
  final int finalPos;

  const _ElementRecur({this.el, this.finalPos});
}