// Copyright (c) 2015, 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.

import 'dart:async';
import 'dart:math' as math;

import 'package:json_rpc_2/json_rpc_2.dart' as rpc;
import 'package:source_span/source_span.dart';

import 'breakpoint.dart';
import 'class.dart';
import 'library.dart';
import 'object.dart';
import 'scope.dart';
import 'source_location.dart';
import 'source_report.dart';

VMScriptRef newVMScriptRef(Scope scope, Map json) {
  if (json == null) return null;
  assert(json["type"] == "@Script" || json["type"] == "Script");
  return new VMScriptRef._(scope, json);
}

VMScriptToken newVMScriptToken(
    String isolateId, String scriptId, int position) {
  if (position == null) return null;
  return new VMScriptToken._(isolateId, scriptId, position);
}

VMScriptToken newVMScriptTokenFromPosition(VMScriptRef script, int position) {
  if (position == null) return null;
  return new VMScriptToken._(script._scope.isolateId, script._id, position);
}

/// A reference to a script in the Dart VM.
///
/// A script contains information about the actual text of a library. Usually
/// there's only one script per library, but the `part` directive can produce
/// libraries made up of multiple scripts.
class VMScriptRef implements VMObjectRef {
  final Scope _scope;

  /// The ID for script library, which is unique relative to its isolate.
  final String _id;

  /// Whether [_id] is guaranteed to be the same for different VM service
  /// script objects that refer to the same script.
  final bool _fixedId;

  Uri get observatoryUrl => _scope.observatoryUrlFor(_id);

  /// The URI from which this script was loaded.
  final Uri uri;

  VMScriptRef._(this._scope, Map json)
      : _id = json["id"],
        _fixedId = json["fixedId"] ?? false,
        uri = Uri.parse(json["uri"]);

  Future<VMScript> load() async =>
      new VMScript._(_scope, await _scope.loadObject(_id));

  /// Adds a breakpoint at [line] (and optionally [column]) in this script.
  ///
  /// Both [line] and [column] are 1-based.
  Future<VMBreakpoint> addBreakpoint(int line, {int column}) async {
    var params = {"scriptId": _id, "line": line};
    if (column != null) params["column"] = column;

    try {
      var response = await _scope.sendRequest("addBreakpoint", params);
      return newVMBreakpoint(_scope, response);
    } on rpc.RpcException catch (error) {
      // Error 102 indicates that the breakpoint couldn't be created.
      if (error.code == 102) return null;
      rethrow;
    }
  }

  /// Generates a set of reports tied to this script.
  ///
  /// If [includeCoverageReport] is `true`, the report includes code coverage
  /// information via [VMSourceReportRange.hits] and
  /// [VMSourceReportRange.misses] in [VMSourceReport.ranges]. Otherwise,
  /// these properties are `null`.
  ///
  /// If [includePossibleBreakpoints] is `true`, the report includes a list of
  /// token positions which correspond to possible breakpoints via
  /// [VMSourceReportRange.possibleBreakpoints] in [VMSourceReport.ranges].
  /// Otherwise, [VMSourceReportRange.possibleBreakpoints] is `null`.
  ///
  /// If [forceCompile] is `true`, all functions in the range of the report
  /// will be compiled. If `false`, functions that are never used may not appear
  /// in [VMSourceReportRange.misses].
  /// Forcing compilation can cause a compilation error, which could terminate
  /// the running Dart program.
  ///
  /// [location] can be provided to restrict analysis to a subrange of the
  /// script. An [ArgumentError] is thrown if `location.end` is `null`.
  Future<VMSourceReport> getSourceReport(
      {bool includeCoverageReport: true,
      bool includePossibleBreakpoints: true,
      bool forceCompile: false,
      VMSourceLocation location}) async {
    var reports = <String>[];
    if (includeCoverageReport) reports.add('Coverage');
    if (includePossibleBreakpoints) reports.add('PossibleBreakpoints');

    var params = <String, dynamic>{'scriptId': _id, 'reports': reports};
    if (forceCompile) params['forceCompile'] = true;
    if (location != null) {
      if (location.end == null) {
        throw new ArgumentError.value(
            location, 'location', 'location.end cannot be null.');
      }
      params['tokenPos'] = location.token._position;
      params['endTokenPos'] = location.end._position;
    }

    var json = await _scope.sendRequest('getSourceReport', params);
    return newSourceReport(_scope, json);
  }

  bool operator ==(other) =>
      other is VMScriptRef && (_fixedId ? _id == other._id : super == other);

