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,
    ),
  );
}
