// Copyright 2014 The Flutter Authors. 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:collection';
import 'dart:developer' show Timeline; // to disambiguate reference in dartdocs below

import 'package:flutter/foundation.dart';

import 'basic.dart';
import 'framework.dart';
import 'localizations.dart';
import 'lookup_boundary.dart';
import 'media_query.dart';
import 'overlay.dart';
import 'table.dart';

// Examples can assume:
// late BuildContext context;
// List<Widget> children = <Widget>[];
// List<Widget> items = <Widget>[];

// Any changes to this file should be reflected in the debugAssertAllWidgetVarsUnset()
// function below.

/// Log the dirty widgets that are built each frame.
///
/// Combined with [debugPrintBuildScope] or [debugPrintBeginFrameBanner], this
/// allows you to distinguish builds triggered by the initial mounting of a
/// widget tree (e.g. in a call to [runApp]) from the regular builds triggered
/// by the pipeline.
///
/// Combined with [debugPrintScheduleBuildForStacks], this lets you watch a
/// widget's dirty/clean lifecycle.
///
/// To get similar information but showing it on the timeline available from the
/// Observatory rather than getting it in the console (where it can be
/// overwhelming), consider [debugProfileBuildsEnabled].
///
/// See also:
///
///  * [WidgetsBinding.drawFrame], which pumps the build and rendering pipeline
///    to generate a frame.
bool debugPrintRebuildDirtyWidgets = false;

/// Signature for [debugOnRebuildDirtyWidget] implementations.
typedef RebuildDirtyWidgetCallback = void Function(Element e, bool builtOnce);

/// Callback invoked for every dirty widget built each frame.
///
/// This callback is only invoked in debug builds.
///
/// See also:
///
///  * [debugPrintRebuildDirtyWidgets], which does something similar but logs
///    to the console instead of invoking a callback.
///  * [debugOnProfilePaint], which does something similar for [RenderObject]
///    painting.
///  * [WidgetInspectorService], which uses the [debugOnRebuildDirtyWidget]
///    callback to generate aggregate profile statistics describing which widget
///    rebuilds occurred when the
///    `ext.flutter.inspector.trackRebuildDirtyWidgets` service extension is
///    enabled.
RebuildDirtyWidgetCallback? debugOnRebuildDirtyWidget;

/// Log all calls to [BuildOwner.buildScope].
///
/// Combined with [debugPrintScheduleBuildForStacks], this allows you to track
/// when a [State.setState] call gets serviced.
///
/// Combined with [debugPrintRebuildDirtyWidgets] or
/// [debugPrintBeginFrameBanner], this allows you to distinguish builds
/// triggered by the initial mounting of a widget tree (e.g. in a call to
/// [runApp]) from the regular builds triggered by the pipeline.
///
/// See also:
///
///  * [WidgetsBinding.drawFrame], which pumps the build and rendering pipeline
///    to generate a frame.
bool debugPrintBuildScope = false;

/// Log the call stacks that mark widgets as needing to be rebuilt.
///
/// This is called whenever [BuildOwner.scheduleBuildFor] adds an element to the
/// dirty list. Typically this is as a result of [Element.markNeedsBuild] being
/// called, which itself is usually a result of [State.setState] being called.
///
/// To see when a widget is rebuilt, see [debugPrintRebuildDirtyWidgets].
///
/// To see when the dirty list is flushed, see [debugPrintBuildScope].
///
/// To see when a frame is scheduled, see [debugPrintScheduleFrameStacks].
bool debugPrintScheduleBuildForStacks = false;

/// Log when widgets with global keys are deactivated and log when they are
/// reactivated (retaken).
///
/// This can help track down framework bugs relating to the [GlobalKey] logic.
bool debugPrintGlobalKeyedWidgetLifecycle = false;

