// Copyright (c) 2019, 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 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/file_system/file_system.dart';
import 'package:dartdoc/src/dartdoc_options.dart';
import 'package:dartdoc/src/model/kind.dart';
import 'package:dartdoc/src/model/model.dart';
import 'package:dartdoc/src/warnings.dart';

/// A subcategory of a package, containing elements tagged with `{@category}`.
final class Category extends LibraryContainer
    with Referable, Warnable, MarkdownFileDocumentation, TopLevelContainer {
  /// The package in which this category is contained.
  ///
  /// All libraries in [libraries] must come from [package].
  @override
  final Package package;

  final String? _name;

  @override
  final DartdocOptionContext config;

  final Set<Class> _classes = {};

  final Set<Class> _exceptions = {};

  @override
  final Set<TopLevelVariable> constants = {};

  @override
  final Set<Extension> extensions = {};

  @override
  final Set<ExtensionType> extensionTypes = {};

  @override
  final Set<Enum> enums = {};

  @override
  final Set<ModelFunction> functions = {};

  @override
  final Set<Mixin> mixins = {};

  @override
  final Set<TopLevelVariable> properties = {};

  @override
  final Set<Typedef> typedefs = {};

  final CategoryDefinition _categoryDefinition;

  Category(this._name, this.package, this.config)
      : _categoryDefinition =
            config.categories.categoryDefinitions[_name.orDefault] ??
                CategoryDefinition(_name, null, null),
        super(isSdk: false, enclosingName: package.name);

  Iterable<ExternalItem> get externalItems => _categoryDefinition.externalItems;

  void addClass(Class class_) {
    if (class_.isErrorOrException) {
      _exceptions.add(class_);
    } else {
      _classes.add(class_);
    }
  }

  @override
  Element? get element => null;

  @override
  String get name => _categoryDefinition.displayName;

  @override
  String get sortKey => _name.orDefault;

  @override
  List<String> get containerOrder => config.categoryOrder;

  @override
  PackageGraph get packageGraph => package.packageGraph;

  @override
  DocumentLocation get documentedWhere => package.documentedWhere;

  @override
  late final bool isDocumented =
      documentedWhere != DocumentLocation.missing && documentationFile != null;

  String get fileName {
    assert(_name != null);
    return '$_name-topic.html';
  }

  String get filePath => 'topics/$fileName';

  /// Prior to dartdoc 8.3.4 the `displayName` was used in the file path
  /// for category pages. We now create a redirect file here instead.
  String get redirectFilePath => 'topics/$name-topic.html';

  @override
  String? get href => isCanonical ? '${package.baseHref}$filePath' : null;

  @override
  String? get aboveSidebarPath => null;

  @override
  String? get belowSidebarPath => null;

  String get categoryLabel {
    final buffer = StringBuffer('<span class="category ');
    buffer.writeAll(name.toLowerCase().split(' '), '-');
    buffer.write(' cp-');
    buffer.write(categoryIndex);

    if (isDocumented) {
      buffer.write(' linked');
    }

    buffer.write('"'); // Wrap up the class list and begin title
    buffer.write(' title="This is part of the ');
    buffer.write(name);
    buffer.write(' ');
    buffer.write(kind);
    buffer.write('.">'); // Wrap up the title

    buffer.write(linkedName);
    buffer.write('</span>');

    return buffer.toString();
  }

  String get linkedName {
    final unbrokenName = name.replaceAll(' ', '&nbsp;');
    if (isDocumented) {
      final href = this.href;
      if (href == null) {
        throw StateError("Requesting the 'linkedName' of a non-canonical "
            "category: '$name'");
      }
      return '<a href="$href">$unbrokenName</a>';
    } else {
      return unbrokenName;
    }
  }

  /// The position in the container order for this category.
  int get categoryIndex => package.categories.indexOf(this);

  @override
  bool get isCanonical =>
      config.categories.categoryDefinitions.containsKey(sortKey);

  @override
  Kind get kind => Kind.topic;

  @override
  File? get documentationFile {
    var documentationMarkdown = _categoryDefinition.documentationMarkdown;
    if (documentationMarkdown != null) {
      return config.resourceProvider.getFile(documentationMarkdown);
    }
    return null;
  }

  @override
  Iterable<Class> get classes => _classes;

  @override
  Iterable<Class> get exceptions => _exceptions;

  @override
  Map<String, Referable> get referenceChildren => const {};

  @override
  Iterable<Referable> get referenceParents => const [];
}

extension on String? {
  String get orDefault => this ?? '<default>';
}

/// A external link for an item in a dartdoc category.
///
/// This is a name, url, and optional documentation text.
class ExternalItem {
  final String name;
  final String url;
  final String docs;

  ExternalItem({
    required this.name,
    required this.url,
    required String? docs,
  }) : docs = docs ?? '';

  @override
  String toString() => '$name $url';
}
