// Copyright (c) 2018, 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:convert';
import 'dart:io';

import 'package:args/args.dart';
import 'package:http/http.dart' as http;
import 'package:path/path.dart' as path;

import 'codegen_dart.dart';
import 'custom/completion.dart';
import 'custom/dart_migrate.dart';
import 'custom/editable_arguments.dart';
import 'custom/interactive_forms.dart';
import 'custom/interactive_refactors.dart';
import 'custom/outlines.dart';
import 'custom/widget_preview.dart';
import 'meta_model.dart';
import 'utils.dart';

Future<void> main(List<String> arguments) async {
  var args = argParser.parse(arguments);
  var help = args[argHelp] as bool;
  if (help) {
    print(argParser.usage);
    return;
  }

  var outFolder = path.join(languageServerProtocolPackagePath, 'lib');
  Directory(outFolder).createSync();

  // Collect definitions for types in the model and our custom extensions.
  var specTypes = await getSpecClasses(args);
  var customTypes = getCustomClasses();

  // Record both sets of types in dictionaries for faster lookups, but also so
  // they can reference each other and we can find the definitions during
  // codegen.
  recordTypes(specTypes);
  recordTypes(customTypes);

  // Generate formatted Dart code (as a string) for each set of types.
  var specTypesOutput = generateDartForTypes(specTypes);
  var customTypesOutput = generateDartForTypes(customTypes);

  File(path.join(outFolder, 'protocol_generated.dart')).writeAsStringSync(
    generatedFileHeader(2018, importCustom: true) + specTypesOutput,
  );
  File(
    path.join(outFolder, 'protocol_custom_generated.dart'),
  ).writeAsStringSync(generatedFileHeader(2019) + customTypesOutput);
}

const argDownload = 'download';

const argHelp = 'help';

final argParser = ArgParser()
  ..addFlag(argHelp, hide: true)
  ..addFlag(
    argDownload,
    negatable: false,
    abbr: 'd',
    help: 'Download the latest version of the LSP spec before generating types',
  );

final String languageServerProtocolPackagePath = path.join(
  sdkRootPath,
  'third_party',
  'pkg',
  'language_server_protocol',
);

final String
licenseComment = LineSplitter.split(File(localLicensePath).readAsStringSync())
    .skipWhile(
      (line) =>
          line !=
          'Files: lib/protocol_custom_generated.dart, lib/protocol_generated.dart',
    )
    .skip(2)
    .map((line) => line.isEmpty ? '//' : '// $line')
    .join('\n');

final String localLicensePath = '$languageServerProtocolPackagePath/LICENSE';
final String localSpecPath =
    '$languageServerProtocolPackagePath/lsp_meta_model.json';

final String sdkRootPath = File(
  Platform.script.toFilePath(),
).parent.parent.parent.parent.parent.path;

final Uri specLicenseUri = Uri.parse(
  'https://microsoft.github.io/language-server-protocol/License-code.txt',
);

/// The URI of the version of the LSP meta model to generate from. This should
/// be periodically updated to the latest version.
final Uri specUri = Uri.parse(
  'https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/metaModel/metaModel.json',
);

Future<void> downloadSpec() async {
  var specResp = await http.get(specUri);
  var licenseResp = await http.get(specLicenseUri);

  assert(specResp.statusCode == 200);
  assert(licenseResp.statusCode == 200);

  var dartSdkLicense = await File('$sdkRootPath/LICENSE').readAsString();
  await File(localSpecPath).writeAsString(specResp.body);
  await File(localLicensePath).writeAsString('''
$dartSdkLicense

------------------

Files: lsp_meta_model.json
Files: lib/protocol_custom_generated.dart, lib/protocol_generated.dart

${licenseResp.body}
''');
}

String generatedFileHeader(int year, {bool importCustom = false}) =>
    '''
$licenseComment

// This file has been automatically generated. Please do not edit it manually.
// To regenerate the file, use the script
// "pkg/analysis_server/tool/lsp_spec/generate_all.dart".

// ignore_for_file: constant_identifier_names

import 'dart:convert' show JsonEncoder;

import 'package:collection/collection.dart';
import 'package:language_server_protocol/json_parsing.dart';
import 'package:language_server_protocol/protocol_special.dart';
import 'package:language_server_protocol/protocol${importCustom ? '_custom' : ''}_generated.dart';

const jsonEncoder = JsonEncoder.withIndent('    ');

''';