/// Adds [Timeline] events for every Widget built.
///
/// The timing information this flag exposes is not representative of the actual
/// cost of building, because the overhead of adding timeline events is
/// significant relative to the time each object takes to build. However, it can
/// expose unexpected widget behavior in the timeline.
///
/// In debug builds, additional information is included in the trace (such as
/// the properties of widgets being built). Collecting this data is
/// expensive and further makes these traces non-representative of actual
/// performance. This data is omitted in profile builds.
///
/// For more information about performance debugging in Flutter, see
/// <https://flutter.dev/docs/perf/rendering>.
///
/// See also:
///
///  * [debugPrintRebuildDirtyWidgets], which does something similar but
///    reporting the builds to the console.
///  * [debugProfileLayoutsEnabled], which does something similar for layout,
///    and [debugPrintLayouts], its console equivalent.
///  * [debugProfilePaintsEnabled], which does something similar for painting.
///  * [debugProfileBuildsEnabledUserWidgets], which adds events for user-created
///    [Widget] build times and incurs less overhead.
///  * [debugEnhanceBuildTimelineArguments], which enhances the trace with
///    debugging information related to [Widget] builds.
bool debugProfileBuildsEnabled = false;

/// Adds [Timeline] events for every user-created [Widget] built.
///
/// A user-created [Widget] is any [Widget] that is constructed in the root
/// library. Often [Widget]s contain child [Widget]s that are constructed in
/// libraries (for example, a [TextButton] having a [RichText] child). Timeline
/// events for those children will be omitted with this flag. This works for any
/// [Widget] not just ones declared in the root library.
///
/// See also:
///
///  * [debugProfileBuildsEnabled], which functions similarly but shows events
///    for every widget and has a higher overhead cost.
///  * [debugEnhanceBuildTimelineArguments], which enhances the trace with
///    debugging information related to [Widget] builds.
bool debugProfileBuildsEnabledUserWidgets = false;

/// Adds debugging information to [Timeline] events related to [Widget] builds.
///
/// This flag will only add [Timeline] event arguments for debug builds.
/// Additional arguments will be added for the "BUILD" [Timeline] event and for
/// all [Widget] build [Timeline] events, which are the [Timeline] events that
/// are added when either of [debugProfileBuildsEnabled] and
/// [debugProfileBuildsEnabledUserWidgets] are true. The debugging information
/// that will be added in trace arguments includes stats around [Widget] dirty
/// states and [Widget] diagnostic information (i.e. [Widget] properties).
///
/// See also:
///
///  * [debugProfileBuildsEnabled], which adds [Timeline] events for every
///    [Widget] built.
///  * [debugProfileBuildsEnabledUserWidgets], which adds [Timeline] events for
///    every user-created [Widget] built.
///  * [debugEnhanceLayoutTimelineArguments], which does something similar for
///    events related to [RenderObject] layouts.
///  * [debugEnhancePaintTimelineArguments], which does something similar for
///    events related to [RenderObject] paints.
bool debugEnhanceBuildTimelineArguments = false;

/// Show banners for deprecated widgets.
bool debugHighlightDeprecatedWidgets = false;

Key? _firstNonUniqueKey(Iterable<Widget> widgets) {
  final Set<Key> keySet = HashSet<Key>();
  for (final Widget widget in widgets) {
    if (widget.key == null) {
      continue;
    }
    if (!keySet.add(widget.key!)) {
      return widget.key;
    }
  }
  return null;
}

/// Asserts if the given child list contains any duplicate non-null keys.
///
/// To invoke this function, use the following pattern:
///
/// ```dart
/// class MyWidget extends StatelessWidget {
///   MyWidget({ super.key, required this.children }) {
///     assert(!debugChildrenHaveDuplicateKeys(this, children));
///   }
///
///   final List<Widget> children;
///
///   // ...
/// }
/// ```
///
/// If specified, the `message` overrides the default message.
///
/// For a version of this function that can be used in contexts where
/// the list of items does not have a particular parent, see
/// [debugItemsHaveDuplicateKeys].
///
/// Does nothing if asserts are disabled. Always returns false.
bool debugChildrenHaveDuplicateKeys(Widget parent, Iterable<Widget> children, { String? message }) {
  assert(() {
    final Key? nonUniqueKey = _firstNonUniqueKey(children);
    if (nonUniqueKey != null) {
      throw FlutterError(
        "${message ?? 'Duplicate keys found.\n'
                      'If multiple keyed widgets exist as children of another widget, they must have unique keys.'}"
        '\n$parent has multiple children with key $nonUniqueKey.',
      );
    }
    return true;
  }());
  return false;
}

