// 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 'dart:convert';

import 'package:analyzer/dart/element/element.dart';
import 'package:dartdoc/src/model/attribute.dart';
import 'package:dartdoc/src/model/kind.dart';
import 'package:dartdoc/src/model/model.dart';

class Field extends ModelElement
    with GetterSetterCombo, ContainerMember, Inheritable, HasLibrary {
  @override
  final FieldElement element;

  @override
  final ContainerAccessor? getter;

  @override
  final ContainerAccessor? setter;

  @override
  final bool isInherited;

  @override
  final Container enclosingElement;

  Field(
    this.element,
    Library super.library,
    super.packageGraph,
    this.getter,
    this.setter,
  )   : isInherited = false,
        enclosingElement =
            ModelElement.for_(element.enclosingElement, library, packageGraph)
                as Container,
        assert(getter != null || setter != null) {
    getter?.enclosingCombo = this;
    setter?.enclosingCombo = this;
  }

  Field.providedByExtension(
    this.element,
    this.enclosingElement,
    Library super.library,
    super.packageGraph,
    this.getter,
    this.setter,
  )   : isInherited = false,
        assert(getter != null || setter != null) {
    getter?.enclosingCombo = this;
    setter?.enclosingCombo = this;
  }

  Field.inherited(
    this.element,
    this.enclosingElement,
    Library super.library,
    super.packageGraph,
    this.getter,
    this.setter,
  )   : isInherited = true,
        assert(getter != null || setter != null) {
    // Can't set `isInherited` to true if this is the defining element, because
    // that would mean it isn't inherited.
    assert(enclosingElement != definingEnclosingContainer);
    getter?.enclosingCombo = this;
    setter?.enclosingCombo = this;
  }

  @override
  String? get href {
    assert(!identical(canonicalModelElement, this) ||
        canonicalEnclosingContainer == enclosingElement);
    return super.href;
  }

  @override
  bool get isConst => element.isConst;

  /// Whether the [FieldElement] is covariant, or the first parameter for the
  /// setter is covariant.
  @override
  bool get isCovariant => setter?.isCovariant == true || element.isCovariant;

  /// Whether this field is final.
  ///
  /// `Element.isFinal` returns `true` even if it has an explicit getter (which
  /// means we should not document it as "final").
  @override
  bool get isFinal {
    if (hasExplicitGetter) return false;
    return element.isFinal;
  }

  @override
  bool get isLate => isFinal && element.isLate;

  bool get isStatic => element.isStatic;

  @override
  Kind get kind => isConst ? Kind.constant : Kind.property;

  String get fullkind =>
      element.isAbstract ? 'abstract $kind' : kind.toString();

  bool get isProvidedByExtension =>
      element.enclosingElement is ExtensionElement;

  /// The [enclosingElement], which is expected to be an [Extension].
  Extension get enclosingExtension => enclosingElement as Extension;

  @override
  Set<Attribute> get attributes {
    var allAttributes = {...super.attributes, ...comboAttributes};
    // Combo attributes can indicate 'inherited' and 'override' if either the
    // getter or setter has one of those properties, but that's not really
    // specific enough for [Field]s that have public getter/setters.
    if ((getter, setter) case (var getter?, var setter?)
        when getter.isPublic && setter.isPublic) {
      if (getter.isInherited && setter.isInherited) {
        allAttributes.add(Attribute.inherited);
      } else {
        allAttributes.remove(Attribute.inherited);
        if (getter.isInherited) allAttributes.add(Attribute.inheritedGetter);
        if (setter.isInherited) allAttributes.add(Attribute.inheritedSetter);
      }
      if (getter.isOverride && setter.isOverride) {
        allAttributes.add(Attribute.override_);
      } else {
        allAttributes.remove(Attribute.override_);
        if (getter.isOverride) allAttributes.add(Attribute.overrideGetter);
        if (setter.isOverride) allAttributes.add(Attribute.overrideSetter);
      }
    } else {
      if (isInherited) allAttributes.add(Attribute.inherited);
      if (isOverride) allAttributes.add(Attribute.override_);
    }
    return allAttributes;
  }

  @override
  String get fileName => '${isConst ? '$name-constant' : name}.html';

  @override
  String get aboveSidebarPath => enclosingElement.sidebarPath;

  @override
  String? get belowSidebarPath => null;

  late final String _sourceCode = () {
    // We could use a set to figure the dupes out, but that would lose ordering.
    var fieldSourceCode = modelNode?.sourceCode ?? '';
    var getterSourceCode = getter?.sourceCode ?? '';
    var setterSourceCode = setter?.sourceCode ?? '';
    var buffer = StringBuffer();
    if (fieldSourceCode.isNotEmpty) {
      fieldSourceCode = const HtmlEscape().convert(fieldSourceCode);
      buffer.write(fieldSourceCode);
    }
    if (buffer.isNotEmpty) buffer.write('\n\n');
    if (fieldSourceCode != getterSourceCode) {
      if (getterSourceCode != setterSourceCode) {
        buffer.write(getterSourceCode);
        if (buffer.isNotEmpty) buffer.write('\n\n');
      }
    }
    if (fieldSourceCode != setterSourceCode) {
      buffer.write(setterSourceCode);
    }
    return buffer.toString().trim();
  }();

  @override
  String get sourceCode => _sourceCode;

  @override
  Inheritable? get overriddenElement => null;
}
