/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

/*!
 * \file tvm/arith/int_solver.h
 * \brief integer constraints data structures and solvers
 */
#ifndef TVM_ARITH_INT_SOLVER_H_
#define TVM_ARITH_INT_SOLVER_H_

#include <tvm/ir/expr.h>
#include <tvm/tirx/expr.h>
#include <tvm/tirx/op.h>

#include <unordered_map>
#include <utility>
#include <vector>

#include "analyzer.h"

namespace tvm {
namespace arith {

using tirx::IterVar;
using tirx::Var;
using tirx::VarNode;

// According to experiments two best simplifications orders were can->rw and rw->can->rw,
// but rw->can->rw is better for a couple of cases.
// Also we should end with rw because it factors multipliers out.
constexpr int kSimplifyRewriteCanonicalRewrite = 3;

/*!
 * \brief Represent integer grouped bounds which are classified into
 *        lower bounds (inclusive), upper bounds (inclusive) and equalities.
 *        It also contains coefficient as a multiplier for the bounds, i.e.,
 *        coef * var >= lower
 *        coef * var == equal
 *        coef * var <= upper
 * \sa IntGroupBounds
 */
class IntGroupBoundsNode : public ffi::Object {
 public:
  PrimExpr coef;
  ffi::Array<PrimExpr> lower;
  ffi::Array<PrimExpr> equal;
  ffi::Array<PrimExpr> upper;

  static void RegisterReflection() {
    namespace refl = tvm::ffi::reflection;
    refl::ObjectDef<IntGroupBoundsNode>()
        .def_ro("coef", &IntGroupBoundsNode::coef)
        .def_ro("lower", &IntGroupBoundsNode::lower)
        .def_ro("equal", &IntGroupBoundsNode::equal)
        .def_ro("upper", &IntGroupBoundsNode::upper);
  }

  static constexpr TVMFFISEqHashKind _type_s_eq_hash_kind = kTVMFFISEqHashKindTreeNode;
  TVM_FFI_DECLARE_OBJECT_INFO_FINAL("arith.IntGroupBounds", IntGroupBoundsNode, ffi::Object);
};

/*!
 * \brief Managed reference to IntGroupBoundsNode.
 * \sa IntGroupBoundsNode
 */
class IntGroupBounds : public ffi::ObjectRef {
 public:
  /*!
   * \brief Constructor by fields
   * \param coef The coefficient. Must be integer.
   *        coef * var >= lower
   *        coef * var == equal
   *        coef * var >= upper
   * \param lower the lower bounds (include)
   * \param equal equalities
   * \param upper the upper bounds (include)
   */
  TVM_DLL IntGroupBounds(PrimExpr coef, ffi::Array<PrimExpr> lower, ffi::Array<PrimExpr> equal,
                         ffi::Array<PrimExpr> upper);

  /*!
   * \brief Construct bounds from a range.
   * \param r The range
   * \return constructed bounds.
   */
  static IntGroupBounds FromRange(const Range& r);

  /*!
   * \brief Perform substitution on all components of the struct.
   */
  IntGroupBounds Substitute(const ffi::Map<Var, PrimExpr>& subst) const;

  /*!
   * \brief Find the best range from the grouped bounds.
   * \param vranges_addl additional variable ranges that help infer the best range.
   * \return The best range (has the least difference between the lower bound and upper bound).
   *         undefined if (-inf, +inf).
   */
  Range FindBestRange(const ffi::Map<Var, Range>& vranges_addl = {}) const;

  /*!
   * \brief Combine the bounds with another range.
   * \param r range to be combined.
   * \return combined bounds.
   */
  IntGroupBounds operator+(const Range& r);

  TVM_FFI_DEFINE_OBJECT_REF_METHODS_NULLABLE(IntGroupBounds, ffi::ObjectRef, IntGroupBoundsNode);
};

/*!
 * \brief Represent integer constrains including (integer) variables, their ranges and
 *        the relations between them (either equations or inequalities).
 * \sa LinearSystem
 */
class IntConstraintsNode : public ffi::Object {
 public:
  // e.g., \alpha, \beta, must be integers
  ffi::Array<Var> variables;
  // e.g., 1 <= \alpha <= N, etc.
  // it is absolutely ok to include ranges for parameters
  // (variables that are not in this->variables) in this map
  ffi::Map<Var, Range> ranges;
  // linear equalities or inequalities
  // e.g., A \alpha = \beta or A \alpha <= \beta
  ffi::Array<PrimExpr> relations;

  static void RegisterReflection() {
    namespace refl = tvm::ffi::reflection;
    refl::ObjectDef<IntConstraintsNode>()
        .def_ro("variables", &IntConstraintsNode::variables)
        .def_ro("ranges", &IntConstraintsNode::ranges)
        .def_ro("relations", &IntConstraintsNode::relations);
  }

