import 'dart:convert' show ChunkedConversionSink;

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/node_list.dart';
import '../../xml/visitors/visitor.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/event_attribute.dart';
import '../utils/list_converter.dart';

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

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

  @override
  ChunkedConversionSink<List<XmlNode>> startChunkedConversion(
    Sink<List<XmlEvent>> sink,
  ) => _XmlNodeEncoderSink(sink);
}

class _XmlNodeEncoderSink
    with XmlVisitor
    implements ChunkedConversionSink<List<XmlNode>> {
  _XmlNodeEncoderSink(this.sink);

  final Sink<List<XmlEvent>> sink;

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

  @override
  void close() => sink.close();

  @override
  void visitElement(XmlElement node) {
    final isSelfClosing = node.isSelfClosing && node.children.isEmpty;
    sink.add([
      XmlStartElementEvent(
        node.name.qualified,
        convertAttributes(node.attributes),
        isSelfClosing,
      ),
    ]);
    if (!isSelfClosing) {
      node.children.forEach(visit);
      sink.add([XmlEndElementEvent(node.name.qualified)]);
    }
  }

  @override
  void visitCDATA(XmlCDATA node) => sink.add([XmlCDATAEvent(node.value)]);

  @override
  void visitComment(XmlComment node) => sink.add([XmlCommentEvent(node.value)]);

  @override
  void visitDeclaration(XmlDeclaration node) =>
      sink.add([XmlDeclarationEvent(convertAttributes(node.attributes))]);

  @override
  void visitDoctype(XmlDoctype node) => sink.add([
    XmlDoctypeEvent(node.name, node.externalId, node.internalSubset),
  ]);

  @override
  void visitProcessing(XmlProcessing node) =>
      sink.add([XmlProcessingEvent(node.target, node.value)]);

  @override
  void visitText(XmlText node) => sink.add([XmlTextEvent(node.value)]);

  List<XmlEventAttribute> convertAttributes(
    XmlNodeList<XmlAttribute> attributes,
  ) => attributes
      .map(
        (attribute) => XmlEventAttribute(
          attribute.name.qualified,
          attribute.value,
          attribute.attributeType,
        ),
      )
      .toList(growable: false);
}
