// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:markdown/markdown.dart' as md;
import 'package:flutter/widgets.dart';
import 'package:flutter/gestures.dart';
import 'markdown_style_raw.dart';

typedef void MarkdownLinkCallback(String href);


/// A [Widget] that renders markdown formatted text. It supports all standard
/// markdowns from the original markdown specification found here:
/// https://daringfireball.net/projects/markdown/ The rendered markdown is
/// placed in a padded scrolling view port. If you do not want the scrolling
/// behaviour, use the [MarkdownBodyRaw] class instead.
class MarkdownRaw extends StatelessWidget {

  /// Creates a new Markdown [Widget] that renders the markdown formatted string
  /// passed in as [data]. By default the markdown will be rendered using the
  /// styles from the current theme, but you can optionally pass in a custom
  /// [markdownStyle] that specifies colors and fonts to use. Code blocks are
  /// by default not using syntax highlighting, but it's possible to pass in
  /// a custom [syntaxHighlighter].
  ///
  ///     new MarkdownRaw(data: "Hello _world_!", markdownStyle: myStyle);
  MarkdownRaw({
    this.data,
    this.markdownStyle,
    this.syntaxHighlighter,
    this.padding: const EdgeInsets.all(16.0),
    this.onTapLink
  });

  /// Markdown styled text
  final String data;

  /// Style used for rendering the markdown
  final MarkdownStyleRaw markdownStyle;

  /// The syntax highlighter used to color text in code blocks
  final SyntaxHighlighter syntaxHighlighter;

  /// Padding used
  final EdgeInsets padding;

  /// Callback when a link is tapped
  final MarkdownLinkCallback onTapLink;

  @override
  Widget build(BuildContext context) {
    return new ScrollableViewport(
      child: new Padding(
        padding: padding,
        child: createMarkdownBody(
          data: data,
          markdownStyle: markdownStyle,
          syntaxHighlighter: syntaxHighlighter,
          onTapLink: onTapLink
        )
      )
    );
  }

  MarkdownBodyRaw createMarkdownBody({
    String data,
    MarkdownStyleRaw markdownStyle,
    SyntaxHighlighter syntaxHighlighter,
    MarkdownLinkCallback onTapLink
  }) {
    return new MarkdownBodyRaw(
      data: data,
      markdownStyle: markdownStyle,
      syntaxHighlighter: syntaxHighlighter,
      onTapLink: onTapLink
    );
  }
}

/// A [Widget] that renders markdown formatted text. It supports all standard
/// markdowns from the original markdown specification found here:
/// https://daringfireball.net/projects/markdown/ This class doesn't implement
/// any scrolling behavior, if you want scrolling either wrap the widget in
/// a [ScrollableViewport] or use the [MarkdownRaw] widget.
class MarkdownBodyRaw extends StatefulWidget {

  /// Creates a new Markdown [Widget] that renders the markdown formatted string
  /// passed in as [data]. You need to pass in a [markdownStyle] that defines
  /// how the code is rendered. Code blocks are by default not using syntax
  /// highlighting, but it's possible to pass in a custom [syntaxHighlighter].
  ///
  /// Typically, you may want to wrap the [MarkdownBodyRaw] widget in a
  /// [Padding] and a [ScrollableViewport], or use the [Markdown class]
  ///
  ///     new ScrollableViewport(
  ///       child: new Padding(
  ///         padding: new EdgeInsets.all(16.0),
  ///         child: new MarkdownBodyRaw(
  ///           data: markdownSource,
  ///           markdownStyle: myStyle
  ///         )
  ///       )
  ///     )
  MarkdownBodyRaw({
    this.data,
    this.markdownStyle,
    this.syntaxHighlighter,
    this.onTapLink
  });

  /// Markdown styled text
  final String data;

  /// Style used for rendering the markdown
  final MarkdownStyleRaw markdownStyle;

  /// The syntax highlighter used to color text in code blocks
  final SyntaxHighlighter syntaxHighlighter;

  /// Callback when a link is tapped
  final MarkdownLinkCallback onTapLink;