  static constexpr TVMFFISEqHashKind _type_s_eq_hash_kind = kTVMFFISEqHashKindTreeNode;
  TVM_FFI_DECLARE_OBJECT_INFO_FINAL("arith.IntConstraints", IntConstraintsNode, ffi::Object);
};

/*!
 * \brief Managed reference to IntConstraintsNode.
 * \sa IntConstraintsNode
 */
class IntConstraints : public ffi::ObjectRef {
 public:
  /*!
   * \brief Constructor by fields
   * \param variables The variables in the constraints, must be integers.
   * \param ranges    The ranges of the variables.
   * \param relations The linear relations between the variables
   *                  (either equations or inequalities)
   */
  TVM_DLL IntConstraints(ffi::Array<Var> variables, ffi::Map<Var, Range> ranges,
                         ffi::Array<PrimExpr> relations);

  TVM_FFI_DEFINE_OBJECT_REF_METHODS_NULLABLE(IntConstraints, ffi::ObjectRef, IntConstraintsNode);
};

/*!
 * \brief We can have different set of variables to represent the same constraints.
 *        For example, the following two systems are equivalent,
 *        {a + b = 0 | a >= 0, b >= 0} and
 *        {m - n = 0 | m >= 0, n <= 0}
 *        This data structure represents the transformation
 *        between two equivalent linear systems.
 *        In the above example,
 *        src        : {a + b = 0 | a >= 0, b >= 0}
 *        dst        : {m - n = 0 | m >= 0, n <= 0}
 *        src_to_dst : {a -> m, b -> -n}
 *        dst_to_src : {m -> a, n -> -b}
 * \sa IntConstraintsTransform
 */
class IntConstraintsTransformNode : public ffi::Object {
 public:
  IntConstraints src;
  IntConstraints dst;
  ffi::Map<Var, PrimExpr> src_to_dst;
  ffi::Map<Var, PrimExpr> dst_to_src;

  static void RegisterReflection() {
    namespace refl = tvm::ffi::reflection;
    refl::ObjectDef<IntConstraintsTransformNode>()
        .def_ro("src", &IntConstraintsTransformNode::src)
        .def_ro("dst", &IntConstraintsTransformNode::dst)
        .def_ro("src_to_dst", &IntConstraintsTransformNode::src_to_dst)
        .def_ro("dst_to_src", &IntConstraintsTransformNode::dst_to_src);
  }

  static constexpr TVMFFISEqHashKind _type_s_eq_hash_kind = kTVMFFISEqHashKindTreeNode;
  TVM_FFI_DECLARE_OBJECT_INFO_FINAL("arith.IntConstraintsTransform", IntConstraintsTransformNode,
                                    ffi::Object);
};

/*!
 * \brief Managed reference to IntConstraintsTransformNode.
 * \sa IntConstraintsTransformNode
 */
class IntConstraintsTransform : public ffi::ObjectRef {
 public:
  /*!
   * \brief Constructor by fields
   * \param src        source integer constraints, e.g., {a + b = 0 | a >= 0, b >= 0}
   * \param dst        integer constraints equivalent to the source,
   *                   e.g., {m - n = 0 | m >= 0, n <= 0}
   * \param src_to_dst mapping from variables in the \p src to the variables in the \p dst,
   *                   e.g., {a -> m, b -> -n}
   * \param dst_to_src mapping from variables in the \p dst to the variables in the \p src,
   *                   e.g., {m -> a, n -> -b}
   */
  TVM_DLL IntConstraintsTransform(IntConstraints src, IntConstraints dst,
                                  ffi::Map<Var, PrimExpr> src_to_dst,
                                  ffi::Map<Var, PrimExpr> dst_to_src);

  /*!
   * \brief Chain-compose two IntConstraintsTransform together.
   *        this->dst must be the same as other->src.
   * @param other another IntConstraintsTransform whose src is same as this->dst.
   * @return composed IntConstraintsTransform(this->src, other->dst)
   *         with its variables and ranges are properly modified.
   */
  IntConstraintsTransform operator+(const IntConstraintsTransform& other) const;

