// Copyright 2020 The IREE Authors
//
// Licensed under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception

#include <utility>

#include "iree/compiler/Dialect/HAL/Analysis/DeviceAnalysis.h"
#include "iree/compiler/Dialect/HAL/IR/HALDialect.h"
#include "iree/compiler/Dialect/HAL/IR/HALOps.h"
#include "iree/compiler/Dialect/HAL/Transforms/Passes.h"
#include "iree/compiler/Dialect/Util/IR/UtilDialect.h"
#include "iree/compiler/Utils/StringUtils.h"
#include "mlir/IR/Attributes.h"
#include "mlir/IR/Builders.h"
#include "mlir/IR/BuiltinTypes.h"
#include "mlir/IR/Diagnostics.h"
#include "mlir/Pass/Pass.h"

namespace mlir::iree_compiler::IREE::HAL {

#define GEN_PASS_DEF_MEMOIZEDEVICEQUERIESPASS
#include "iree/compiler/Dialect/HAL/Transforms/Passes.h.inc"

namespace {

//===----------------------------------------------------------------------===//
// --iree-hal-memoize-device-queries
//===----------------------------------------------------------------------===//

// All queries for a particular !hal.device global.
struct DeviceQueries {
  // Global !hal.device.
  IREE::Util::GlobalOpInterface deviceOp;
  // [category, key, default] used for lookup/indexing.
  SmallVector<Attribute> queryKeys;
  // Ops performing queries against the device by [category, key, default].
  DenseMap<Attribute, SmallVector<IREE::HAL::DeviceQueryOp>> queryOps;
};

// A query being replaced by global lookups.
struct Query {
  Query(Location loc) : loc(loc) {}
  Location loc;
  IREE::Util::GlobalOp okGlobalOp;
  IREE::Util::GlobalOp valueGlobalOp;
  StringAttr categoryAttr;
  StringAttr keyAttr;
  TypedAttr defaultValueAttr;
};

static std::string getDeviceNamePrefix(IREE::Util::GlobalOpInterface deviceOp) {
  StringRef deviceName = deviceOp.getGlobalName().getValue();
  if (deviceName.starts_with("__")) {
    return deviceName.str();
  }
  auto prefixedName = "__" + deviceName;
  return prefixedName.str();
}

// NOTE: this implementation is just for a single active device. As we start to
// support multiple devices we'll need to change this to be per-device.
struct MemoizeDeviceQueriesPass
    : public IREE::HAL::impl::MemoizeDeviceQueriesPassBase<
          MemoizeDeviceQueriesPass> {
  void runOnOperation() override {
    auto moduleOp = getOperation();

    // Analyze the module to determine which devices are used where.
    DeviceAnalysis deviceAnalysis(moduleOp);
    if (failed(deviceAnalysis.run())) {
      return signalPassFailure();
    }

    // Prepare device table indexed by symbol name.
    DenseMap<Attribute, DeviceQueries> allDeviceQueries;
    for (auto deviceOp : deviceAnalysis.getDeviceGlobals()) {
      allDeviceQueries[deviceOp.getGlobalName()].deviceOp = deviceOp;
    }

    // Find all query ops we want to memoize and group them together.
    // This lets us easily replace all usages of a match with a single variable.
    for (auto callableOp : moduleOp.getOps<mlir::CallableOpInterface>()) {
      callableOp.walk([&](IREE::HAL::DeviceQueryOp queryOp) {
        // Try to find the device this query is made on. If analysis failed then
        // we can't memoize the query today.
        auto deviceGlobals =
            deviceAnalysis.lookupDeviceGlobals(queryOp.getDevice());
        if (!deviceGlobals || deviceGlobals->size() != 1)
          return WalkResult::advance();
        IREE::Util::GlobalOpInterface deviceGlobalOp = deviceGlobals->front();

        // Construct key used to dedupe/lookup the query.
        auto fullKey = ArrayAttr::get(
            moduleOp.getContext(),
            {
                StringAttr::get(moduleOp.getContext(),
                                queryOp.getCategory() + queryOp.getKey()),
                queryOp.getDefaultValue().has_value()
                    ? queryOp.getDefaultValueAttr()
                    : Attribute{},
            });

        // Track the query on the device.
        auto &deviceQueries = allDeviceQueries[deviceGlobalOp.getGlobalName()];
        auto lookup = deviceQueries.queryOps.try_emplace(
            fullKey, SmallVector<IREE::HAL::DeviceQueryOp>{});
        if (lookup.second) {
          deviceQueries.queryKeys.push_back(std::move(fullKey));
        }
        lookup.first->second.push_back(queryOp);

        return WalkResult::advance();
      });
    }

    // Create each query variable and replace the uses with loads.
    SymbolTable symbolTable(moduleOp);
    auto moduleBuilder = OpBuilder::atBlockBegin(moduleOp.getBody());
    for (auto deviceOp : deviceAnalysis.getDeviceGlobals()) {
      auto &deviceQueries = allDeviceQueries[deviceOp.getGlobalName()];
      if (deviceQueries.queryKeys.empty()) {
        // No queries against this device.
        continue;
      }

      // Create one global per unique query key against the device.
      SmallVector<Query> queries;
      moduleBuilder.setInsertionPointAfter(deviceOp);
      for (auto [i, queryKey] : llvm::enumerate(deviceQueries.queryKeys)) {
        auto &queryOps = deviceQueries.queryOps[queryKey];
        auto queryLoc = moduleBuilder.getFusedLoc(llvm::map_to_vector(
            queryOps, [&](auto queryOp) { return queryOp.getLoc(); }));

        // Create a global for the ok flag and the queried value.
        // TODO(benvanik): create a better name based on the key.
        auto anyQueryOp = queryOps.front();
        auto queryType = anyQueryOp.getValue().getType();
        std::string variableName =
            getDeviceNamePrefix(deviceOp) + "_query_" + std::to_string(i) +
            "_" + sanitizeSymbolName(anyQueryOp.getCategory()) + "_" +
            sanitizeSymbolName(anyQueryOp.getKey());
        auto okGlobalOp = moduleBuilder.create<IREE::Util::GlobalOp>(
            queryLoc, variableName + "_ok",
            /*isMutable=*/false, moduleBuilder.getI1Type());
        symbolTable.insert(okGlobalOp);
        okGlobalOp.setPrivate();
        auto valueGlobalOp = moduleBuilder.create<IREE::Util::GlobalOp>(
            queryLoc, variableName,
            /*isMutable=*/false, queryType);
        symbolTable.insert(valueGlobalOp);
        valueGlobalOp.setPrivate();

        // Stash the globals for initialization.
        Query query(queryLoc);
        query.okGlobalOp = okGlobalOp;
        query.valueGlobalOp = valueGlobalOp;
        query.categoryAttr = anyQueryOp.getCategoryAttr();
        query.keyAttr = anyQueryOp.getKeyAttr();
        query.defaultValueAttr = anyQueryOp.getDefaultValueAttr();
        queries.push_back(query);

        // Replace all queries with loads of the global values.
        for (auto queryOp : queryOps) {
          OpBuilder replaceBuilder(queryOp);
          auto okLoadOp =
              okGlobalOp.createLoadOp(queryOp.getLoc(), replaceBuilder);
          auto resultLoadOp =
              valueGlobalOp.createLoadOp(queryOp.getLoc(), replaceBuilder);
          queryOp.replaceAllUsesWith(ValueRange{
              okLoadOp.getLoadedGlobalValue(),
              resultLoadOp.getLoadedGlobalValue(),
          });
          queryOp.erase();
        }
      }

      // Create an initializer for the device where we will perform all queries.
      auto fusedLoc = moduleBuilder.getFusedLoc(
          llvm::map_to_vector(queries, [&](auto &query) { return query.loc; }));
      auto initializerOp =
          moduleBuilder.create<IREE::Util::InitializerOp>(fusedLoc);
      auto funcBuilder = OpBuilder::atBlockBegin(initializerOp.addEntryBlock());
      Value device =
          deviceOp.createLoadOp(fusedLoc, funcBuilder).getLoadedGlobalValue();
      for (auto [i, queryKey] : llvm::enumerate(deviceQueries.queryKeys)) {
        auto &query = queries[i];
        auto queryOp = funcBuilder.create<IREE::HAL::DeviceQueryOp>(
            fusedLoc, funcBuilder.getI1Type(),
            query.valueGlobalOp.getGlobalType(), device, query.categoryAttr,
            query.keyAttr, query.defaultValueAttr);
        query.okGlobalOp.createStoreOp(fusedLoc, queryOp.getOk(), funcBuilder);
        query.valueGlobalOp.createStoreOp(fusedLoc, queryOp.getValue(),
                                          funcBuilder);
      }
      funcBuilder.create<IREE::Util::ReturnOp>(fusedLoc);
    }
  }
};

} // namespace

} // namespace mlir::iree_compiler::IREE::HAL
