// Copyright 2015 The Chromium 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' as math;
import 'dart:typed_data';

import 'package:flutter/foundation.dart';
import 'package:vector_math/vector_math_64.dart';

import 'basic_types.dart';

/// Utility functions for working with matrices.
class MatrixUtils {
  MatrixUtils._();

  /// Returns the given [transform] matrix as an [Offset], if the matrix is
  /// nothing but a 2D translation.
  ///
  /// Otherwise, returns null.
  static Offset getAsTranslation(Matrix4 transform) {
    assert(transform != null);
    final Float64List values = transform.storage;
    // Values are stored in column-major order.
    if (values[0] == 1.0 && // col 1
        values[1] == 0.0 &&
        values[2] == 0.0 &&
        values[3] == 0.0 &&
        values[4] == 0.0 && // col 2
        values[5] == 1.0 &&
        values[6] == 0.0 &&
        values[7] == 0.0 &&
        values[8] == 0.0 && // col 3
        values[9] == 0.0 &&
        values[10] == 1.0 &&
        values[11] == 0.0 &&
        values[14] == 0.0 && // bottom of col 4 (values 12 and 13 are the x and y offsets)
        values[15] == 1.0) {
      return new Offset(values[12], values[13]);
    }
    return null;
  }

  /// Returns the given [transform] matrix as a [double] describing a uniform
  /// scale, if the matrix is nothing but a symmetric 2D scale transform.
  ///
  /// Otherwise, returns null.
  static double getAsScale(Matrix4 transform) {
    assert(transform != null);
    final Float64List values = transform.storage;
    // Values are stored in column-major order.
    if (values[1] == 0.0 && // col 1 (value 0 is the scale)
        values[2] == 0.0 &&
        values[3] == 0.0 &&
        values[4] == 0.0 && // col 2 (value 5 is the scale)
        values[6] == 0.0 &&
        values[7] == 0.0 &&
        values[8] == 0.0 && // col 3
        values[9] == 0.0 &&
        values[10] == 1.0 &&
        values[11] == 0.0 &&
        values[12] == 0.0 && // col 4
        values[13] == 0.0 &&
        values[14] == 0.0 &&
        values[15] == 1.0 &&
        values[0] == values[5]) { // uniform scale
      return values[0];
    }
    return null;
  }

  /// Returns true if the given matrices are exactly equal, and false
  /// otherwise. Null values are assumed to be the identity matrix.
  static bool matrixEquals(Matrix4 a, Matrix4 b) {
    if (identical(a, b))
      return true;
    assert(a != null || b != null);
    if (a == null)
      return isIdentity(b);
    if (b == null)
      return isIdentity(a);
    assert(a != null && b != null);
    return a.storage[0] == b.storage[0]
        && a.storage[1] == b.storage[1]
        && a.storage[2] == b.storage[2]
        && a.storage[3] == b.storage[3]
        && a.storage[4] == b.storage[4]
        && a.storage[5] == b.storage[5]
        && a.storage[6] == b.storage[6]
        && a.storage[7] == b.storage[7]
        && a.storage[8] == b.storage[8]
        && a.storage[9] == b.storage[9]
        && a.storage[10] == b.storage[10]
        && a.storage[11] == b.storage[11]
        && a.storage[12] == b.storage[12]
        && a.storage[13] == b.storage[13]
        && a.storage[14] == b.storage[14]
        && a.storage[15] == b.storage[15];
  }

  /// Whether the given matrix is the identity matrix.
  static bool isIdentity(Matrix4 a) {
    assert(a != null);
    return a.storage[0] == 1.0 // col 1
        && a.storage[1] == 0.0
        && a.storage[2] == 0.0
        && a.storage[3] == 0.0
        && a.storage[4] == 0.0 // col 2
        && a.storage[5] == 1.0
        && a.storage[6] == 0.0
        && a.storage[7] == 0.0
        && a.storage[8] == 0.0 // col 3
        && a.storage[9] == 0.0
        && a.storage[10] == 1.0
        && a.storage[11] == 0.0
        && a.storage[12] == 0.0 // col 4
        && a.storage[13] == 0.0
        && a.storage[14] == 0.0
        && a.storage[15] == 1.0;
  }

  /// Applies the given matrix as a perspective transform to the given point.
  ///
  /// This function assumes the given point has a z-coordinate of 0.0. The
  /// z-coordinate of the result is ignored.
  static Offset transformPoint(Matrix4 transform, Offset point) {
    final Vector3 position3 = new Vector3(point.dx, point.dy, 0.0);
    final Vector3 transformed3 = transform.perspectiveTransform(position3);
    return new Offset(transformed3.x, transformed3.y);
  }

  /// Returns a rect that bounds the result of applying the given matrix as a
  /// perspective transform to the given rect.
  ///
  /// This function assumes the given rect is in the plane with z equals 0.0.
  /// The transformed rect is then projected back into the plane with z equals
  /// 0.0 before computing its bounding rect.
  static Rect transformRect(Matrix4 transform, Rect rect) {
    final Offset point1 = transformPoint(transform, rect.topLeft);
    final Offset point2 = transformPoint(transform, rect.topRight);
    final Offset point3 = transformPoint(transform, rect.bottomLeft);
    final Offset point4 = transformPoint(transform, rect.bottomRight);
    return new Rect.fromLTRB(
        _min4(point1.dx, point2.dx, point3.dx, point4.dx),
        _min4(point1.dy, point2.dy, point3.dy, point4.dy),
        _max4(point1.dx, point2.dx, point3.dx, point4.dx),
        _max4(point1.dy, point2.dy, point3.dy, point4.dy)
    );
  }

