// 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 'package:flutter/foundation.dart';

import 'framework.dart';
import 'inherited_model.dart';

/// The type of the [SharedAppData.getValue] `init` parameter.
///
/// This callback is used to lazily create the initial value for
/// a [SharedAppData] keyword.
typedef SharedAppDataInitCallback<T> = T Function();

/// Enables sharing key/value data with its `child` and all of the
/// child's descendants.
///
/// - `SharedAppData.getValue(context, key, initCallback)` creates a dependency
/// on the key and returns the value for the key from the shared data table.
/// If no value exists for key then the initCallback is used to create
/// the initial value.
///
/// - `SharedAppData.setValue(context, key, value)` changes the value of an entry
/// in the shared data table and forces widgets that depend on that entry
/// to be rebuilt.
///
/// A widget whose build method uses SharedAppData.getValue(context,
/// keyword, initCallback) creates a dependency on the SharedAppData. When
/// the value of keyword changes with SharedAppData.setValue(), the widget
/// will be rebuilt. The values managed by the SharedAppData are expected
/// to be immutable: intrinsic changes to values will not cause
/// dependent widgets to be rebuilt.
///
/// An instance of this widget is created automatically by [WidgetsApp].
///
/// There are many ways to share data with a widget subtree. This
/// class is based on [InheritedModel], which is an [InheritedWidget].
/// It's intended to be used by packages that need to share a modest
/// number of values among their own components.
///
/// SharedAppData is not intended to be a substitute for Provider or any of
/// the other general purpose application state systems. SharedAppData is
/// for situations where a package's custom widgets need to share one
/// or a handful of immutable data objects that can be lazily
/// initialized. It exists so that packages like that can deliver
/// custom widgets without requiring the developer to add a
/// package-specific umbrella widget to their application.
///
/// A good way to create an SharedAppData key that avoids potential
/// collisions with other packages is to use a static `Object()` value.
/// The `SharedObject` example below does this.
///
/// {@tool dartpad}
/// The following sample demonstrates using the automatically created
/// `SharedAppData`. Button presses cause changes to the values for keys
/// 'foo', and 'bar', and those changes only cause the widgets that
/// depend on those keys to be rebuilt.
///
/// ** See code in examples/api/lib/widgets/shared_app_data/shared_app_data.0.dart **
/// {@end-tool}
///
/// {@tool dartpad}
/// The following sample demonstrates how a single lazily computed
/// value could be shared within an app. A Flutter package that
/// provided custom widgets might use this approach to share a (possibly
/// private) value with instances of those widgets.
///
/// ** See code in examples/api/lib/widgets/shared_app_data/shared_app_data.1.dart **
/// {@end-tool}
class SharedAppData extends StatefulWidget {
  /// Creates a widget based on [InheritedModel] that supports build
  /// dependencies qualified by keywords. Descendant widgets create
  /// such dependencies with [SharedAppData.getValue] and they trigger
  /// rebuilds with [SharedAppData.setValue].
  ///
  /// This widget is automatically created by the [WidgetsApp].
  const SharedAppData({ Key? key, required this.child }) : super(key: key);

  /// The widget below this widget in the tree.
  ///
  /// {@macro flutter.widgets.ProxyWidget.child}
  final Widget child;

  @override
  State<StatefulWidget> createState() => _SharedAppDataState();

  /// Returns the app model's value for `key` and ensures that each
  /// time the value of `key` is changed with [SharedAppData.setValue], the
  /// specified context will be rebuilt.
  ///
  /// If no value for `key` exists then the `init` callback is used to
  /// generate an initial value. The callback is expected to return
  /// an immutable value because intrinsic changes to the value will
  /// not cause dependent widgets to be rebuilt.
  ///
  /// A widget that depends on the app model's value for `key` should use
  /// this method in their `build` methods to ensure that they are rebuilt
  /// if the value changes.
  ///
  /// The type parameter `K` is the type of the keyword and `V`
  /// is the type of the value.
  static V getValue<K extends Object, V>(BuildContext context, K key, SharedAppDataInitCallback<V> init) {
    final _SharedAppModel? model = InheritedModel.inheritFrom<_SharedAppModel>(context, aspect: key);
    assert(_debugHasSharedAppData(model, context, 'getValue'));
    return model!.sharedAppDataState.getValue<K, V>(key, init);
  }

  /// Changes the app model's `value` for `key` and rebuilds any widgets
  /// that have created a dependency on `key` with [SharedAppData.getValue].
  ///
  /// If `value` is `==` to the current value of `key` then nothing
  /// is rebuilt.
  ///
  /// The `value` is expected to be immutable because intrinsic
  /// changes to the value will not cause dependent widgets to be
  /// rebuilt.
  ///
  /// Unlike [SharedAppData.getValue], this method does _not_ create a dependency
  /// between `context` and `key`.
  ///
  /// The type parameter `K` is the type of the value's keyword and `V`
  /// is the type of the value.
  static void setValue<K extends Object, V>(BuildContext context, K key, V value) {
    final _SharedAppModel? model = context.getElementForInheritedWidgetOfExactType<_SharedAppModel>()?.widget as _SharedAppModel?;
    assert(_debugHasSharedAppData(model, context, 'setValue'));
    model!.sharedAppDataState.setValue<K, V>(key, value);
  }

  static bool _debugHasSharedAppData(_SharedAppModel? model, BuildContext context, String methodName) {
    assert(() {
      if (model == null) {
        throw FlutterError.fromParts(
          <DiagnosticsNode>[
            ErrorSummary('No SharedAppData widget found.'),
            ErrorDescription('SharedAppData.$methodName requires an SharedAppData widget ancestor.\n'),
            context.describeWidget('The specific widget that could not find an SharedAppData ancestor was'),
            context.describeOwnershipChain('The ownership chain for the affected widget is'),
            ErrorHint(
              'Typically, the SharedAppData widget is introduced by the MaterialApp '
              'or WidgetsApp widget at the top of your application widget tree. It '
              'provides a key/value map of data that is shared with the entire '
              'application.',
            ),
          ],
        );
      }
      return true;
    }());
    return true;
  }
}

class _SharedAppDataState extends State<SharedAppData> {
  late Map<Object, Object?> data = <Object, Object?>{};

  @override
  Widget build(BuildContext context) {
    return _SharedAppModel(sharedAppDataState: this, child: widget.child);
  }

  V getValue<K extends Object, V>(K key, SharedAppDataInitCallback<V> init) {
    data[key] ??= init();
    return data[key] as V;
  }

  void setValue<K extends Object, V>(K key, V value) {
    if (data[key] != value) {
      setState(() {
        data = Map<Object, Object?>.of(data);
        data[key] = value;
      });
    }
  }
}

class _SharedAppModel extends InheritedModel<Object> {
  _SharedAppModel({
    Key? key,
    required this.sharedAppDataState,
    required Widget child
  }) : data = sharedAppDataState.data, super(key: key, child: child);

  final _SharedAppDataState sharedAppDataState;
  final Map<Object, Object?> data;

  @override
  bool updateShouldNotify(_SharedAppModel old) {
    return data != old.data;
  }

  @override
  bool updateShouldNotifyDependent(_SharedAppModel old, Set<Object> keys) {
    for (final Object key in keys) {
      if (data[key] != old.data[key]) {
        return true;
      }
    }
    return false;
  }
}