/// Asserts if the given list of items contains any duplicate non-null keys.
///
/// To invoke this function, use the following pattern:
///
/// ```dart
/// assert(!debugItemsHaveDuplicateKeys(items));
/// ```
///
/// For a version of this function specifically intended for parents
/// checking their children lists, see [debugChildrenHaveDuplicateKeys].
///
/// Does nothing if asserts are disabled. Always returns false.
bool debugItemsHaveDuplicateKeys(Iterable<Widget> items) {
  assert(() {
    final Key? nonUniqueKey = _firstNonUniqueKey(items);
    if (nonUniqueKey != null) {
      throw FlutterError('Duplicate key found: $nonUniqueKey.');
    }
    return true;
  }());
  return false;
}

/// Asserts that the given context has a [Table] ancestor.
///
/// Used by [TableRowInkWell] to make sure that it is only used in an appropriate context.
///
/// To invoke this function, use the following pattern, typically in the
/// relevant Widget's build method:
///
/// ```dart
/// assert(debugCheckHasTable(context));
/// ```
///
/// Always place this before any early returns, so that the invariant is checked
/// in all cases. This prevents bugs from hiding until a particular codepath is
/// hit.
///
/// This method can be expensive (it walks the element tree).
///
/// Does nothing if asserts are disabled. Always returns true.
bool debugCheckHasTable(BuildContext context) {
  assert(() {
    if (context.widget is! Table && context.findAncestorWidgetOfExactType<Table>() == null) {
      throw FlutterError.fromParts(<DiagnosticsNode>[
        ErrorSummary('No Table widget found.'),
        ErrorDescription('${context.widget.runtimeType} widgets require a Table widget ancestor.'),
        context.describeWidget('The specific widget that could not find a Table ancestor was'),
        context.describeOwnershipChain('The ownership chain for the affected widget is'),
      ]);
    }
    return true;
  }());
  return true;
}

/// Asserts that the given context has a [MediaQuery] ancestor.
///
/// Used by various widgets to make sure that they are only used in an
/// appropriate context.
///
/// To invoke this function, use the following pattern, typically in the
/// relevant Widget's build method:
///
/// ```dart
/// assert(debugCheckHasMediaQuery(context));
/// ```
///
/// Always place this before any early returns, so that the invariant is checked
/// in all cases. This prevents bugs from hiding until a particular codepath is
/// hit.
///
/// Does nothing if asserts are disabled. Always returns true.
bool debugCheckHasMediaQuery(BuildContext context) {
  assert(() {
    if (context.widget is! MediaQuery && context.getElementForInheritedWidgetOfExactType<MediaQuery>() == null) {
      throw FlutterError.fromParts(<DiagnosticsNode>[
        ErrorSummary('No MediaQuery widget ancestor found.'),
        ErrorDescription('${context.widget.runtimeType} widgets require a MediaQuery widget ancestor.'),
        context.describeWidget('The specific widget that could not find a MediaQuery ancestor was'),
        context.describeOwnershipChain('The ownership chain for the affected widget is'),
        ErrorHint(
          'No MediaQuery ancestor could be found starting from the context '
          'that was passed to MediaQuery.of(). This can happen because you '
          'have not added a WidgetsApp, CupertinoApp, or MaterialApp widget '
          '(those widgets introduce a MediaQuery), or it can happen if the '
          'context you use comes from a widget above those widgets.',
        ),
      ]);
    }
    return true;
  }());
  return true;
}