  @override
  _MarkdownBodyRawState createState() => new _MarkdownBodyRawState();

  MarkdownStyleRaw createDefaultStyle(BuildContext context) => null;
}

class _MarkdownBodyRawState extends State<MarkdownBodyRaw> {

  @override
  void initState() {
    super.initState();
    _buildMarkdownCache();
  }

  @override
  void dispose() {
    _linkHandler.dispose();
    super.dispose();
  }

  @override
  void didUpdateConfig(MarkdownBodyRaw oldConfig) {
    super.didUpdateConfig(oldConfig);

    if (oldConfig.data != config.data ||
        oldConfig.markdownStyle != config.markdownStyle ||
        oldConfig.syntaxHighlighter != config.syntaxHighlighter ||
        oldConfig.onTapLink != config.onTapLink)
      _buildMarkdownCache();
  }

  void _buildMarkdownCache() {
    MarkdownStyleRaw markdownStyle = config.markdownStyle ?? config.createDefaultStyle(context);
    SyntaxHighlighter syntaxHighlighter = config.syntaxHighlighter ?? new _DefaultSyntaxHighlighter(markdownStyle.code);

    _linkHandler?.dispose();
    _linkHandler = new _LinkHandler(config.onTapLink);

    // TODO: This can be optimized by doing the split and removing \r at the same time
    List<String> lines = config.data.replaceAll('\r\n', '\n').split('\n');
    md.Document document = new md.Document();

    _Renderer renderer = new _Renderer();
    _cachedBlocks = renderer.render(document.parseLines(lines), markdownStyle, syntaxHighlighter, _linkHandler);
  }

  List<_Block> _cachedBlocks;
  _LinkHandler _linkHandler;

  @override
  Widget build(BuildContext context) {
    List<Widget> blocks = <Widget>[];
    for (_Block block in _cachedBlocks) {
      blocks.add(block.build(context));
    }

    return new Column(
      crossAxisAlignment: CrossAxisAlignment.stretch,
      children: blocks
    );
  }

  @override
  void debugFillDescription(List<String> description) {
    description.add('cached blocks identity: ${_cachedBlocks.hashCode}');
  }
}

class _Renderer implements md.NodeVisitor {
  List<_Block> render(List<md.Node> nodes, MarkdownStyleRaw markdownStyle, SyntaxHighlighter syntaxHighlighter, _LinkHandler linkHandler) {
    assert(markdownStyle != null);

    _blocks = <_Block>[];
    _listIndents = <String>[];
    _markdownStyle = markdownStyle;
    _syntaxHighlighter = syntaxHighlighter;
    _linkHandler = linkHandler;

    for (final md.Node node in nodes) {
      node.accept(this);
    }

    return _blocks;
  }

  List<_Block> _blocks;
  List<String> _listIndents;
  MarkdownStyleRaw _markdownStyle;
  SyntaxHighlighter _syntaxHighlighter;
  _LinkHandler _linkHandler;

  @override
  void visitText(md.Text text) {
    _MarkdownNodeList topList = _currentBlock.stack.last;
    List<_MarkdownNode> top = topList.list;

    if (_currentBlock.tag == 'pre')
      top.add(new _MarkdownNodeTextSpan(_syntaxHighlighter.format(text.text)));
    else
      top.add(new _MarkdownNodeString(text.text));
  }

  @override
  bool visitElementBefore(md.Element element) {
    if (_isListTag(element.tag))
      _listIndents.add(element.tag);

    if (_isBlockTag(element.tag)) {
      List<_Block> blockList;
      if (_currentBlock == null)
        blockList = _blocks;
      else
        blockList = _currentBlock.subBlocks;

      _Block newBlock = new _Block(element.tag, element.attributes, _markdownStyle, new List<String>.from(_listIndents), blockList.length);
      blockList.add(newBlock);
    } else {
      _LinkInfo linkInfo;
      if (element.tag == 'a') {
        linkInfo = _linkHandler.createLinkInfo(element.attributes['href']);
      }

      TextStyle style = _markdownStyle.styles[element.tag] ?? new TextStyle();
      List<_MarkdownNode> styleElement = <_MarkdownNode>[new _MarkdownNodeTextStyle(style, linkInfo)];
      _currentBlock.stack.add(new _MarkdownNodeList(styleElement));
    }
    return true;
  }

