/* Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.

Licensed 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. */

#include <popart/tensor.hpp>
#include <popart/tensors.hpp>
#include <popart/tensorindex.hpp>
#include <popart/op.hpp>
#include <popart/op/matmul.hpp>
#include <popart/op/dropout.hpp>
#include <popart/op/mul.hpp>
#include <popart/op/collectives/replicatedreducescatter.hpp>
#include <popart/op/collectives/replicatedallgather.hpp>
#include <popart/op/adamupdater.hpp>
#include <popart/op/adamvarupdate.hpp>
#include <popart/op/accumulate.hpp>
#include <popart/popx/op/matmulx.hpp>
#include <popart/logging.hpp>
#include <queue>

template <class T, popart::ExecutionContext Ctx = popart::ExecutionContext::Normal>
static T *search_producers_for(popart::Tensor *t, int max_depth=-1) {

    // Searched as far as we can without success
    if (t->tensorType() == popart::TensorType::Variable || !t->hasProducer()) {
        return nullptr;
    }
    auto op = t->getProducer();
    if (op->isConvertibleTo<T>() && op->settings.executionContext == Ctx) {
        return dynamic_cast<T *>(op);
    }

    if (op->input->n() < 1) {
        return nullptr;
    }

    unsigned producer_index = 0;
    if (op->input->n() > 1) {
        if (op->isConvertibleTo<popart::AdamUpdaterOp>()) {
            producer_index = popart::AdamUpdaterOp::getAccl1InIndex();
        } else if (op->isConvertibleTo<popart::AdamVarUpdateOp>()) {
            producer_index = popart::AdamVarUpdateOp::getUpdaterInIndex();
        } else if (op->isConvertibleTo<popart::AccumulateBaseOp>()) {
            producer_index = popart::AccumulateBaseOp::getUpdaterInIndex();
        } else if (op->isConvertibleTo<popart::DropoutGradOp>()) {
            producer_index = popart::DropoutGradOp::getGradInIndex();
        } else if (op->isConvertibleTo<popart::MulOp>()) {
            // Grad Unscaling for Adam-based optimizers
            producer_index = popart::MulOp::getArg0InIndex();
        } else if (op->isConvertibleTo<popart::ReplicatedReduceScatterOp>()) {
            // Replicated Tensor Sharding
            producer_index = popart::ReplicatedReduceScatterOp::getInIndex();
        } else if (op->isConvertibleTo<popart::ReplicatedAllGatherOp>()) {
            // Replicated Tensor Sharding
            producer_index = popart::ReplicatedAllGatherOp::getInIndex();
        } else {
            return nullptr;
        }
    }

    // Providing a max-search depth of -1 will remove the depth limit at the cost of potentially
    // unnecessary checks.
    if (max_depth > 0) {
        max_depth -= 1;
        if (max_depth == 0) {
            return nullptr;
        }
    }

    return search_producers_for<T, Ctx>(op->input->tensor(producer_index), max_depth);
}

// Finds the underlying variable by searching through producers.
static popart::Tensor *get_variable(popart::Tensor *t) {
    if (t->tensorType() == popart::TensorType::Variable || t->tensorType() == popart::TensorType::Const) {
        return t;
    } else if (!t->hasProducer()) {
        return nullptr;
    }
    auto op = t->getProducer();
    if (op->input->n() != 1) {
        return nullptr;
    }
    return get_variable(op->input->tensors().front());
}

// Attempts to find T by searching through consumers.
template <class T, popart::ExecutionContext Ctx = popart::ExecutionContext::Normal>
static T *search_consumers_for(popart::Tensor *w, std::queue<popart::Tensor *> &q) {
    for (auto consumer : w->consumers.getOps()) {
        if (consumer->isConvertibleTo<T>() && consumer->settings.executionContext == Ctx) {
            return dynamic_cast<T *>(consumer);
        }

        if (consumer->isConvertibleTo<popart::DropoutGradOp>()) {
            q.push(consumer->output->tensor(popart::DropoutGradOp::getGradInIndex()));
        }
        if (consumer->isConvertibleTo<popart::ReplicatedReduceScatterOp>()) {
          q.push(consumer->output->tensor(
              popart::ReplicatedReduceScatterOp::getOutIndex()));
        }

        // TODO: Improve this as it's too general. Most ops that have one input and one output are view changing.
        if (consumer->input->n() == 1 && consumer->output->n() == 1) {
            q.push(consumer->output->tensor(0));
        }
    }
    if (q.size() < 1) {
        return nullptr;
    }
    w = q.front();
    q.pop();
    return search_consumers_for<T, Ctx>(w, q);
}
template <class T, popart::ExecutionContext Ctx = popart::ExecutionContext::Normal>
static T *search_consumers_for(popart::Tensor *w) {
    std::queue<popart::Tensor *> q;
    return search_consumers_for<T, Ctx>(w, q);
}

template <class T>
static T *weight_consumed_by(popart::Tensor *w) {
    w = get_variable(w);
    if (w) {
        return search_consumers_for<T>(w);
    }
    return nullptr;
}

template <class T, popart::ExecutionContext Ctx>
static void find_all_consumers(popart::Tensor *w,std::queue<popart::Tensor *> &q, std::vector<T *> &result) {
    for (auto consumer : w->consumers.getOps()) {
        if (std::find(result.begin(), result.end(), consumer) == result.end()) {
            if (consumer->isConvertibleTo<T>() && consumer->settings.executionContext == Ctx) {
                result.push_back(dynamic_cast<T *>(consumer));
            }
            if (consumer->isConvertibleTo<popart::MatMulOp>()) {
                q.push(consumer->output->tensor(popart::MatMulOp::getOutIndex()));
            }
            if (consumer->isConvertibleTo<popart::ReplicatedReduceScatterOp>()) {
              q.push(consumer->output->tensor(
                  popart::ReplicatedReduceScatterOp::getOutIndex()));
            }
            // Most ops that have one input and one output are view changing.
            if (consumer->input->n() == 1 && consumer->output->n() == 1) {
                q.push(consumer->output->tensor(0));
            }
        }
    }
    if (q.size() < 1) {
        return;
    }
    w = q.front();
    q.pop();
    return find_all_consumers<T, Ctx>(w, q, result);
}
template <class T, popart::ExecutionContext Ctx = popart::ExecutionContext::Normal>
static std::vector<T *> find_all_consumers(popart::Tensor *w) {
    std::queue<popart::Tensor *> q;
    std::vector<T *> result;
    find_all_consumers<T, Ctx>(w, q, result);
    return result;
}