/// Asserts that the given context has a [Directionality] ancestor.
///
/// Used by various widgets to make sure that they are only used in an
/// appropriate context.
///
/// To invoke this function, use the following pattern, typically in the
/// relevant Widget's build method:
///
/// ```dart
/// assert(debugCheckHasDirectionality(context));
/// ```
///
/// To improve the error messages you can add some extra color using the
/// named arguments.
///
///  * why: explain why the direction is needed, for example "to resolve
///    the 'alignment' argument". Should be an adverb phrase describing why.
///  * hint: explain why this might be happening, for example "The default
///    value of the 'alignment' argument of the $runtimeType widget is an
///    AlignmentDirectional value.". Should be a fully punctuated sentence.
///  * alternative: provide additional advice specific to the situation,
///    especially an alternative to providing a Directionality ancestor.
///    For example, "Alternatively, consider specifying the 'textDirection'
///    argument.". Should be a fully punctuated sentence.
///
/// Each one can be null, in which case it is skipped (this is the default).
/// If they are non-null, they are included in the order above, interspersed
/// with the more generic advice regarding [Directionality].
///
/// Always place this before any early returns, so that the invariant is checked
/// in all cases. This prevents bugs from hiding until a particular codepath is
/// hit.
///
/// Does nothing if asserts are disabled. Always returns true.
bool debugCheckHasDirectionality(BuildContext context, { String? why, String? hint, String? alternative }) {
  assert(() {
    if (context.widget is! Directionality && context.getElementForInheritedWidgetOfExactType<Directionality>() == null) {
      why = why == null ? '' : ' $why';
      throw FlutterError.fromParts(<DiagnosticsNode>[
        ErrorSummary('No Directionality widget found.'),
        ErrorDescription('${context.widget.runtimeType} widgets require a Directionality widget ancestor$why.\n'),
        if (hint != null)
          ErrorHint(hint),
        context.describeWidget('The specific widget that could not find a Directionality ancestor was'),
        context.describeOwnershipChain('The ownership chain for the affected widget is'),
        ErrorHint(
          'Typically, the Directionality widget is introduced by the MaterialApp '
          'or WidgetsApp widget at the top of your application widget tree. It '
          'determines the ambient reading direction and is used, for example, to '
          'determine how to lay out text, how to interpret "start" and "end" '
          'values, and to resolve EdgeInsetsDirectional, '
          'AlignmentDirectional, and other *Directional objects.',
        ),
        if (alternative != null)
          ErrorHint(alternative),
      ]);
    }
    return true;
  }());
  return true;
}

/// Asserts that the `built` widget is not null.
///
/// Used when the given `widget` calls a builder function to check that the
/// function returned a non-null value, as typically required.
///
/// Does nothing when asserts are disabled.
void debugWidgetBuilderValue(Widget widget, Widget? built) {
  assert(() {
    if (built == null) {
      throw FlutterError.fromParts(<DiagnosticsNode>[
        ErrorSummary('A build function returned null.'),
        DiagnosticsProperty<Widget>('The offending widget is', widget, style: DiagnosticsTreeStyle.errorProperty),
        ErrorDescription('Build functions must never return null.'),
        ErrorHint(
          'To return an empty space that causes the building widget to fill available room, return "Container()". '
          'To return an empty space that takes as little room as possible, return "Container(width: 0.0, height: 0.0)".',
        ),
      ]);
    }
    if (widget == built) {
      throw FlutterError.fromParts(<DiagnosticsNode>[
        ErrorSummary('A build function returned context.widget.'),
        DiagnosticsProperty<Widget>('The offending widget is', widget, style: DiagnosticsTreeStyle.errorProperty),
        ErrorDescription(
          'Build functions must never return their BuildContext parameter\'s widget or a child that contains "context.widget". '
          'Doing so introduces a loop in the widget tree that can cause the app to crash.',
        ),
      ]);
    }
    return true;
  }());
}