List<LspEntity> getCustomClasses() {
  var customTypes = <LspEntity>[
    TypeAlias(
      name: 'LSPAny',
      baseType: TypeReference.lspAny,
      renameReferences: false,
    ),
    TypeAlias(
      name: 'LSPObject',
      baseType: TypeReference.lspObject,
      renameReferences: false,
    ),
    // The DocumentFilter more complex in v3.17's meta_model (to allow
    // TextDocumentFilters to be guaranteed to have at least one of language,
    // pattern, scheme) but we only ever use a single type in the server so
    // for compatibility, alias that type to the original TS-spec name.
    // TODO(dantup): Improve this after the TS->JSON Spec migration.
    TypeAlias(
      name: 'DocumentFilter',
      baseType: TypeReference('TextDocumentFilterScheme'),
      renameReferences: true,
    ),
    // Similarly, the meta_model includes String as an option for
    // DocumentSelector which is deprecated and we never previously supported
    // (because the TypeScript spec did not include it in the type) so preserve
    // that.
    // TODO(dantup): Improve this after the TS->JSON Spec migration.
    TypeAlias(
      name: 'DocumentSelector',
      baseType: ArrayType(TypeReference('TextDocumentFilterScheme')),
      renameReferences: true,
    ),
    interface('Message', [
      field('jsonrpc', type: 'string'),
      field('clientRequestTime', type: 'int', canBeUndefined: true),
    ]),
    interface('IncomingMessage', [
      field('method', type: 'Method'),
      field('params', type: 'LSPAny', canBeUndefined: true),
    ], baseType: 'Message'),
    interface('RequestMessage', [
      Field(
        name: 'id',
        type: UnionType([TypeReference.int, TypeReference.string]),
        allowsNull: false,
        allowsUndefined: false,
      ),
    ], baseType: 'IncomingMessage'),
    interface('NotificationMessage', [], baseType: 'IncomingMessage'),
    interface('ResponseMessage', [
      Field(
        name: 'id',
        type: UnionType([TypeReference.int, TypeReference.string]),
        allowsNull: true,
        allowsUndefined: false,
      ),
      field('result', type: 'LSPAny', canBeUndefined: true),
      field('error', type: 'ResponseError', canBeUndefined: true),
    ], baseType: 'Message'),
    interface('ResponseError', [
      field('code', type: 'ErrorCodes'),
      field('message', type: 'string'),
      // This is Object? normally, but since this class can be serialized
      // we will crash if it data is set to something that can't be converted to
      // JSON (for ex. Uri) so this forces anyone setting this to convert to a
      // String.
      field(
        'data',
        type: 'string',
        canBeUndefined: true,
        comment:
            'A string that contains additional information about the error. '
            'Can be omitted.',
      ),
    ]),
    TypeAlias(
      name: 'DocumentUri',
      baseType: TypeReference('Uri'),
      renameReferences: false,
    ),
    // The LSP Spec uses "URI" but since that's fairly generic and will show up
    // everywhere in code completion, we rename it to "LspUri" before using
    // a typedef onto the Dart URI class.
    TypeAlias(
      name: 'URI',
      baseType: TypeReference('LSPUri'),
      renameReferences: true,
    ),
    TypeAlias(
      name: 'LSPUri',
      baseType: TypeReference('Uri'),
      renameReferences: false,
    ),

    interface('ConnectToDtdParams', [
      field('uri', type: 'Uri'),
      field(
        'registerExperimentalHandlers',
        type: 'boolean',
        canBeUndefined: true,
        comment:
            'Whether to register experimental LSP handlers with DTD. '
            'This should not be set by clients automatically but opt-in for '
            'users that are developing/testing incomplete functionality.',
      ),
    ]),
    interface('DartDiagnosticServer', [field('port', type: 'int')]),
    interface('AnalyzerStatusParams', [field('isAnalyzing', type: 'boolean')]),
    interface('PublishClosingLabelsParams', [
      field('uri', type: 'Uri'),
      field('labels', type: 'ClosingLabel', array: true),
    ]),
    interface('OpenUriParams', [field('uri', type: 'Uri')]),
    interface('ClosingLabel', [
      field('range', type: 'Range'),
      field('label', type: 'string'),
    ]),

    // Custom types for experimental SnippetTextEdits
    // https://github.com/rust-analyzer/rust-analyzer/blob/b35559a2460e7f0b2b79a7029db0c5d4e0acdb44/docs/dev/lsp-extensions.md#snippet-textedit
    interface('SnippetTextEdit', [
      field('insertTextFormat', type: 'InsertTextFormat'),
    ], baseType: 'TextEdit'),
    // Return type for refactor.validate command.
    interface('ValidateRefactorResult', [
      field('valid', type: 'boolean'),
      field('message', type: 'string', canBeUndefined: true),
    ]),
    interface('TypeHierarchyItemInfo', [
      field(
        'ref',
        type: 'string',
        comment:
            'The ElementLocation for this element, used to re-locate the '
            'element when subtypes/supertypes are '
            'fetched later.',
      ),
    ]),

    TypeAlias(
      name: 'TextDocumentEditEdits',
      baseType: ArrayType(
        UnionType([
          TypeReference('SnippetTextEdit'),
          TypeReference('AnnotatedTextEdit'),
          TypeReference('TextEdit'),
        ]),
      ),
      renameReferences: false,
    ),

    // Types for `dart/textDocument/summary`.
    interface('DartTextDocumentSummaryParams', [
      field('uri', type: 'DocumentUri'),
    ]),
    interface('DocumentSummary', [
      field('summary', type: 'String', canBeNull: true),
    ]),

    // Strongly typed classes for `completionItem/resolve`.
    ...completionResolutionClasses,

    // Support for the Outline notifications.
    ...outlineClasses,

    // Support for the Flutter Widget Preview.
    ...flutterWidgetPreviewClasses,

    // Support for Editable Arguments used by the Property Editor.
    ...editableArgumentsClasses,

    // Support for `dart/workspace/migrate`.
    ...dartMigrateClasses,

    // Support for the original (Dart-specific) interactive-refactors.
    ...interactiveRefactorsClasses,

    // Support for Interactive Forms.
    ...interactiveFormClasses,
  ];
  return customTypes;
}

Future<List<LspEntity>> getSpecClasses(ArgResults args) async {
  var download = args[argDownload] as bool;
  if (download) {
    await downloadSpec();
  }

  var file = File(localSpecPath);
  var model = LspMetaModelReader().readFile(file);
  model = LspMetaModelCleaner().cleanModel(model);

  return model.types;
}
