// Copyright (c) 2014, the Dart project authors.  Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

/// Defines messages templates and an adapter for TransformLogger to be able
/// report error messages from transformers and refer to them in a consistent
/// manner long term.
library code_transformers.messages;

// Note: this library purposely doesn't depend on dart:io, dart:html, or barback
// so it can easily be used both in the transformers and in client-side apps
// (for example in the log_injector).
import 'dart:collection' show LinkedHashMap;

import 'package:source_span/source_span.dart';

/// A globally unique identifier for an error message. This identifier should be
/// stable, that is, it should never change after it is asigned to a particular
/// message. That allows us to document our error messages and make them
/// searchable for prosperity.
class MessageId implements Comparable<MessageId> {
  /// Name of the package that declares this message.
  final String package;

  /// Message identifier number, unique within the package.
  final int id;

  const MessageId(this.package, this.id);

  static const MessageId NOT_SPECIFIED = const MessageId('unknown', 0);

  /// Serialize this message. We use a string and not a map to encode ids so
  /// they can be used as keys in JSON maps.
  String toJson() => toString();

  toString() => '${package}#$id';

  int compareTo(MessageId other) {
    var res = package.compareTo(other.package);
    if (res != 0) return res;
    return id.compareTo(other.id);
  }

  /// Creates a new [MessageId] from an encoded value produced via [toJson].
  factory MessageId.fromJson(data) {
    var index = data.lastIndexOf('#');
    if (index == -1) throw 'Invalid message id: $data';
    return new MessageId(
        data.substring(0, index), int.parse(data.substring(index + 1)));
  }

  operator ==(Object other) =>
      other is MessageId && package == other.package && id == other.id;

  int get hashCode => 31 * package.hashCode + id;
}

/// An instance of an error message. These are typically produced from a
/// [MessageTemplate].
class Message {
  /// A globally unique identifier for this message.
  final MessageId id;

  /// A snippet message that is presented to the user.
  final String snippet;

  const Message(this.id, this.snippet);

  const Message.unknown(this.snippet) : id = MessageId.NOT_SPECIFIED;

  /// Serializes this message to JSON.
  Map toJson() => {'id': id.toJson(), 'snippet': snippet};
  String toString() => 'id: $id, snippet: $snippet';

  /// Creates a new [Message] from an encoded value produced via [toJson].
  factory Message.fromJson(data) =>
      new Message(new MessageId.fromJson(data['id']), data['snippet']);
}

/// Template for a message. Templates can include placeholders to indicate
/// values that are different for each instance of the error. Calling [create]
/// will generate the actual message, with the placeholders replaced with
/// values. If there are no placeholders, an instance of [MessageTemplate] is a
/// valid instance of [Message] as well.
class MessageTemplate implements Message {
  /// Unique and stable id for the message.
  final MessageId id;

  /// Template message with placeholders of the form `%-name-%`.
  final String snippetTemplate;

  /// This returns the message snippet, only if it the template has no
  /// placeholders, otherwise this throws an exception. Most messages have no
  /// placeholder arguments, in those cases, the snippet  can be computed
  /// without specifying any arguments (exactly like calling `create()` with no
  /// arguments).
  String get snippet => _createSnippet();

  /// Short description of the error message, typically used as a title of the
  /// error message in autogenerated documentation. This should be a single
  /// phrase, and cannot use placeholders.
  final String description;

  /// Additional details about this error message. These are used to
  /// automatically generate documentation.
  final String details;

  const MessageTemplate(
      this.id, this.snippetTemplate, this.description, this.details);

  static final _placeholderPattern = new RegExp(r"%-(\w*)-%");

  _createSnippet([Map args = const {}, bool fillUnknowns = false]) {
    var snippet = snippetTemplate.replaceAllMapped(_placeholderPattern, (m) {
      var arg = m.group(1);
      var value = args[arg];
      if (value != null) return '$value';
      if (fillUnknowns) return '';
      throw "missing argument $arg, for error message: $snippetTemplate";
    });
    return snippet;
  }

  create([Map args = const {}, bool fillUnknowns = false]) =>
      new Message(id, _createSnippet(args, fillUnknowns));

  /// Serializes this message to JSON.
  Map toJson() => create().toJson();
  String toString() => '${toJson()}';
}

/// Represents an actual log entry for a build error message. Including the
/// actual message, its severity level (warning, error, etc), and a source span
/// for a code location that is revelant to the message.
class BuildLogEntry {
  /// The actual message.
  final Message message;

  /// Severity level.
  final String level;

  /// Location associated with this message, if any.
  final SourceSpan span;

  BuildLogEntry(this.message, this.span, this.level);

  /// Creates a new [BuildLogEntry] from an encoded value produced via [toJson].
  factory BuildLogEntry.fromJson(Map data) {
    var spanData = data['span'];
    var span = null;
    if (spanData != null) {
      var locData = spanData['start'];
      var start = new SourceLocation(locData['offset'],
          sourceUrl: Uri.parse(locData['url']),
          line: locData['line'],
          column: locData['column']);
      locData = spanData['end'];
      var end = new SourceLocation(locData['offset'],
          sourceUrl: Uri.parse(locData['url']),
          line: locData['line'],
          column: locData['column']);
      span = new SourceSpan(start, end, spanData['text']);
    }
    return new BuildLogEntry(
        new Message.fromJson(data['message']), span, data['level']);
  }

  /// Serializes this log entry to JSON.
  Map toJson() {
    var data = {
      'level': level,
      'message': message.toJson(),
    };
    if (span != null) {
      data['span'] = {
        'start': {
          'url': span.start.sourceUrl.toString(),
          'offset': span.start.offset,
          'line': span.start.line,
          'column': span.start.column,
        },
        'end': {
          'url': span.end.sourceUrl.toString(),
          'offset': span.end.offset,
          'line': span.end.line,
          'column': span.end.column,
        },
        'text': span.text,
      };
    }
    return data;
  }

  String toString() => '${toJson()}';
}

/// A table of entries, that clusters error messages by id.
class LogEntryTable {
  final Map<MessageId, List<BuildLogEntry>> entries;

  LogEntryTable() : entries = new LinkedHashMap();

  /// Creates a new [LogEntryTable] from an encoded value produced via [toJson].
  factory LogEntryTable.fromJson(Map<String, Iterable> json) {
    var res = new LogEntryTable();
    for (String key in json.keys) {
      var id = new MessageId.fromJson(key);
      res.entries[id] =
          json[key].map((v) => new BuildLogEntry.fromJson(v)).toList();
    }
    return res;
  }

  /// Serializes this entire table as JSON.
  Map toJson() {
    var res = {};
    entries.forEach((key, value) {
      res['$key'] = value.map((e) => e.toJson()).toList();
    });
    return res;
  }

  String toString() => '${toJson()}';

  void add(BuildLogEntry entry) {
    entries.putIfAbsent(entry.message.id, () => []).add(entry);
  }

  void addAll(LogEntryTable other) {
    for (var key in other.entries.keys) {
      var values = entries.putIfAbsent(key, () => []);
      values.addAll(other.entries[key]);
    }
  }
}