/// Asserts that the given context has a [Localizations] ancestor that contains
/// a [WidgetsLocalizations] delegate.
///
/// To call this function, use the following pattern, typically in the
/// relevant Widget's build method:
///
/// ```dart
/// assert(debugCheckHasWidgetsLocalizations(context));
/// ```
///
/// Always place this before any early returns, so that the invariant is checked
/// in all cases. This prevents bugs from hiding until a particular codepath is
/// hit.
///
/// Does nothing if asserts are disabled. Always returns true.
bool debugCheckHasWidgetsLocalizations(BuildContext context) {
  assert(() {
    if (Localizations.of<WidgetsLocalizations>(context, WidgetsLocalizations) == null) {
      throw FlutterError.fromParts(<DiagnosticsNode>[
        ErrorSummary('No WidgetsLocalizations found.'),
        ErrorDescription(
          '${context.widget.runtimeType} widgets require WidgetsLocalizations '
          'to be provided by a Localizations widget ancestor.',
        ),
        ErrorDescription(
          'The widgets library uses Localizations to generate messages, '
          'labels, and abbreviations.',
        ),
        ErrorHint(
          'To introduce a WidgetsLocalizations, either use a '
          'WidgetsApp at the root of your application to include them '
          'automatically, or add a Localization widget with a '
          'WidgetsLocalizations delegate.',
        ),
        ...context.describeMissingAncestor(expectedAncestorType: WidgetsLocalizations),
      ]);
    }
    return true;
  }());
  return true;
}

/// Asserts that the given context has an [Overlay] ancestor.
///
/// To call this function, use the following pattern, typically in the
/// relevant Widget's build method:
///
/// ```dart
/// assert(debugCheckHasOverlay(context));
/// ```
///
/// Always place this before any early returns, so that the invariant is checked
/// in all cases. This prevents bugs from hiding until a particular codepath is
/// hit.
///
/// This method can be expensive (it walks the element tree).
///
/// Does nothing if asserts are disabled. Always returns true.
bool debugCheckHasOverlay(BuildContext context) {
  assert(() {
    if (LookupBoundary.findAncestorWidgetOfExactType<Overlay>(context) == null) {
      final bool hiddenByBoundary = LookupBoundary.debugIsHidingAncestorWidgetOfExactType<Overlay>(context);
      throw FlutterError.fromParts(<DiagnosticsNode>[
        ErrorSummary('No Overlay widget found${hiddenByBoundary ? ' within the closest LookupBoundary' : ''}.'),
        if (hiddenByBoundary)
          ErrorDescription(
              'There is an ancestor Overlay widget, but it is hidden by a LookupBoundary.'
          ),
        ErrorDescription(
          '${context.widget.runtimeType} widgets require an Overlay '
          'widget ancestor within the closest LookupBoundary.\n'
          'An overlay lets widgets float on top of other widget children.',
        ),
        ErrorHint(
          'To introduce an Overlay widget, you can either directly '
          'include one, or use a widget that contains an Overlay itself, '
          'such as a Navigator, WidgetApp, MaterialApp, or CupertinoApp.',
        ),
        ...context.describeMissingAncestor(expectedAncestorType: Overlay),
      ]);
    }
    return true;
  }());
  return true;
}

/// Returns true if none of the widget library debug variables have been changed.
///
/// This function is used by the test framework to ensure that debug variables
/// haven't been inadvertently changed.
///
/// See [the widgets library](widgets/widgets-library.html) for a complete list.
bool debugAssertAllWidgetVarsUnset(String reason) {
  assert(() {
    if (debugPrintRebuildDirtyWidgets ||
        debugPrintBuildScope ||
        debugPrintScheduleBuildForStacks ||
        debugPrintGlobalKeyedWidgetLifecycle ||
        debugProfileBuildsEnabled ||
        debugHighlightDeprecatedWidgets ||
        debugProfileBuildsEnabledUserWidgets) {
      throw FlutterError(reason);
    }
    return true;
  }());
  return true;
}
