import 'dart:async';
import 'dart:io';

import 'package:analysis_server_lib/analysis_server_lib.dart';
import 'package:path/path.dart' as path;

import '../models/api_models.dart';
import 'project.dart';
import 'scheduler.dart';

// Use very long timeouts to ensure that the server has enough time to restart.
const Duration _analysisServerTimeout = Duration(seconds: 35);
const Duration _analysisWarmupTimeout = Duration(seconds: 60);

const String _warmupSrc = 'main() { int b = 2;  b++;   b. }';
final mainPath = path.join(jasprBasicTemplatePath, 'lib', 'main.dart');

class Analyzer {
  final TaskScheduler serverScheduler = TaskScheduler();

  /// Instance to handle communication with the server.
  late AnalysisServer analysisServer;

  Analyzer() {
    init();
  }

  Future<void> init() async {
    serverScheduler.schedule(ClosureTask(() async {
      var sdkPath = Platform.environment['DART_SDK_PATH'];
      analysisServer = await AnalysisServer.create(sdkPath: sdkPath, clientId: 'JasprPad');

      analysisServer.server.onError.listen((ServerError error) {
        print('server error${error.isFatal ? ' (fatal)' : ''}: ${error.message}');
      });
      await analysisServer.server.onConnected.first;
      await analysisServer.server.setSubscriptions(<String>['STATUS']);

      await analysisServer.analysis.setAnalysisRoots([jasprBasicTemplatePath], []);

      // Warmup.
      await _sendAddOverlays({mainPath: _warmupSrc});
      await _sendRemoveOverlays();
    }, timeoutDuration: _analysisWarmupTimeout));
  }

  Future<AnalyzeResponse> analyze(AnalyzeRequest request) {
    return serverScheduler.schedule(ClosureTask<AnalyzeResponse>(() async {
      var sources = request.sources.map((k, v) => MapEntry(path.join(jasprBasicTemplatePath, 'lib', k), v));
      await _loadSources(sources);
      final errors = (await analysisServer.analysis.getErrors(mainPath)).errors;
      await _unloadSources();

      var lines = request.sources.map((k, v) => MapEntry(k, Lines(v)));

      return AnalyzeResponse(errors.map((error) {
        var name = path.basename(error.location.file);
        var line = lines[name]!;

        final endLine = line.getLineForOffset(error.location.offset + error.location.length);
        final offsetForEndLine = line.offsetForLine(endLine);
        var endColumn = error.location.offset + error.location.length - offsetForEndLine;

        return Issue(
          kind: IssueKindMapper.fromValue(error.severity.toLowerCase()),
          location: IssueLocation(
            startLine: error.location.startLine - 1,
            startColumn: error.location.startColumn - 1,
            endLine: endLine,
            endColumn: endColumn,
          ),
          message: error.message,
          sourceName: name,
          hasFixes: error.hasFix ?? false,
          url: error.url,
          correction: error.correction,
        );
      }).toList());
    }, timeoutDuration: _analysisServerTimeout));
  }

  Future<FormatResponse> format(FormatRequest request) async {
    return serverScheduler.schedule(ClosureTask<FormatResponse>(() async {
      await _loadSources({mainPath: request.source});
      final FormatResult result;
      try {
        result = await analysisServer.edit.format(mainPath, request.offset, 0);
      } finally {
        await _unloadSources();
      }

      final edits = result.edits;
      edits.sort((SourceEdit e1, SourceEdit e2) => -1 * e1.offset.compareTo(e2.offset));

      var src = request.source;
      for (final edit in edits) {
        src = src.replaceRange(edit.offset, edit.offset + edit.length, edit.replacement);
      }

      return FormatResponse(src, result.selectionOffset);
    }, timeoutDuration: _analysisServerTimeout));
  }

  Future<DocumentResponse> document(DocumentRequest request) async {
    return serverScheduler.schedule(ClosureTask<DocumentResponse>(() async {
      var sources = request.sources.map((k, v) => MapEntry(path.join(jasprBasicTemplatePath, 'lib', k), v));
      await _loadSources(sources);

      final result = await analysisServer.analysis.getHover(
        path.join(jasprBasicTemplatePath, 'lib', request.name),
        request.offset,
      );
      await _unloadSources();

      if (result.hovers.isEmpty) {
        return DocumentResponse(HoverInfo(), null);
      }

      final info = result.hovers.first;

      return DocumentResponse(
        HoverInfo(
          description: info.elementDescription,
          kind: info.elementKind,
          dartdoc: info.dartdoc,
          enclosingClassName: info.containingClassDescription,
          libraryName: info.containingLibraryName,
          parameter: info.parameter,
          deprecated: info.isDeprecated,
          staticType: info.staticType,
          propagatedType: info.propagatedType,
        ),
        null,
      );
    }, timeoutDuration: _analysisServerTimeout));
  }

  final Set<String> _overlayPaths = <String>{};

  /// Loads [sources] as file system overlays to the analysis server.
  ///
  /// The analysis server then begins to analyze these as priority files.
  Future<void> _loadSources(Map<String, String> sources) async {
    if (_overlayPaths.isNotEmpty) {
      throw StateError('There should be no overlay paths while loading sources, but we '
          'have: $_overlayPaths');
    }
    await _sendAddOverlays(sources);
    await analysisServer.analysis.setPriorityFiles(sources.keys.toList());
  }

  Future<void> _unloadSources() async {
    await _sendRemoveOverlays();
    await analysisServer.analysis.setPriorityFiles([]);
  }

  /// Sends [overlays] to the analysis server.
  Future<void> _sendAddOverlays(Map<String, String> overlays) async {
    final contentOverlays = overlays.map((overlayPath, content) => MapEntry(overlayPath, AddContentOverlay(content)));

    _overlayPaths.addAll(contentOverlays.keys);

    await analysisServer.analysis.updateContent(contentOverlays);
  }

  Future<void> _sendRemoveOverlays() async {
    final contentOverlays = {for (final overlayPath in _overlayPaths) overlayPath: RemoveContentOverlay()};
    _overlayPaths.clear();

    await analysisServer.analysis.updateContent(contentOverlays);
  }
}

class Lines {
  final _starts = <int>[];

  Lines(String source) {
    final units = source.codeUnits;
    var nextIsEol = true;
    for (var i = 0; i < units.length; i++) {
      if (nextIsEol) {
        nextIsEol = false;
        _starts.add(i);
      }
      if (units[i] == 10) nextIsEol = true;
    }
  }

  /// Return the 0-based line number.
  int getLineForOffset(int offset) {
    if (_starts.isEmpty) return 0;
    for (var i = 1; i < _starts.length; i++) {
      if (offset < _starts[i]) return i - 1;
    }
    return _starts.length - 1;
  }

  int offsetForLine(int line) {
    assert(line >= 0);
    if (_starts.isEmpty) return 0;
    if (line >= _starts.length) line = _starts.length - 1;
    return _starts[line];
  }
}