  TVM_FFI_DEFINE_OBJECT_REF_METHODS_NULLABLE(IntConstraintsTransform, ffi::ObjectRef,
                                             IntConstraintsTransformNode);
};

typedef std::pair<ffi::Map<Var, IntGroupBounds>, ffi::Array<PrimExpr>> PartialSolvedInequalities;

/*!
 * \brief Obtain Smith Normal Form of linear equation A x = y.
 *        Smith Normal Form of matrix A_{mxn} is S_{mxn} = U_{mxm} A_{mxn} V_{nxn},
 *        in which S_{mxn} is diag(s1, s2, ..., sr, 0, ..., 0) and r is the rank of A.
 *        NOTE: Although in standard Smith Normal Form the diagonal elements satisfy
 *              s_i | s_{i+1} (| means divides), the implement here does not guarantee it.
 *        TODO(yzhliu): From sergei-grechanik:
 *          computing the proper Smith normal form may improve stability of automatic
 * differentiation (generating the same gradient code for slightly different but equivalent input
 * code U_{mxm} and V_{nxn} are invertible matrices. This function modifies \p S to be S_{mxn}, \p V
 * to be V_{nxn}, \p y to be U_{mxm} y_{mx1} and \p x to be V^{-1} x. \param S  the original
 * A_{mxn}, it will be modified to S_{mxn} \param V  an identity matrix, it will be modified to
 * V_{nxn} \param x  the x in A x = y. it will be modified to V^{-1}_{nxn} x_{nx1} \param y  the y
 * in A x = y. it will be modified to U_{mxm} y_{mx1}
 */
void SmithNormalFormDiag(std::vector<std::vector<int64_t>>* S, std::vector<std::vector<int64_t>>* V,
                         std::vector<PrimExpr>* x, std::vector<PrimExpr>* y);

/*!
 * \brief Solve linear equations.
 * \param system_to_solve the variables to solve, their ranges, and a list of equations.
 * \return  A new linear system, with less variables (if \p system_to_solve is NOT of full rank),
 *          or no variable (if \p system_to_solve is of full rank),
 *          or an empty linear system (if \p system_to_solve is unsolvable).
 *          It also provides the ranges of the variables in the new system,
 *          as well as inequalities inferred from the \p system_to_solve.
 *          You can get the mapping from the original variables to the solution via ret->src_to_dst.
 */
IntConstraintsTransform SolveLinearEquations(const IntConstraints& system_to_solve);

/*!
 * \brief Solve linear inequalities.
 * \param system_to_solve the variables to solve, their ranges, and a list of inequalities.
 *        The inequalities are rewritten using Fourier-Motzkin elimination.
 *        This function takes an array of (in)equalities and an array of variables, and essentially
 *        rewrites the (in)equalities into an array of (in)equalities of the following form,
 *
 *        x0 >= f0(x1, x2, ..., xn)
 *        x0 <= g0(x1, x2, ..., xn)
 *        x1 >= f1(x2, ..., xn)
 *        x1 <= g1(x2, ..., xn)
 *        ...
 *        xn >= fn()  // just a constant
 *        xn <= gn()  // just a constant
 *
 * \return A map of variables and their solved bounds,
 *         and constrains that cannot be solved to bounds.
 */
PartialSolvedInequalities SolveLinearInequalities(const IntConstraints& system_to_solve);

/*!
 * \brief Combine the information into an array of (in)equalities.
 * \param variables The variables in \p bounds.
 *        It is used to determine the iteration order to avoid indeterministic results.
 * \param bounds grouped boundary of the variables.
 * \param relations other relations.
 */
ffi::Array<PrimExpr> AsConditions(const ffi::Array<Var>& variables,
                                  const ffi::Map<Var, IntGroupBounds>& bounds,
                                  const ffi::Array<PrimExpr>& relations);

/*!
 * \brief Solve linear inequalities and infer the range of each variable.
 * \param system_to_solve the variables to solve, their ranges, and a list of inequalities.
 * \return The result ranges for each variables.
 *         The returned IntConstraints(variables, ranges, relations) contains,
 *         1. variables  - the variables that have been solved.
 *         2. ranges     - the best range of each variable.
 *         3. relations  - constraints that cannot be transformed to
 *                         Range will be stored in relations.
 */
IntConstraints SolveInequalitiesToRange(const IntConstraints& system_to_solve);

/*!
 * \brief Solve linear inequalities and deskew the ranges towards zero.
 * \param system_to_solve the variables to solve, their ranges, and a list of inequalities.
 * \return A transform (src IntConstraints -> dst IntConstraints)
 *         from original variables to a set of new variables.
 *         The ranges of new variables always start from zero,
 *         their extents are solved from \p system_to_solve.
 *         src IntConstraints is the same as \p system_to_solve.
 *         dst IntConstraints(variables, ranges, relations) contains,
 *         1. variables  - the variables that have been solved.
 *         2. ranges     - the best range (start from zero) of each variable.
 *         3. relations  - constraints that cannot be transformed to
 *                         Range will be stored in relations.
 *         Variable mapping can be obtained from
 *         IntConstraintsTransform.src_to_dst and IntConstraintsTransform.dst_to_src.
 */
IntConstraintsTransform SolveInequalitiesDeskewRange(const IntConstraints& system_to_solve);

}  // namespace arith
}  // namespace tvm
#endif  // TVM_ARITH_INT_SOLVER_H_
