/*
 * Copyright 2021 Google LLC
 *
 * Use of this source code is governed by a BSD-style license that can be
 * found in the LICENSE file.
 */

#ifndef skgpu_graphite_geom_Transform_DEFINED
#define skgpu_graphite_geom_Transform_DEFINED

#include "include/core/SkM44.h"
#include "include/core/SkMatrix.h"
#include "include/private/base/SkAssert.h"
#include "include/private/base/SkFloatingPoint.h"

#include <utility>

namespace skgpu::graphite {

class Rect;

// Transform encapsulates an SkM44 matrix, its inverse, and other properties dependent on the
// original matrix value that are useful when rendering.
class Transform {
public:
    // Type classifies the transform into coarse categories so that certain optimizations or
    // properties can be queried efficiently
    enum class Type : unsigned {
        // Applying the matrix to a vector or point is a no-op, so could be skipped entirely.
        kIdentity,
        // The matrix transforms a rect to another rect, without mirrors or rotations, so both
        // pre-and-post transform coordinates can be exactly represented as rects.
        kSimpleRectStaysRect,
        // The matrix transforms a rect to another rect, but may mirror or rotate the corners
        // relative to each other. This means that the post-transformed rect completely fills
        // that space.
        kRectStaysRect,
        // The matrix transform may have skew or rotation, so a mapped rect does not fill space,
        // but there is no need to perform perspective division or w-plane clipping. This also
        // includes orthographic projections.
        kAffine,
        // The matrix includes perspective and requires further projection to 2D, so care must be
        // taken when w is less than or near 0, and homogeneous division and perspective-correct
        // interpolation are needed when rendering.
        kPerspective,
        // The matrix is not invertible or not finite, so should not be used to draw.
        kInvalid,
    };

    explicit Transform(SkM44::Uninitialized_Constructor)
            : fM(SkM44::kUninitialized_Constructor)
            , fInvM(SkM44::kUninitialized_Constructor)
            , fType(Type::kInvalid) {}

    explicit Transform(const SkM44& m);
    Transform(const Transform& t) = default;

    static constexpr Transform Identity() {
        return Transform(SkM44(), SkM44(), Type::kIdentity, 1.f, 1.f);
    }
    static constexpr Transform Invalid() {
        return Transform(SkM44(SkM44::kNaN_Constructor), SkM44(SkM44::kNaN_Constructor),
                         Type::kInvalid, 1.f, 1.f);
    }

    static inline Transform Translate(float x, float y) {
        if (x == 0.f && y == 0.f) {
            return Identity();
        } else if (SkIsFinite(x, y)) {
            return Transform(SkM44::Translate(x, y), SkM44::Translate(-x, -y),
                             Type::kSimpleRectStaysRect, 1.f, 1.f);
        } else {
            return Invalid();
        }
    }

    static inline Transform Inverse(const Transform& t) {
        return Transform(t.fInvM, t.fM, t.fType, 1.f / t.fMaxScaleFactor, 1.f / t.fMinScaleFactor);
    }

    Transform& operator=(const Transform& t) = default;

    operator const SkM44&() const { return fM; }
    operator SkMatrix() const { return fM.asM33(); }

    bool operator!=(const Transform& t) const { return !(*this == t); }
    bool operator==(const Transform& t) const {
        return this->valid() == t.valid() && (!this->valid() || fM == t.fM);
    }

    const SkM44& matrix() const { return fM; }
    const SkM44& inverse() const { return fInvM; }

    Type type() const { return fType; }
    bool valid() const { return fType != Type::kInvalid; }

    // Return the {min,max} scale factor at the pre-transformed location 'p'. A unit circle about
    // 'p' transformed by this Transform will be contained in an ellipse with radii equal to 'min'
    // and 'max', e.g. moving 1 local unit will move at least 'min' pixels and at most 'max' pixels
    std::pair<float, float> scaleFactors(const SkV2& p) const;

    // This is valid for non-projection types and 1.0 for projection matrices.
    float maxScaleFactor() const {
        SkASSERT(this->valid());
        return fMaxScaleFactor;
    }

    // Return the minimum distance needed to move in local (pre-transform) space to ensure that the
    // transformed coordinates are at least 1px away from the original mapped point. This minimum
    // distance is specific to the given local 'bounds' since the scale factors change with
    // perspective.
    //
    // If the bounds would be clipped by the w=0 plane or otherwise is ill-conditioned, this will
    // return positive infinity.
    float localAARadius(const Rect& bounds) const;

    Rect mapRect(const Rect& rect) const;
    Rect inverseMapRect(const Rect& rect) const;

    void mapPoints(const Rect& localRect, SkV4 deviceOut[4]) const;
    void mapPoints(const SkV2* localIn, SkV4* deviceOut, int count) const;

    void mapPoints(const SkV4* localIn, SkV4* deviceOut, int count) const;
    void inverseMapPoints(const SkV4* deviceIn, SkV4* localOut, int count) const;

    // Returns a transform equal to the pre- or post-translation of this matrix
    Transform preTranslate(float x, float y) const {
        return this->concat(SkM44::Translate(x, y));
    }
    Transform postTranslate(float x, float y) const {
        return Translate(x, y).concat(*this);
    }

    // Returns a transform equal to (this * t)
    Transform concat(const Transform& t) const {
        SkASSERT(this->valid());
        return Transform(fM * t.fM);
    }
    Transform concat(const SkM44& t) const {
        SkASSERT(this->valid());
        return Transform(fM * t);
    }

    // Returns a transform equal to (this * t^-1)
    Transform concatInverse(const Transform& t) const {
        SkASSERT(this->valid());
        return Transform(fM * t.fInvM);
    }
    Transform concatInverse(const SkM44& t) const {
        SkASSERT(this->valid());
        // Saves a multiply compared to inverting just 't' and calculating both fM*t^-1 and t*fInvM
        // (t * this^-1)^-1 = this * t^-1
        return Inverse(Transform(t * fInvM));
    }

private:
    // Used for static factories that have known properties
    constexpr Transform(const SkM44& m, const SkM44& invM, Type type,
                        float minScale, float maxScale)
            : fM(m)
            , fInvM(invM)
            , fType(type)
            , fMinScaleFactor(minScale)
            , fMaxScaleFactor(maxScale) {}

    SkM44 fM;
    SkM44 fInvM; // M^-1
    Type  fType;

    // These are cached for non-projection transforms since they are constant; projection matrices
    // must be computed per point, and these values are ignored.
    float fMinScaleFactor = 1.f;
    float fMaxScaleFactor = 1.f;
};

} // namespace skgpu::graphite

#endif // skgpu_graphite_geom_Transform_DEFINED