  static double _min4(double a, double b, double c, double d) {
    return math.min(a, math.min(b, math.min(c, d)));
  }
  static double _max4(double a, double b, double c, double d) {
    return math.max(a, math.max(b, math.max(c, d)));
  }

  /// Returns a rect that bounds the result of applying the inverse of the given
  /// matrix as a perspective transform to the given rect.
  ///
  /// This function assumes the given rect is in the plane with z equals 0.0.
  /// The transformed rect is then projected back into the plane with z equals
  /// 0.0 before computing its bounding rect.
  static Rect inverseTransformRect(Matrix4 transform, Rect rect) {
    assert(rect != null);
    assert(transform.determinant != 0.0);
    if (isIdentity(transform))
      return rect;
    transform = new Matrix4.copy(transform)..invert();
    return transformRect(transform, rect);
  }

  /// Create a transformation matrix which mimics the effects of tangentially
  /// wrapping the plane on which this transform is applied around a cylinder
  /// and then looking at the cylinder from a point outside the cylinder.
  ///
  /// The `radius` simulates the radius of the cylinder the plane is being
  /// wrapped onto. If the transformation is applied to a 0-dimensional dot
  /// instead of a plane, the dot would simply translate by +/- `radius` pixels
  /// along the `orientation` [Axis] when rotating from 0 to +/- 90 degrees.
  ///
  /// A positive radius means the object is closest at 0 `angle` and a negative
  /// radius means the object is closest at π `angle` or 180 degrees.
  ///
  /// The `angle` argument is the difference in angle in radians between the
  /// object and the viewing point. A positive `angle` on a positive `radius`
  /// moves the object up when `orientation` is vertical and right when
  /// horizontal.
  ///
  /// The transformation is always done such that a 0 `angle` keeps the
  /// transformed object at exactly the same size as before regardless of
  /// `radius` and `perspective` when `radius` is positive.
  ///
  /// The `perspective` argument is a number between 0 and 1 where 0 means
  /// looking at the object from infinitely far with an infinitely narrow field
  /// of view and 1 means looking at the object from infinitely close with an
  /// infinitely wide field of view. Defaults to a sane but arbitrary 0.001.
  ///
  /// The `orientation` is the direction of the rotation axis.
  ///
  /// Because the viewing position is a point, it's never possible to see the
  /// outer side of the cylinder at or past +/- π / 2 or 90 degrees and it's
  /// almost always possible to end up seeing the inner side of the cylinder
  /// or the back side of the transformed plane before π / 2 when perspective > 0.
  static Matrix4 createCylindricalProjectionTransform({
    @required double radius,
    @required double angle,
    double perspective: 0.001,
    Axis orientation: Axis.vertical,
  }) {
    assert(radius != null);
    assert(angle != null);
    assert(perspective >= 0 && perspective <= 1.0);
    assert(orientation != null);

    // Pre-multiplied matrix of a projection matrix and a view matrix.
    //
    // Projection matrix is a simplified perspective matrix
    // http://web.iitd.ac.in/~hegde/cad/lecture/L9_persproj.pdf
    // in the form of
    // [[1.0, 0.0, 0.0, 0.0],
    //  [0.0, 1.0, 0.0, 0.0],
    //  [0.0, 0.0, 1.0, 0.0],
    //  [0.0, 0.0, -perspective, 1.0]]
    //
    // View matrix is a simplified camera view matrix.
    // Basically re-scales to keep object at original size at angle = 0 at
    // any radius in the form of
    // [[1.0, 0.0, 0.0, 0.0],
    //  [0.0, 1.0, 0.0, 0.0],
    //  [0.0, 0.0, 1.0, -radius],
    //  [0.0, 0.0, 0.0, 1.0]]
    Matrix4 result = new Matrix4.identity()
        ..setEntry(3, 2, -perspective)
        ..setEntry(2, 3, -radius)
        ..setEntry(3, 3, perspective * radius + 1.0);

    // Model matrix by first translating the object from the origin of the world
    // by radius in the z axis and then rotating against the world.
    result *= (
        orientation == Axis.horizontal
            ? new Matrix4.rotationY(angle)
            : new Matrix4.rotationX(angle)
    ) * new Matrix4.translationValues(0.0, 0.0, radius);

    // Essentially perspective * view * model.
    return result;
  }
}

/// Returns a list of strings representing the given transform in a format
/// useful for [TransformProperty].
///
/// If the argument is null, returns a list with the single string "null".
List<String> debugDescribeTransform(Matrix4 transform) {
  if (transform == null)
    return const <String>['null'];
  final List<String> matrix = transform.toString().split('\n').map((String s) => '  $s').toList();
  matrix.removeLast();
  return matrix;
}

/// Property which handles [Matrix4] that represent transforms.
class TransformProperty extends DiagnosticsProperty<Matrix4> {
  /// Create a diagnostics property for [Matrix4] objects.
  ///
  /// The [showName] and [level] arguments must not be null.
  TransformProperty(String name, Matrix4 value, {
    bool showName: true,
    Object defaultValue: kNoDefaultValue,
    DiagnosticLevel level: DiagnosticLevel.info,
  }) : assert(showName != null),
       assert(level != null),
       super(
         name,
         value,
         showName: showName,
         defaultValue: defaultValue,
         level: level,
       );

  @override
  String valueToString({ TextTreeConfiguration parentConfiguration }) {
    if (parentConfiguration != null && !parentConfiguration.lineBreakProperties) {
      // Format the value on a single line to be compatible with the parent's
      // style.
      final List<Vector4> rows = <Vector4>[
        value.getRow(0),
        value.getRow(1),
        value.getRow(2),
        value.getRow(3),
      ];
      return '[${rows.join("; ")}]';
    }
    return debugDescribeTransform(value).join('\n');
  }
}
