// 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:math' show max, min;

import 'package:flutter/foundation.dart';

/// A class that describes how textual contents should be scaled for better
/// readability.
///
/// The [scale] function computes the scaled font size given the original
/// unscaled font size specified by app developers.
///
/// The [==] operator defines the equality of 2 [TextScaler]s, which the
/// framework uses to determine whether text widgets should rebuild when their
/// [TextScaler] changes. Consider overriding the [==] operator if applicable
/// to avoid unnecessary rebuilds.
@immutable
abstract class TextScaler {
  /// Creates a TextScaler.
  const TextScaler();

  /// Creates a proportional [TextScaler] that scales the incoming font size by
  /// multiplying it with the given `textScaleFactor`.
  const factory TextScaler.linear(double textScaleFactor) = _LinearTextScaler;

  /// A [TextScaler] that doesn't scale the input font size.
  ///
  /// This is equivalent to `TextScaler.linear(1.0)`, the [TextScaler.scale]
  /// implementation always returns the input font size as-is.
  static const TextScaler noScaling = _LinearTextScaler(1.0);

  /// Computes the scaled font size (in logical pixels) with the given unscaled
  /// `fontSize` (in logical pixels).
  ///
  /// The input `fontSize` must be finite and non-negative.
  ///
  /// When given the same `fontSize` input, this method returns the same value.
  /// The output of a larger input `fontSize` is typically larger than that of a
  /// smaller input, but on unusual occasions they may produce the same output.
  /// For example, some platforms use single-precision floats to represent font
  /// sizes, as a result of truncation two different unscaled font sizes can be
  /// scaled to the same value.
  double scale(double fontSize);

  /// The estimated number of font pixels for each logical pixel. This property
  /// exists only for backward compatibility purposes, and will be removed in
  /// a future version of Flutter.
  ///
  /// The value of this property is only an estimate, so it may not reflect the
  /// exact text scaling strategy this [TextScaler] represents, especially when
  /// this [TextScaler] is not linear. Consider using [TextScaler.scale] instead.
  @Deprecated(
    'Use of textScaleFactor was deprecated in preparation for the upcoming nonlinear text scaling support. '
    'This feature was deprecated after v3.12.0-2.0.pre.',
  )
  double get textScaleFactor;

  /// Returns a new [TextScaler] that restricts the scaled font size to within
  /// the range `[minScaleFactor * fontSize, maxScaleFactor * fontSize]`.
  TextScaler clamp({ double minScaleFactor = 0, double maxScaleFactor = double.infinity }) {
    assert(maxScaleFactor >= minScaleFactor);
    assert(!maxScaleFactor.isNaN);
    assert(minScaleFactor.isFinite);
    assert(minScaleFactor >= 0);

    return minScaleFactor == maxScaleFactor
      ? TextScaler.linear(minScaleFactor)
      : _ClampedTextScaler(this, minScaleFactor, maxScaleFactor);
  }
}

final class _LinearTextScaler implements TextScaler {
  const _LinearTextScaler(this.textScaleFactor) : assert(textScaleFactor >= 0);

  @override
  final double textScaleFactor;

  @override
  double scale(double fontSize) {
    assert(fontSize >= 0);
    assert(fontSize.isFinite);
    return fontSize * textScaleFactor;
  }

  @override
  TextScaler clamp({ double minScaleFactor = 0, double maxScaleFactor = double.infinity }) {
    assert(maxScaleFactor >= minScaleFactor);
    assert(!maxScaleFactor.isNaN);
    assert(minScaleFactor.isFinite);
    assert(minScaleFactor >= 0);

    final double newScaleFactor = clampDouble(textScaleFactor, minScaleFactor, maxScaleFactor);
    return newScaleFactor == textScaleFactor ? this : _LinearTextScaler(newScaleFactor);
  }

  @override
  bool operator ==(Object other) {
    if (identical(this, other)) {
      return true;
    }
    return other is _LinearTextScaler && other.textScaleFactor == textScaleFactor;
  }

  @override
  int get hashCode => textScaleFactor.hashCode;

  @override
  String toString() => textScaleFactor == 1.0 ? 'no scaling' : 'linear (${textScaleFactor}x)';
}

final class _ClampedTextScaler implements TextScaler {
  const _ClampedTextScaler(this.scaler, this.minScale, this.maxScale) : assert(maxScale > minScale);
  final TextScaler scaler;
  final double minScale;
  final double maxScale;

  @override
  double get textScaleFactor => clampDouble(scaler.textScaleFactor, minScale, maxScale);

  @override
  double scale(double fontSize) {
    assert(fontSize >= 0);
    assert(fontSize.isFinite);
    return minScale == maxScale
      ? minScale * fontSize
      : clampDouble(scaler.scale(fontSize), minScale * fontSize, maxScale * fontSize);
  }

  @override
  TextScaler clamp({ double minScaleFactor = 0, double maxScaleFactor = double.infinity }) {
    return minScaleFactor == maxScaleFactor
      ? _LinearTextScaler(minScaleFactor)
      : _ClampedTextScaler(scaler, max(minScaleFactor, minScale), min(maxScaleFactor, maxScale));
  }

  @override
  bool operator ==(Object other) {
    if (identical(this, other)) {
      return true;
    }
    return other is _ClampedTextScaler
        && minScale == other.minScale
        && maxScale == other.maxScale
        && (minScale == maxScale || scaler == other.scaler);
  }

  @override
  int get hashCode => minScale == maxScale ? minScale.hashCode : Object.hash(scaler, minScale, maxScale);
}
