/// IP Geolocation API using ip-api.com.
library ip_api;

import 'dart:convert';
import 'dart:io';

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

final httpClient = HttpClient();

final args.ArgParser argumentParser = args.ArgParser()
  ..addFlag(
    'incremental',
    abbr: 'i',
    defaultsTo: true,
    help: 'Incrementally parses and prints the response',
  )
  ..addMultiOption('fields',
      abbr: 'f',
      defaultsTo: ['status', 'message', 'query', 'country', 'city'],
      allowed: [
        'as',
        'asname',
        'city',
        'continent',
        'continentCode',
        'country',
        'countryCode',
        'currency',
        'district',
        'isp',
        'lat',
        'lon',
        'message',
        'mobile',
        'org',
        'proxy',
        'query',
        'region',
        'regionName',
        'reverse',
        'status',
        'timezone',
        'zip',
      ],
      help: 'Fields to be returned')
  ..addOption(
    'lang',
    abbr: 'l',
    defaultsTo: 'en',
    allowed: ['en', 'de', 'es', 'pt-BR', 'fr', 'ja', 'zh-CN', 'ru'],
    help: 'Localizes city, region and country names',
  )
  ..addFlag(
    'help',
    abbr: 'h',
    defaultsTo: false,
    help: 'Displays the help text',
  );

void printUsage() {
  stdout.writeln('Usage: ip_lookup -s [query]');
  stdout.writeln();
  stdout.writeln(argumentParser.usage);
  exit(1);
}

Future<void> lookupIp(args.ArgResults results, [String query = '']) async {
  final incremental = results['incremental'] as bool;
  final fields = results['fields'] as List<String>;
  final lang = results['lang'] as String;

  // Build the query URL, perform the request, and convert response to UTF-8.
  final url = Uri.parse(
      'http://ip-api.com/xml/$query?fields=${fields.join(',')}&lang=$lang');
  final request = await httpClient.getUrl(url);
  final response = await request.close();
  final stream = response.transform(utf8.decoder);

  // Typically you would only implement one of the following two approaches,
  // but for demonstration sake we show both in this example:
  if (incremental) {
    void textHandler(XmlEvent event, String text) =>
        stdout.writeln('${event.parent?.name}: $text');

    // Decode the input stream, normalize it, attach parent information,
    // select the events we are interested in, then print the information.
    // This approach uses less memory and is emitting results incrementally;
    // thought the implementation is more involved.
    await stream
        .toXmlEvents()
        .normalizeEvents()
        .withParentEvents()
        .selectSubtreeEvents((event) => event.parent?.name == 'query')
        .forEachEvent(
          onText: (event) => textHandler(event, event.value),
          onCDATA: (event) => textHandler(event, event.value),
        );
  } else {
    // Wait until we have the full response body, then parse the input to a
    // XML DOM tree and extract the information to be printed. This approach
    // uses more memory and waits for the complete data to be downloaded
    // and parsed before printing any results; thought the implementation is
    // simpler.
    final input = await stream.join();
    final document = XmlDocument.parse(input);
    for (final element in document.rootElement.childElements) {
      stdout.writeln('${element.name}: ${element.innerText}');
    }
  }
}

Future<void> main(List<String> arguments) async {
  final results = argumentParser.parse(arguments);

  if (results['help'] as bool) {
    printUsage();
  }

  if (results.rest.isEmpty) {
    await lookupIp(results);
  } else {
    for (final query in results.rest) {
      await lookupIp(results, query);
      stdout.writeln();
    }
  }
}