  int get hashCode => _fixedId ? _id.hashCode : super.hashCode;

  String toString() => uri.toString();
}

/// A script in the Dart VM.
class VMScript extends VMScriptRef implements VMObject {
  final VMClassRef klass;

  final int size;

  /// The library that owns this script.
  final VMLibraryRef library;

  /// The source code for this script.
  ///
  /// For certain built-in libraries, this may be reconstructed without source
  /// comments.
  final String source;

  /// A table encoding a mapping from token position to line and column.
  ///
  /// Each subarray consists of an int designating the line, followed by any
  /// number of position-column pairs that represent the positions of tokens
  /// known to the VM.
  ///
  /// Because this encodes all the known token positions, it's more efficient to
  /// access than the representation used by a [SourceFile] as long as you're
  /// looking up a known token boundary.
  final List<List<int>> _tokenPositions;

  /// A source file that provides access to location and span information about
  /// this script.
  ///
  /// This is generally less efficient than calling [sourceSpan] and
  /// [sourceLocation] directly, and should only be used when you don't have
  /// [VMSourceLocation] or [VMScriptToken] objects.
  ///
  /// Note that [SourceFile] uses 0-based lines and columns, whereas the rest of
  /// this package uses 1-based lines and columns.
  SourceFile get sourceFile {
    _sourceFile ??= new SourceFile.fromString(source, url: uri);
    return _sourceFile;
  }

  SourceFile _sourceFile;

  VMScript._(Scope scope, Map json)
      : klass = newVMClassRef(scope, json["class"]),
        size = json["size"],
        library = newVMLibraryRef(scope, json["library"]),
        source = json["source"],
        _tokenPositions = (json["tokenPosTable"] as List)
            .map((sublist) => (sublist as List).cast<int>())
            .toList(),
        super._(scope, json);

  /// Returns a [FileSpan] representing the source covered by [location].
  ///
  /// If [location] doesn't have a [VMSourceLocation.end] token, this will be a
  /// point span. Note that [FileSpan] uses 0-based lines and columns, whereas
  /// the rest of this package uses 1-based lines and columns.
  ///
  /// Throws an [ArgumentError] if [location] isn't for this script.
  FileSpan sourceSpan(VMSourceLocation location) {
    if (location.script._scope.isolateId != _scope.isolateId ||
        (_fixedId && location.script._id != _id)) {
      throw new ArgumentError("SourceLocation isn't for this script.");
    }

    var end = location.end ?? location.token;
    return new _ScriptSpan(this, location.token._position, end._position);
  }

  /// Returns a [FileLocation] representing the location indicated by [token].
  ///
  /// Note that [FileLocation] uses 0-based lines and columns, whereas the rest
  /// of this package uses 1-based lines and columns.
  ///
  /// Throws an [ArgumentError] if [token] isn't for this script.
  FileLocation sourceLocation(VMScriptToken token) {
    if (token._isolateId != _scope.isolateId ||
        (_fixedId && token._scriptId != _id)) {
      throw new ArgumentError("Token isn't for this script.");
    }

    return new _ScriptLocation(this, token._position);
  }

  /// Binary searches [_tokenPositions] for the line and column information for
  /// the token at [position].
  ///
  /// This returns a `line, column` pair if the token is found, and throws a
  /// [StateError] otherwise.
  List<int> _lineAndColumn(int position) {
    var min = 0;
    var max = _tokenPositions.length;
    while (min < max) {
      var mid = min + ((max - min) >> 1);

      var row = _tokenPositions[mid];

      if (row[1] > position) {
        max = mid;
      } else {
        for (var i = 1; i < row.length; i += 2) {
          if (row[i] == position) return [row.first, row[i + 1]];
        }

        min = mid + 1;
      }
    }

    // We only call this for positions that come from the VM, so we shouldn't
    // ever actually reach this point.
    throw new StateError("Couldn't find line and column for token $position in "
        "$uri.");
  }
}

/// The location of a token in a Dart script.
///
/// A token can be passed to [VMScript.sourceLocation] to get the line and
/// column information for the token.
class VMScriptToken {
  /// The ID of this token's script's isolate.
  final String _isolateId;

  /// The ID of this token's script.
  final String _scriptId;

  /// This is deprecated.
  ///
  /// Use `VMScript.sourceLocation().offset` instead.
  @Deprecated("Will be removed in 0.3.0.")
  int get offset => _position;
  final int _position;

  VMScriptToken._(this._isolateId, this._scriptId, this._position);

  String toString() => _position.toString();
}

