/// XML pretty printer and highlighter.
library xml_pp;

import 'dart:io';

import 'package:args/args.dart' as args;
import 'package:xml/xml.dart';

const entityMapping = XmlDefaultEntityMapping.xml();

const String ansiReset = '\u001b[0m';
const String ansiRed = '\u001b[31m';
const String ansiGreen = '\u001b[32m';
const String ansiYellow = '\u001b[33m';
const String ansiBlue = '\u001b[34m';
const String ansiMagenta = '\u001b[35m';
const String ansiCyan = '\u001b[36m';

const String attributeStyle = ansiBlue;
const String cdataStyle = ansiYellow;
const String commentStyle = ansiGreen;
const String declarationStyle = ansiCyan;
const String doctypeStyle = ansiCyan;
const String documentStyle = ansiReset;
const String documentFragmentStyle = ansiCyan;
const String elementStyle = ansiMagenta;
const String nameStyle = ansiRed;
const String processingStyle = ansiCyan;
const String textStyle = ansiReset;

final args.ArgParser argumentParser = args.ArgParser()
  ..addFlag(
    'color',
    abbr: 'c',
    help: 'Colorize the output.',
    defaultsTo: stdout.supportsAnsiEscapes,
  )
  ..addOption(
    'indent',
    abbr: 'i',
    help: 'Customize the indention when pretty printing.',
    defaultsTo: '  ',
  )
  ..addOption(
    'newline',
    abbr: 'n',
    help: 'Change the newline character when pretty printing.',
    defaultsTo: '\n',
  )
  ..addFlag(
    'pretty',
    abbr: 'p',
    help: 'Reformat the output to be pretty.',
    defaultsTo: true,
  );

void printUsage() {
  stdout.writeln('Usage: xml_pp [options] {files}');
  stdout.writeln();
  stdout.writeln(argumentParser.usage);
  exit(1);
}

void main(List<String> arguments) {
  final files = <File>[];
  final results = argumentParser.parse(arguments);
  final color = results['color'] as bool;
  final indent = results['indent'] as String;
  final newLine = results['newline'] as String;
  final pretty = results['pretty'] as bool;

  for (final argument in results.rest) {
    final file = File(argument);
    if (file.existsSync()) {
      files.add(file);
    } else {
      stderr.writeln('File not found: $file');
      exit(2);
    }
  }
  if (files.isEmpty) {
    printUsage();
  }

  // Select the appropriate printing visitor. For simpler use-cases one would
  // just call `document.toXmlString(pretty: true, indent: '  ')`.
  final visitor = pretty
      ? (color
          ? XmlColoredPrettyWriter(stdout,
              entityMapping: entityMapping, indent: indent, newLine: newLine)
          : XmlPrettyWriter(stdout,
              entityMapping: entityMapping, indent: indent, newLine: newLine))
      : (color
          ? XmlColoredWriter(stdout, entityMapping: entityMapping)
          : XmlWriter(stdout, entityMapping: entityMapping));
  for (final file in files) {
    visitor.visit(XmlDocument.parse(file.readAsStringSync()));
  }
}

mixin ColoredWriter {
  StringSink get buffer;

  List<String> get styles;

  void style(String style, void Function() callback) {
    styles.add(style);
    buffer.write(style);
    callback();
    styles.removeLast();
    buffer.write(styles.isEmpty ? ansiReset : styles.last);
  }
}

class XmlColoredWriter extends XmlWriter with ColoredWriter {
  XmlColoredWriter(super.buffer, {super.entityMapping});

  @override
  final List<String> styles = [];

  @override
  void visitAttribute(XmlAttribute node) =>
      style(attributeStyle, () => super.visitAttribute(node));

  @override
  void visitCDATA(XmlCDATA node) =>
      style(cdataStyle, () => super.visitCDATA(node));

  @override
  void visitComment(XmlComment node) =>
      style(commentStyle, () => super.visitComment(node));

  @override
  void visitDeclaration(XmlDeclaration node) =>
      style(declarationStyle, () => super.visitDeclaration(node));

  @override
  void visitDocument(XmlDocument node) =>
      style(documentStyle, () => super.visitDocument(node));

  @override
  void visitDocumentFragment(XmlDocumentFragment node) =>
      style(documentFragmentStyle, () => super.visitDocumentFragment(node));

  @override
  void visitDoctype(XmlDoctype node) =>
      style(doctypeStyle, () => super.visitDoctype(node));

  @override
  void visitElement(XmlElement node) =>
      style(elementStyle, () => super.visitElement(node));

  @override
  void visitName(XmlName name) => style(nameStyle, () => super.visitName(name));

  @override
  void visitProcessing(XmlProcessing node) =>
      style(processingStyle, () => super.visitProcessing(node));

  @override
  void visitText(XmlText node) => style(textStyle, () => super.visitText(node));
}

class XmlColoredPrettyWriter extends XmlPrettyWriter with ColoredWriter {
  XmlColoredPrettyWriter(super.buffer,
      {super.entityMapping, super.indent, super.newLine});
  @override
  final List<String> styles = [];

  @override
  void visitAttribute(XmlAttribute node) =>
      style(attributeStyle, () => super.visitAttribute(node));

  @override
  void visitCDATA(XmlCDATA node) =>
      style(cdataStyle, () => super.visitCDATA(node));

  @override
  void visitComment(XmlComment node) =>
      style(commentStyle, () => super.visitComment(node));

  @override
  void visitDeclaration(XmlDeclaration node) =>
      style(declarationStyle, () => super.visitDeclaration(node));

  @override
  void visitDocument(XmlDocument node) =>
      style(documentStyle, () => super.visitDocument(node));

  @override
  void visitDocumentFragment(XmlDocumentFragment node) =>
      style(documentFragmentStyle, () => super.visitDocumentFragment(node));

  @override
  void visitDoctype(XmlDoctype node) =>
      style(doctypeStyle, () => super.visitDoctype(node));

  @override
  void visitElement(XmlElement node) =>
      style(elementStyle, () => super.visitElement(node));

  @override
  void visitName(XmlName name) => style(nameStyle, () => super.visitName(name));

  @override
  void visitProcessing(XmlProcessing node) =>
      style(processingStyle, () => super.visitProcessing(node));

  @override
  void visitText(XmlText node) => style(textStyle, () => super.visitText(node));
}
