import 'dart:convert' show ChunkedConversionSink;

import 'package:meta/meta.dart';

import '../../xml/exceptions/tag_exception.dart';
import '../../xml/extensions/parent.dart';
import '../../xml/nodes/attribute.dart';
import '../../xml/nodes/cdata.dart';
import '../../xml/nodes/comment.dart';
import '../../xml/nodes/declaration.dart';
import '../../xml/nodes/doctype.dart';
import '../../xml/nodes/element.dart';
import '../../xml/nodes/node.dart';
import '../../xml/nodes/processing.dart';
import '../../xml/nodes/text.dart';
import '../../xml/utils/name.dart';
import '../event.dart';
import '../events/cdata.dart';
import '../events/comment.dart';
import '../events/declaration.dart';
import '../events/doctype.dart';
import '../events/end_element.dart';
import '../events/processing.dart';
import '../events/start_element.dart';
import '../events/text.dart';
import '../utils/conversion_sink.dart';
import '../utils/event_attribute.dart';
import '../utils/list_converter.dart';
import '../visitor.dart';

extension XmlNodeDecoderExtension on Stream<List<XmlEvent>> {
  /// Converts a sequence of [XmlEvent] objects to [XmlNode] objects.
  Stream<List<XmlNode>> toXmlNodes() => transform(const XmlNodeDecoder());
}

/// A converter that decodes a sequence of [XmlEvent] objects to a forest of
/// [XmlNode] objects.
class XmlNodeDecoder extends XmlListConverter<XmlEvent, XmlNode> {
  const XmlNodeDecoder();

  @override
  ChunkedConversionSink<List<XmlEvent>> startChunkedConversion(
          Sink<List<XmlNode>> sink) =>
      _XmlNodeDecoderSink(sink);

  // Internal helper to efficiently convert an [Iterable] of [XmlEvent] to a
  // list of [XmlNodes].
  @internal
  List<XmlNode> convertIterable(Iterable<XmlEvent> events) {
    final result = <XmlNode>[];
    final sink =
        _XmlNodeDecoderSink(ConversionSink<List<XmlNode>>(result.addAll));
    events.forEach(sink.visit);
    return result;
  }
}

class _XmlNodeDecoderSink
    with XmlEventVisitor
    implements ChunkedConversionSink<List<XmlEvent>> {
  _XmlNodeDecoderSink(this.sink);

  final Sink<List<XmlNode>> sink;
  XmlElement? parent;

  @override
  void add(List<XmlEvent> chunk) => chunk.forEach(visit);

  @override
  void visitCDATAEvent(XmlCDATAEvent event) =>
      commit(XmlCDATA(event.value), event);

  @override
  void visitCommentEvent(XmlCommentEvent event) =>
      commit(XmlComment(event.value), event);

  @override
  void visitDeclarationEvent(XmlDeclarationEvent event) =>
      commit(XmlDeclaration(convertAttributes(event.attributes)), event);

  @override
  void visitDoctypeEvent(XmlDoctypeEvent event) => commit(
      XmlDoctype(event.name, event.externalId, event.internalSubset), event);

  @override
  void visitEndElementEvent(XmlEndElementEvent event) {
    if (parent == null) {
      throw XmlTagException.unexpectedClosingTag(event.name,
          buffer: event.buffer, position: event.start);
    }
    final element = parent!;
    XmlTagException.checkClosingTag(element.name.qualified, event.name,
        buffer: event.buffer, position: event.start);
    element.isSelfClosing = element.children.isNotEmpty;
    parent = element.parentElement;

    if (parent == null) {
      commit(element, event.parent);
    }
  }

  @override
  void visitProcessingEvent(XmlProcessingEvent event) =>
      commit(XmlProcessing(event.target, event.value), event);

  @override
  void visitStartElementEvent(XmlStartElementEvent event) {
    final element = XmlElement.tag(event.name,
        attributes: convertAttributes(event.attributes));
    if (event.isSelfClosing) {
      commit(element, event);
    } else {
      if (parent != null) {
        parent!.children.add(element);
      }
      parent = element;
    }
  }

  @override
  void visitTextEvent(XmlTextEvent event) =>
      commit(XmlText(event.value), event);

  @override
  void close() {
    if (parent != null) {
      throw XmlTagException.missingClosingTag(parent!.name.qualified);
    }
    sink.close();
  }

  void commit(XmlNode node, XmlEvent? event) {
    if (parent == null) {
      // If we have information about a parent event, create hidden
      // [XmlElement] nodes to make sure namespace resolution works
      // as expected.
      for (var outerElement = node, outerEvent = event?.parent;
          outerEvent != null;
          outerEvent = outerEvent.parent) {
        outerElement = XmlElement.tag(outerEvent.name,
            attributes: convertAttributes(outerEvent.attributes),
            children: [outerElement],
            isSelfClosing: outerEvent.isSelfClosing);
      }
      sink.add(<XmlNode>[node]);
    } else {
      parent!.children.add(node);
    }
  }

  Iterable<XmlAttribute> convertAttributes(
          Iterable<XmlEventAttribute> attributes) =>
      attributes.map((attribute) => XmlAttribute(
          XmlName.fromString(attribute.name),
          attribute.value,
          attribute.attributeType));
}