  @override
  void visitElementAfter(md.Element element) {
    if (_isListTag(element.tag))
      _listIndents.removeLast();

    if (_isBlockTag(element.tag)) {
      if (_currentBlock.stack.length > 0) {
        _MarkdownNodeList stackList = _currentBlock.stack.first;
        _currentBlock.stack = stackList.list;
        _currentBlock.open = false;
      } else {
        _currentBlock.stack = <_MarkdownNode>[new _MarkdownNodeString('')];
      }
    } else {
      if (_currentBlock.stack.length > 1) {
        _MarkdownNodeList poppedList = _currentBlock.stack.last;
        List<_MarkdownNode> popped = poppedList.list;
        _currentBlock.stack.removeLast();

        _MarkdownNodeList topList = _currentBlock.stack.last;
        List<_MarkdownNode> top = topList.list;
        top.add(new _MarkdownNodeList(popped));
      }
    }
  }

  static const List<String> _kBlockTags = const <String>['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'li', 'blockquote', 'img', 'pre', 'ol', 'ul'];
  static const List<String> _kListTags = const <String>['ul', 'ol'];

  bool _isBlockTag(String tag) {
    return _kBlockTags.contains(tag);
  }

  bool _isListTag(String tag) {
    return _kListTags.contains(tag);
  }

  _Block get _currentBlock => _currentBlockInList(_blocks);

  _Block _currentBlockInList(List<_Block> blocks) {
    if (blocks.isEmpty)
      return null;

    if (!blocks.last.open)
      return null;

    _Block childBlock = _currentBlockInList(blocks.last.subBlocks);
    if (childBlock != null)
      return childBlock;

    return blocks.last;
  }
}

abstract class _MarkdownNode {
}

class _MarkdownNodeList extends _MarkdownNode {
  _MarkdownNodeList(this.list);
  List<_MarkdownNode> list;
}

class _MarkdownNodeTextStyle extends _MarkdownNode {
  _MarkdownNodeTextStyle(this.style, [this.linkInfo = null]);
  TextStyle style;
  _LinkInfo linkInfo;
}

class _MarkdownNodeString extends _MarkdownNode {
  _MarkdownNodeString(this.string);
  String string;
}

class _MarkdownNodeTextSpan extends _MarkdownNode {
  _MarkdownNodeTextSpan(this.textSpan);
  TextSpan textSpan;
}

class _Block {
  _Block(this.tag, this.attributes, this.markdownStyle, this.listIndents, this.blockPosition) {
    TextStyle style = markdownStyle.styles[tag];
    if (style == null)
      style = new TextStyle(color: const Color(0xffff0000));

    stack = <_MarkdownNode>[new _MarkdownNodeList(<_MarkdownNode>[new _MarkdownNodeTextStyle(style)])];
    subBlocks = <_Block>[];
  }

  final String tag;
  final Map<String, String> attributes;
  final MarkdownStyleRaw markdownStyle;
  final List<String> listIndents;
  final int blockPosition;

  List<_MarkdownNode> stack;
  List<_Block> subBlocks;

  bool get open => _open;
  void set open(bool open) {
    _open = open;
    if (!open && subBlocks.length > 0)
      subBlocks.last.isLast = true;
  }

  bool _open = true;
  bool isLast = false;

