// Copyright (c) 2017 Google Inc.
//
// 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 "source/opt/flatten_decoration_pass.h"

#include <cassert>
#include <memory>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>

#include "source/opt/ir_context.h"

namespace spvtools {
namespace opt {

using Words = std::vector<uint32_t>;
using OrderedUsesMap = std::unordered_map<uint32_t, Words>;

Pass::Status FlattenDecorationPass::Process() {
  bool modified = false;

  // The target Id of OpDecorationGroup instructions.
  // We have to track this separately from its uses, in case it
  // has no uses.
  std::unordered_set<uint32_t> group_ids;
  // Maps a decoration group Id to its GroupDecorate targets, in order
  // of appearance.
  OrderedUsesMap normal_uses;
  // Maps a decoration group Id to its GroupMemberDecorate targets and
  // their indices, in of appearance.
  OrderedUsesMap member_uses;

  auto annotations = context()->annotations();

  // On the first pass, record each OpDecorationGroup with its ordered uses.
  // Rely on unordered_map::operator[] to create its entries on first access.
  for (const auto& inst : annotations) {
    switch (inst.opcode()) {
      case SpvOp::SpvOpDecorationGroup:
        group_ids.insert(inst.result_id());
        break;
      case SpvOp::SpvOpGroupDecorate: {
        Words& words = normal_uses[inst.GetSingleWordInOperand(0)];
        for (uint32_t i = 1; i < inst.NumInOperandWords(); i++) {
          words.push_back(inst.GetSingleWordInOperand(i));
        }
      } break;
      case SpvOp::SpvOpGroupMemberDecorate: {
        Words& words = member_uses[inst.GetSingleWordInOperand(0)];
        for (uint32_t i = 1; i < inst.NumInOperandWords(); i++) {
          words.push_back(inst.GetSingleWordInOperand(i));
        }
      } break;
      default:
        break;
    }
  }

  // On the second pass, replace OpDecorationGroup and its uses with
  // equivalent normal and struct member uses.
  auto inst_iter = annotations.begin();
  // We have to re-evaluate the end pointer
  while (inst_iter != context()->annotations().end()) {
    // Should we replace this instruction?
    bool replace = false;
    switch (inst_iter->opcode()) {
      case SpvOp::SpvOpDecorationGroup:
      case SpvOp::SpvOpGroupDecorate:
      case SpvOp::SpvOpGroupMemberDecorate:
        replace = true;
        break;
      case SpvOp::SpvOpDecorate: {
        // If this decoration targets a group, then replace it
        // by sets of normal and member decorations.
        const uint32_t group = inst_iter->GetSingleWordOperand(0);
        const auto normal_uses_iter = normal_uses.find(group);
        if (normal_uses_iter != normal_uses.end()) {
          for (auto target : normal_uses[group]) {
            std::unique_ptr<Instruction> new_inst(inst_iter->Clone(context()));
            new_inst->SetInOperand(0, Words{target});
            inst_iter = inst_iter.InsertBefore(std::move(new_inst));
            ++inst_iter;
            replace = true;
          }
        }
        const auto member_uses_iter = member_uses.find(group);
        if (member_uses_iter != member_uses.end()) {
          const Words& member_id_pairs = (*member_uses_iter).second;
          // The collection is a sequence of pairs.
          assert((member_id_pairs.size() % 2) == 0);
          for (size_t i = 0; i < member_id_pairs.size(); i += 2) {
            // Make an OpMemberDecorate instruction for each (target, member)
            // pair.
            const uint32_t target = member_id_pairs[i];
            const uint32_t member = member_id_pairs[i + 1];
            std::vector<Operand> operands;
            operands.push_back(Operand(SPV_OPERAND_TYPE_ID, {target}));
            operands.push_back(
                Operand(SPV_OPERAND_TYPE_LITERAL_INTEGER, {member}));
            auto decoration_operands_iter = inst_iter->begin();
            decoration_operands_iter++;  // Skip the group target.
            operands.insert(operands.end(), decoration_operands_iter,
                            inst_iter->end());
            std::unique_ptr<Instruction> new_inst(new Instruction(
                context(), SpvOp::SpvOpMemberDecorate, 0, 0, operands));
            inst_iter = inst_iter.InsertBefore(std::move(new_inst));
            ++inst_iter;
            replace = true;
          }
        }
        // If this is an OpDecorate targeting the OpDecorationGroup itself,
        // remove it even if that decoration group itself is not the target of
        // any OpGroupDecorate or OpGroupMemberDecorate.
        if (!replace && group_ids.count(group)) {
          replace = true;
        }
      } break;
      default:
        break;
    }
    if (replace) {
      inst_iter = inst_iter.Erase();
      modified = true;
    } else {
      // Handle the case of decorations unrelated to decoration groups.
      ++inst_iter;
    }
  }

  // Remove OpName instructions which reference the removed group decorations.
  // An OpDecorationGroup instruction might not have been used by an
  // OpGroupDecorate or OpGroupMemberDecorate instruction.
  if (!group_ids.empty()) {
    for (auto debug_inst_iter = context()->debug2_begin();
         debug_inst_iter != context()->debug2_end();) {
      if (debug_inst_iter->opcode() == SpvOp::SpvOpName) {
        const uint32_t target = debug_inst_iter->GetSingleWordOperand(0);
        if (group_ids.count(target)) {
          debug_inst_iter = debug_inst_iter.Erase();
          modified = true;
        } else {
          ++debug_inst_iter;
        }
      }
    }
  }

  return modified ? Status::SuccessWithChange : Status::SuccessWithoutChange;
}

}  // namespace opt
}  // namespace spvtools