/// An implementation of [FileLocation] based on a known token position.
class _ScriptLocation extends SourceLocationMixin implements FileLocation {
  /// The script that produced this location.
  final VMScript _script;

  /// The position of the token used to generate this location.
  ///
  /// This is opaque, but it can be used to look up the line and column
  /// information.
  final int _position;

  int get offset {
    // TODO(nweiz): Make this more efficient when sdk#26390 is fixed.
    _offset ??= _script.sourceFile.getOffset(line, column);
    return _offset;
  }

  int _offset;

  SourceFile get file => _script.sourceFile;

  Uri get sourceUrl => _script.uri;

  int get line {
    _ensureLineAndColumn();
    return _line;
  }

  int _line;

  int get column {
    _ensureLineAndColumn();
    return _column;
  }

  int _column;

  _ScriptLocation(this._script, this._position);

  /// Ensures that [_line] and [_column] are set based on [_script]'s
  /// information.
  void _ensureLineAndColumn() {
    if (_line != null) return null;
    var result = _script._lineAndColumn(_position);

    // SourceLocation lines and columns are zero-based, the VM service's are
    // 1-based.
    _line = result.first - 1;
    _column = result.last - 1;
  }

  int compareTo(SourceLocation other) =>
      other is _ScriptLocation && other.sourceUrl == sourceUrl
          ? _position - other._position
          : super.compareTo(other);

  bool operator ==(other) => other is _ScriptLocation
      ? _position == other._position && sourceUrl == other.sourceUrl
      : super == other;

  FileSpan pointSpan() =>
      new _ScriptSpan(_script, _position, _position, this, this);
}

/// An implementation of [FileSpan] based on known token positions.
class _ScriptSpan extends SourceSpanMixin implements FileSpan {
  /// The script that produced this location.
  final VMScript _script;

  SourceFile get file => _script.sourceFile;

  /// The position of the start token.
  final int _startPosition;

  /// The position of the end token.
  final int _endPosition;

  Uri get sourceUrl => _script.uri;

  int get length {
    // Prefer checking just the lines and columns, since they're more efficient
    // to access.
    if (start.line == end.line) return end.column - start.column;
    return end.offset - start.offset;
  }

  FileLocation get start {
    _start ??= new _ScriptLocation(_script, _startPosition);
    return _start;
  }

  FileLocation _start;

  FileLocation get end {
    _end ??= new _ScriptLocation(_script, _endPosition);
    return _end;
  }

  FileLocation _end;

  String get text => _script.source.substring(start.offset, end.offset);

  String get context => file.getText(file.getOffset(start.line),
      end.line == file.lines - 1 ? null : file.getOffset(end.line + 1));

  _ScriptSpan(this._script, this._startPosition, this._endPosition,
      [this._start, this._end]);

  int compareTo(SourceSpan other) {
    if (other is! _ScriptSpan || other.sourceUrl != sourceUrl) {
      return super.compareTo(other);
    }

    _ScriptSpan otherFile = other;
    var result = _startPosition.compareTo(otherFile._startPosition);
    return result == 0
        ? _endPosition.compareTo(otherFile._endPosition)
        : result;
  }

  SourceSpan union(SourceSpan other) {
    if (other is _ScriptSpan) {
      var span = expand(other) as _ScriptSpan;
      var beginSpan = span._startPosition == _startPosition ? this : other;
      var endSpan = span._endPosition == _endPosition ? this : other;

      if (beginSpan._endPosition < endSpan._startPosition) {
        throw new ArgumentError("Spans $this and $other are disjoint.");
      }

      return span;
    }
    return super.union(other);
  }

  bool operator ==(other) => other is _ScriptSpan
      ? _startPosition == other._startPosition &&
          _endPosition == other._endPosition &&
          sourceUrl == other.sourceUrl
      : super == other;

  FileSpan expand(FileSpan other) {
    if (sourceUrl != other.sourceUrl) {
      throw new ArgumentError("Source URLs \"${sourceUrl}\" and "
          " \"${other.sourceUrl}\" don't match.");
    }

    if (other is _ScriptSpan) {
      var startPosition = math.min(this._startPosition, other._startPosition);
      var endPosition = math.max(this._endPosition, other._endPosition);
      return new _ScriptSpan(
          _script,
          startPosition,
          endPosition,
          startPosition == this._startPosition ? this._start : other._start,
          endPosition == this._endPosition ? this._end : other._end);
    } else {
      var start = math.min(this.start.offset, other.start.offset);
      var end = math.max(this.end.offset, other.end.offset);
      return file.span(start, end);
    }
  }
}