  Widget build(BuildContext context) {

    if (tag == 'img') {
      return _buildImage(context, attributes['src']);
    }

    double spacing = markdownStyle.blockSpacing;
    if (isLast) spacing = 0.0;

    Widget contents;

    if (subBlocks.length > 0) {
      List<Widget> subWidgets = <Widget>[];
      for (_Block subBlock in subBlocks) {
        subWidgets.add(subBlock.build(context));
      }

      contents = new Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: subWidgets
      );
    } else {
      TextSpan span = _stackToTextSpan(new _MarkdownNodeList(stack));
      contents = new RichText(text: span);

      if (listIndents.length > 0) {
        Widget bullet;
        if (listIndents.last == 'ul') {
          bullet = new Text(
            '•',
            style: new TextStyle(textAlign: TextAlign.center)
          );
        }
        else {
          bullet = new Padding(
            padding: new EdgeInsets.only(right: 5.0),
            child: new Text(
              "${blockPosition + 1}.",
              style: new TextStyle(textAlign: TextAlign.right)
            )
          );
        }

        contents = new Row(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: <Widget>[
            new SizedBox(
              width: listIndents.length * markdownStyle.listIndent,
              child: bullet
            ),
            new Flexible(child: contents)
          ]
        );
      }
    }

    BoxDecoration decoration;
    EdgeInsets padding;

    if (tag == 'blockquote') {
      decoration = markdownStyle.blockquoteDecoration;
      padding = new EdgeInsets.all(markdownStyle.blockquotePadding);
    } else if (tag == 'pre') {
      decoration = markdownStyle.codeblockDecoration;
      padding = new EdgeInsets.all(markdownStyle.codeblockPadding);
    }

    return new Container(
      padding: padding,
      margin: new EdgeInsets.only(bottom: spacing),
      child: contents,
      decoration: decoration
    );
  }

  TextSpan _stackToTextSpan(_MarkdownNode stack) {
    if (stack is _MarkdownNodeTextSpan)
      return stack.textSpan;

    if (stack is _MarkdownNodeList) {
      List<_MarkdownNode> list = stack.list;
      _MarkdownNodeTextStyle styleNode = list[0];
      _LinkInfo linkInfo = styleNode.linkInfo;
      TextStyle style = styleNode.style;

      List<TextSpan> children = <TextSpan>[];
      for (int i = 1; i < list.length; i++) {
        children.add(_stackToTextSpan(list[i]));
      }

      String text;
      if (children.length == 1 && _isPlainText(children[0])) {
        text = children[0].text;
        children = null;
      }

      TapGestureRecognizer recognizer = linkInfo?.recognizer;

      return new TextSpan(style: style, children: children, recognizer: recognizer, text: text);
    }

    if (stack is _MarkdownNodeString) {
      return new TextSpan(text: stack.string);
    }

    return null;
  }

  bool _isPlainText(TextSpan span) {
    return (span.text != null && span.style == null && span.recognizer == null && span.children == null);
  }

  Widget _buildImage(BuildContext context, String src) {
    List<String> parts = src.split('#');
    if (parts.length == 0) return new Container();

    String path = parts.first;
    double width;
    double height;
    if (parts.length == 2) {
      List<String> dimensions = parts.last.split('x');
      if (dimensions.length == 2) {
        width = double.parse(dimensions[0]);
        height = double.parse(dimensions[1]);
      }
    }

    return new NetworkImage(src: path, width: width, height: height);
  }
}

class _LinkInfo {
  _LinkInfo(this.href, this.recognizer);

  final String href;
  final TapGestureRecognizer recognizer;
}

class _LinkHandler {
  _LinkHandler(this.onTapLink);

  List<_LinkInfo> links = <_LinkInfo>[];
  MarkdownLinkCallback onTapLink;

  _LinkInfo createLinkInfo(String href) {
    TapGestureRecognizer recognizer = new TapGestureRecognizer();
    recognizer.onTap = () {
      if (onTapLink != null)
        onTapLink(href);
    };

    _LinkInfo linkInfo = new _LinkInfo(href, recognizer);
    links.add(linkInfo);

    return linkInfo;
  }

  void dispose() {
    for (_LinkInfo linkInfo in links) {
      linkInfo.recognizer.dispose();
    }
  }
}

abstract class SyntaxHighlighter { // ignore: one_member_abstracts
  TextSpan format(String source);
}

class _DefaultSyntaxHighlighter extends SyntaxHighlighter{
  _DefaultSyntaxHighlighter(this.style);

  final TextStyle style;

  @override
  TextSpan format(String source) {
    return new TextSpan(style: style, children: <TextSpan>[new TextSpan(text: source)]);
  }
}
