/*
 * Copyright (C) 2014 The Android Open Source Project
 *
 * 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 "base/arena_allocator.h"
#include "base/malloc_arena_pool.h"
#include "nodes.h"
#include "parallel_move_resolver.h"

#include "gtest/gtest-typed-test.h"
#include "gtest/gtest.h"

namespace art {

constexpr int kScratchRegisterStartIndexForTest = 100;

static void DumpRegisterForTest(std::ostream& os, int reg) {
  if (reg >= kScratchRegisterStartIndexForTest) {
    os << "T" << reg - kScratchRegisterStartIndexForTest;
  } else {
    os << reg;
  }
}

static void DumpLocationForTest(std::ostream& os, Location location) {
  if (location.IsConstant()) {
    os << "C";
  } else if (location.IsPair()) {
    DumpRegisterForTest(os, location.low());
    os << ",";
    DumpRegisterForTest(os, location.high());
  } else if (location.IsRegister()) {
    DumpRegisterForTest(os, location.reg());
  } else if (location.IsStackSlot()) {
    os << location.GetStackIndex() << "(sp)";
  } else {
    DCHECK(location.IsDoubleStackSlot())<< location;
    os << "2x" << location.GetStackIndex() << "(sp)";
  }
}

class TestParallelMoveResolverWithSwap : public ParallelMoveResolverWithSwap {
 public:
  explicit TestParallelMoveResolverWithSwap(ArenaAllocator* allocator)
      : ParallelMoveResolverWithSwap(allocator) {}

  void EmitMove(size_t index) override {
    MoveOperands* move = moves_[index];
    if (!message_.str().empty()) {
      message_ << " ";
    }
    message_ << "(";
    DumpLocationForTest(message_, move->GetSource());
    message_ << " -> ";
    DumpLocationForTest(message_, move->GetDestination());
    message_ << ")";
  }

  void EmitSwap(size_t index) override {
    MoveOperands* move = moves_[index];
    if (!message_.str().empty()) {
      message_ << " ";
    }
    message_ << "(";
    DumpLocationForTest(message_, move->GetSource());
    message_ << " <-> ";
    DumpLocationForTest(message_, move->GetDestination());
    message_ << ")";
  }

  void SpillScratch(int reg ATTRIBUTE_UNUSED) override {}
  void RestoreScratch(int reg ATTRIBUTE_UNUSED) override {}

  std::string GetMessage() const {
    return  message_.str();
  }

 private:
  std::ostringstream message_;


  DISALLOW_COPY_AND_ASSIGN(TestParallelMoveResolverWithSwap);
};

class TestParallelMoveResolverNoSwap : public ParallelMoveResolverNoSwap {
 public:
  explicit TestParallelMoveResolverNoSwap(ArenaAllocator* allocator)
      : ParallelMoveResolverNoSwap(allocator), scratch_index_(kScratchRegisterStartIndexForTest) {}

  void PrepareForEmitNativeCode() override {
    scratch_index_ = kScratchRegisterStartIndexForTest;
  }

  void FinishEmitNativeCode() override {}

  Location AllocateScratchLocationFor(Location::Kind kind) override {
    if (kind == Location::kStackSlot || kind == Location::kFpuRegister ||
        kind == Location::kRegister) {
      kind = Location::kRegister;
    } else {
      // Allocate register pair for double stack slot which simulates 32-bit backend's behavior.
      kind = Location::kRegisterPair;
    }
    Location scratch = GetScratchLocation(kind);
    if (scratch.Equals(Location::NoLocation())) {
      AddScratchLocation(Location::RegisterLocation(scratch_index_));
      AddScratchLocation(Location::RegisterLocation(scratch_index_ + 1));
      AddScratchLocation(Location::RegisterPairLocation(scratch_index_, scratch_index_ + 1));
      scratch = (kind == Location::kRegister) ? Location::RegisterLocation(scratch_index_)
          : Location::RegisterPairLocation(scratch_index_, scratch_index_ + 1);
      scratch_index_ += 2;
    }
    return scratch;
  }

  void FreeScratchLocation(Location loc ATTRIBUTE_UNUSED) override {}

  void EmitMove(size_t index) override {
    MoveOperands* move = moves_[index];
    if (!message_.str().empty()) {
      message_ << " ";
    }
    message_ << "(";
    DumpLocationForTest(message_, move->GetSource());
    message_ << " -> ";
    DumpLocationForTest(message_, move->GetDestination());
    message_ << ")";
  }

  std::string GetMessage() const {
    return  message_.str();
  }

 private:
  std::ostringstream message_;

  int scratch_index_;

  DISALLOW_COPY_AND_ASSIGN(TestParallelMoveResolverNoSwap);
};

static HParallelMove* BuildParallelMove(ArenaAllocator* allocator,
                                        const size_t operands[][2],
                                        size_t number_of_moves) {
  HParallelMove* moves = new (allocator) HParallelMove(allocator);
  for (size_t i = 0; i < number_of_moves; ++i) {
    moves->AddMove(
        Location::RegisterLocation(operands[i][0]),
        Location::RegisterLocation(operands[i][1]),
        DataType::Type::kInt32,
        nullptr);
  }
  return moves;
}

template <typename T>
class ParallelMoveTest : public ::testing::Test {
 public:
  static const bool has_swap;
};

template<> const bool ParallelMoveTest<TestParallelMoveResolverWithSwap>::has_swap = true;
template<> const bool ParallelMoveTest<TestParallelMoveResolverNoSwap>::has_swap = false;

using ParallelMoveResolverTestTypes =
    ::testing::Types<TestParallelMoveResolverWithSwap, TestParallelMoveResolverNoSwap>;

TYPED_TEST_CASE(ParallelMoveTest, ParallelMoveResolverTestTypes);


TYPED_TEST(ParallelMoveTest, Dependency) {
  MallocArenaPool pool;
  ArenaAllocator allocator(&pool);

  {
    TypeParam resolver(&allocator);
    static constexpr size_t moves[][2] = {{0, 1}, {1, 2}};
    resolver.EmitNativeCode(BuildParallelMove(&allocator, moves, arraysize(moves)));
    if (TestFixture::has_swap) {
      ASSERT_STREQ("(1 -> 2) (0 -> 1)", resolver.GetMessage().c_str());
    } else {
      ASSERT_STREQ("(1 -> 2) (0 -> 1)", resolver.GetMessage().c_str());
    }
  }

  {
    TypeParam resolver(&allocator);
    static constexpr size_t moves[][2] = {{0, 1}, {1, 2}, {2, 3}, {1, 4}};
    resolver.EmitNativeCode(BuildParallelMove(&allocator, moves, arraysize(moves)));
    if (TestFixture::has_swap) {
      ASSERT_STREQ("(2 -> 3) (1 -> 2) (1 -> 4) (0 -> 1)", resolver.GetMessage().c_str());
    } else {
      ASSERT_STREQ("(2 -> 3) (1 -> 2) (0 -> 1) (2 -> 4)", resolver.GetMessage().c_str());
    }
  }
}

TYPED_TEST(ParallelMoveTest, Cycle) {
  MallocArenaPool pool;
  ArenaAllocator allocator(&pool);

  {
    TypeParam resolver(&allocator);
    static constexpr size_t moves[][2] = {{0, 1}, {1, 0}};
    resolver.EmitNativeCode(BuildParallelMove(&allocator, moves, arraysize(moves)));
    if (TestFixture::has_swap) {
      ASSERT_STREQ("(1 <-> 0)", resolver.GetMessage().c_str());
    } else {
      ASSERT_STREQ("(1 -> T0) (0 -> 1) (T0 -> 0)", resolver.GetMessage().c_str());
    }
  }

  {
    TypeParam resolver(&allocator);
    static constexpr size_t moves[][2] = {{0, 1}, {1, 2}, {1, 0}};
    resolver.EmitNativeCode(BuildParallelMove(&allocator, moves, arraysize(moves)));
    if (TestFixture::has_swap) {
      ASSERT_STREQ("(1 -> 2) (1 <-> 0)", resolver.GetMessage().c_str());
    } else {
      ASSERT_STREQ("(1 -> 2) (0 -> 1) (2 -> 0)", resolver.GetMessage().c_str());
    }
  }

  {
    TypeParam resolver(&allocator);
    static constexpr size_t moves[][2] = {{0, 1}, {1, 0}, {0, 2}};
    resolver.EmitNativeCode(BuildParallelMove(&allocator, moves, arraysize(moves)));
    if (TestFixture::has_swap) {
      ASSERT_STREQ("(0 -> 2) (1 <-> 0)", resolver.GetMessage().c_str());
    } else {
      ASSERT_STREQ("(0 -> 2) (1 -> 0) (2 -> 1)", resolver.GetMessage().c_str());
    }
  }

  {
    TypeParam resolver(&allocator);
    static constexpr size_t moves[][2] = {{0, 1}, {1, 2}, {2, 3}, {3, 4}, {4, 0}};
    resolver.EmitNativeCode(BuildParallelMove(&allocator, moves, arraysize(moves)));
    if (TestFixture::has_swap) {
      ASSERT_STREQ("(4 <-> 0) (3 <-> 4) (2 <-> 3) (1 <-> 2)", resolver.GetMessage().c_str());
    } else {
      ASSERT_STREQ("(4 -> T0) (3 -> 4) (2 -> 3) (1 -> 2) (0 -> 1) (T0 -> 0)",
          resolver.GetMessage().c_str());
    }
  }
}

TYPED_TEST(ParallelMoveTest, ConstantLast) {
  MallocArenaPool pool;
  ArenaAllocator allocator(&pool);
  TypeParam resolver(&allocator);
  HParallelMove* moves = new (&allocator) HParallelMove(&allocator);
  moves->AddMove(
      Location::ConstantLocation(new (&allocator) HIntConstant(0)),
      Location::RegisterLocation(0),
      DataType::Type::kInt32,
      nullptr);
  moves->AddMove(
      Location::RegisterLocation(1),
      Location::RegisterLocation(2),
      DataType::Type::kInt32,
      nullptr);
  resolver.EmitNativeCode(moves);
  ASSERT_STREQ("(1 -> 2) (C -> 0)", resolver.GetMessage().c_str());
}

TYPED_TEST(ParallelMoveTest, Pairs) {
  MallocArenaPool pool;
  ArenaAllocator allocator(&pool);

  {
    TypeParam resolver(&allocator);
    HParallelMove* moves = new (&allocator) HParallelMove(&allocator);
    moves->AddMove(
        Location::RegisterLocation(2),
        Location::RegisterLocation(4),
        DataType::Type::kInt32,
        nullptr);
    moves->AddMove(
        Location::RegisterPairLocation(0, 1),
        Location::RegisterPairLocation(2, 3),
        DataType::Type::kInt64,
        nullptr);
    resolver.EmitNativeCode(moves);
    ASSERT_STREQ("(2 -> 4) (0,1 -> 2,3)", resolver.GetMessage().c_str());
  }

  {
    TypeParam resolver(&allocator);
    HParallelMove* moves = new (&allocator) HParallelMove(&allocator);
    moves->AddMove(
        Location::RegisterPairLocation(0, 1),
        Location::RegisterPairLocation(2, 3),
        DataType::Type::kInt64,
        nullptr);
    moves->AddMove(
        Location::RegisterLocation(2),
        Location::RegisterLocation(4),
        DataType::Type::kInt32,
        nullptr);
    resolver.EmitNativeCode(moves);
    ASSERT_STREQ("(2 -> 4) (0,1 -> 2,3)", resolver.GetMessage().c_str());
  }

  {
    TypeParam resolver(&allocator);
    HParallelMove* moves = new (&allocator) HParallelMove(&allocator);
    moves->AddMove(
        Location::RegisterPairLocation(0, 1),
        Location::RegisterPairLocation(2, 3),
        DataType::Type::kInt64,
        nullptr);
    moves->AddMove(
        Location::RegisterLocation(2),
        Location::RegisterLocation(0),
        DataType::Type::kInt32,
        nullptr);
    resolver.EmitNativeCode(moves);
    if (TestFixture::has_swap) {
      ASSERT_STREQ("(0,1 <-> 2,3)", resolver.GetMessage().c_str());
    } else {
      ASSERT_STREQ("(2 -> T0) (0,1 -> 2,3) (T0 -> 0)", resolver.GetMessage().c_str());
    }
  }
  {
    TypeParam resolver(&allocator);
    HParallelMove* moves = new (&allocator) HParallelMove(&allocator);
    moves->AddMove(
        Location::RegisterLocation(2),
        Location::RegisterLocation(7),
        DataType::Type::kInt32,
        nullptr);
    moves->AddMove(
        Location::RegisterLocation(7),
        Location::RegisterLocation(1),
        DataType::Type::kInt32,
        nullptr);
    moves->AddMove(
        Location::RegisterPairLocation(0, 1),
        Location::RegisterPairLocation(2, 3),
        DataType::Type::kInt64,
        nullptr);
    resolver.EmitNativeCode(moves);
    if (TestFixture::has_swap) {
      ASSERT_STREQ("(0,1 <-> 2,3) (7 -> 1) (0 -> 7)", resolver.GetMessage().c_str());
    } else {
      ASSERT_STREQ("(0,1 -> T0,T1) (7 -> 1) (2 -> 7) (T0,T1 -> 2,3)",
          resolver.GetMessage().c_str());
    }
  }
  {
    TypeParam resolver(&allocator);
    HParallelMove* moves = new (&allocator) HParallelMove(&allocator);
    moves->AddMove(
        Location::RegisterLocation(2),
        Location::RegisterLocation(7),
        DataType::Type::kInt32,
        nullptr);
    moves->AddMove(
        Location::RegisterPairLocation(0, 1),
        Location::RegisterPairLocation(2, 3),
        DataType::Type::kInt64,
        nullptr);
    moves->AddMove(
        Location::RegisterLocation(7),
        Location::RegisterLocation(1),
        DataType::Type::kInt32,
        nullptr);
    resolver.EmitNativeCode(moves);
    if (TestFixture::has_swap) {
      ASSERT_STREQ("(0,1 <-> 2,3) (7 -> 1) (0 -> 7)", resolver.GetMessage().c_str());
    } else {
      ASSERT_STREQ("(0,1 -> T0,T1) (7 -> 1) (2 -> 7) (T0,T1 -> 2,3)",
          resolver.GetMessage().c_str());
    }
  }
  {
    TypeParam resolver(&allocator);
    HParallelMove* moves = new (&allocator) HParallelMove(&allocator);
    moves->AddMove(
        Location::RegisterPairLocation(0, 1),
        Location::RegisterPairLocation(2, 3),
        DataType::Type::kInt64,
        nullptr);
    moves->AddMove(
        Location::RegisterLocation(2),
        Location::RegisterLocation(7),
        DataType::Type::kInt32,
        nullptr);
    moves->AddMove(
        Location::RegisterLocation(7),
        Location::RegisterLocation(1),
        DataType::Type::kInt32,
        nullptr);
    resolver.EmitNativeCode(moves);
    if (TestFixture::has_swap) {
      ASSERT_STREQ("(0,1 <-> 2,3) (7 -> 1) (0 -> 7)", resolver.GetMessage().c_str());
    } else {
      ASSERT_STREQ("(7 -> T0) (2 -> 7) (0,1 -> 2,3) (T0 -> 1)", resolver.GetMessage().c_str());
    }
  }
  {
    TypeParam resolver(&allocator);
    HParallelMove* moves = new (&allocator) HParallelMove(&allocator);
    moves->AddMove(
        Location::RegisterPairLocation(0, 1),
        Location::RegisterPairLocation(2, 3),
        DataType::Type::kInt64,
        nullptr);
    moves->AddMove(
        Location::RegisterPairLocation(2, 3),
        Location::RegisterPairLocation(0, 1),
        DataType::Type::kInt64,
        nullptr);
    resolver.EmitNativeCode(moves);
    if (TestFixture::has_swap) {
      ASSERT_STREQ("(2,3 <-> 0,1)", resolver.GetMessage().c_str());
    } else {
      ASSERT_STREQ("(2,3 -> T0,T1) (0,1 -> 2,3) (T0,T1 -> 0,1)", resolver.GetMessage().c_str());
    }
  }
  {
    TypeParam resolver(&allocator);
    HParallelMove* moves = new (&allocator) HParallelMove(&allocator);
    moves->AddMove(
        Location::RegisterPairLocation(2, 3),
        Location::RegisterPairLocation(0, 1),
        DataType::Type::kInt64,
        nullptr);
    moves->AddMove(
        Location::RegisterPairLocation(0, 1),
        Location::RegisterPairLocation(2, 3),
        DataType::Type::kInt64,
        nullptr);
    resolver.EmitNativeCode(moves);
    if (TestFixture::has_swap) {
      ASSERT_STREQ("(0,1 <-> 2,3)", resolver.GetMessage().c_str());
    } else {
      ASSERT_STREQ("(0,1 -> T0,T1) (2,3 -> 0,1) (T0,T1 -> 2,3)", resolver.GetMessage().c_str());
    }
  }
}

TYPED_TEST(ParallelMoveTest, MultiCycles) {
  MallocArenaPool pool;
  ArenaAllocator allocator(&pool);

  {
    TypeParam resolver(&allocator);
    static constexpr size_t moves[][2] = {{0, 1}, {1, 0}, {2, 3}, {3, 2}};
    resolver.EmitNativeCode(BuildParallelMove(&allocator, moves, arraysize(moves)));
    if (TestFixture::has_swap) {
      ASSERT_STREQ("(1 <-> 0) (3 <-> 2)",  resolver.GetMessage().c_str());
    } else {
      ASSERT_STREQ("(1 -> T0) (0 -> 1) (T0 -> 0) (3 -> T0) (2 -> 3) (T0 -> 2)",
          resolver.GetMessage().c_str());
    }
  }
  {
    TypeParam resolver(&allocator);
    HParallelMove* moves = new (&allocator) HParallelMove(&allocator);
    moves->AddMove(
        Location::RegisterPairLocation(0, 1),
        Location::RegisterPairLocation(2, 3),
        DataType::Type::kInt64,
        nullptr);
    moves->AddMove(
        Location::RegisterLocation(2),
        Location::RegisterLocation(0),
        DataType::Type::kInt32,
        nullptr);
    moves->AddMove(
        Location::RegisterLocation(3),
        Location::RegisterLocation(1),
        DataType::Type::kInt32,
        nullptr);
    resolver.EmitNativeCode(moves);
    if (TestFixture::has_swap) {
      ASSERT_STREQ("(0,1 <-> 2,3)", resolver.GetMessage().c_str());
    } else {
      ASSERT_STREQ("(2 -> T0) (3 -> T1) (0,1 -> 2,3) (T0 -> 0) (T1 -> 1)",
          resolver.GetMessage().c_str());
    }
  }
  {
    TypeParam resolver(&allocator);
    HParallelMove* moves = new (&allocator) HParallelMove(&allocator);
    moves->AddMove(
        Location::RegisterLocation(2),
        Location::RegisterLocation(0),
        DataType::Type::kInt32,
        nullptr);
    moves->AddMove(
        Location::RegisterLocation(3),
        Location::RegisterLocation(1),
        DataType::Type::kInt32,
        nullptr);
    moves->AddMove(
        Location::RegisterPairLocation(0, 1),
        Location::RegisterPairLocation(2, 3),
        DataType::Type::kInt64,
        nullptr);
    resolver.EmitNativeCode(moves);
    if (TestFixture::has_swap) {
      ASSERT_STREQ("(0,1 <-> 2,3)", resolver.GetMessage().c_str());
    } else {
      ASSERT_STREQ("(3 -> T0) (0,1 -> T2,T3) (T0 -> 1) (2 -> 0) (T2,T3 -> 2,3)",
          resolver.GetMessage().c_str());
    }
  }

  {
    // Test involving registers used in single context and pair context.
    TypeParam resolver(&allocator);
    HParallelMove* moves = new (&allocator) HParallelMove(&allocator);
    moves->AddMove(
        Location::RegisterLocation(10),
        Location::RegisterLocation(5),
        DataType::Type::kInt32,
        nullptr);
    moves->AddMove(
        Location::RegisterPairLocation(4, 5),
        Location::DoubleStackSlot(32),
        DataType::Type::kInt64,
        nullptr);
    moves->AddMove(
        Location::DoubleStackSlot(32),
        Location::RegisterPairLocation(10, 11),
        DataType::Type::kInt64,
        nullptr);
    resolver.EmitNativeCode(moves);
    if (TestFixture::has_swap) {
      ASSERT_STREQ("(2x32(sp) <-> 10,11) (4,5 <-> 2x32(sp)) (4 -> 5)", resolver.GetMessage().c_str());
    } else {
      ASSERT_STREQ("(2x32(sp) -> T0,T1) (4,5 -> 2x32(sp)) (10 -> 5) (T0,T1 -> 10,11)",
          resolver.GetMessage().c_str());
    }
  }
}

// Test that we do 64bits moves before 32bits moves.
TYPED_TEST(ParallelMoveTest, CyclesWith64BitsMoves) {
  MallocArenaPool pool;
  ArenaAllocator allocator(&pool);

  {
    TypeParam resolver(&allocator);
    HParallelMove* moves = new (&allocator) HParallelMove(&allocator);
    moves->AddMove(
        Location::RegisterLocation(0),
        Location::RegisterLocation(1),
        DataType::Type::kInt64,
        nullptr);
    moves->AddMove(
        Location::RegisterLocation(1),
        Location::StackSlot(48),
        DataType::Type::kInt32,
        nullptr);
    moves->AddMove(
        Location::StackSlot(48),
        Location::RegisterLocation(0),
        DataType::Type::kInt32,
        nullptr);
    resolver.EmitNativeCode(moves);
    if (TestFixture::has_swap) {
      ASSERT_STREQ("(0 <-> 1) (48(sp) <-> 0)", resolver.GetMessage().c_str());
    } else {
      ASSERT_STREQ("(48(sp) -> T0) (1 -> 48(sp)) (0 -> 1) (T0 -> 0)",
          resolver.GetMessage().c_str());
    }
  }

  {
    TypeParam resolver(&allocator);
    HParallelMove* moves = new (&allocator) HParallelMove(&allocator);
    moves->AddMove(
        Location::RegisterPairLocation(0, 1),
        Location::RegisterPairLocation(2, 3),
        DataType::Type::kInt64,
        nullptr);
    moves->AddMove(
        Location::RegisterPairLocation(2, 3),
        Location::DoubleStackSlot(32),
        DataType::Type::kInt64,
        nullptr);
    moves->AddMove(
        Location::DoubleStackSlot(32),
        Location::RegisterPairLocation(0, 1),
        DataType::Type::kInt64,
        nullptr);
    resolver.EmitNativeCode(moves);
    if (TestFixture::has_swap) {
      ASSERT_STREQ("(2x32(sp) <-> 0,1) (2,3 <-> 2x32(sp))", resolver.GetMessage().c_str());
    } else {
      ASSERT_STREQ("(2x32(sp) -> T0,T1) (2,3 -> 2x32(sp)) (0,1 -> 2,3) (T0,T1 -> 0,1)",
          resolver.GetMessage().c_str());
    }
  }
}

TYPED_TEST(ParallelMoveTest, CyclesWith64BitsMoves2) {
  MallocArenaPool pool;
  ArenaAllocator allocator(&pool);

  {
    TypeParam resolver(&allocator);
    HParallelMove* moves = new (&allocator) HParallelMove(&allocator);
    moves->AddMove(
        Location::RegisterLocation(0),
        Location::RegisterLocation(3),
        DataType::Type::kInt32,
        nullptr);
    moves->AddMove(
        Location::RegisterPairLocation(2, 3),
        Location::RegisterPairLocation(0, 1),
        DataType::Type::kInt64,
        nullptr);
    moves->AddMove(
        Location::RegisterLocation(7),
        Location::RegisterLocation(2),
        DataType::Type::kInt32,
        nullptr);
    resolver.EmitNativeCode(moves);
    if (TestFixture::has_swap) {
      ASSERT_STREQ("(2,3 <-> 0,1) (2 -> 3) (7 -> 2)", resolver.GetMessage().c_str());
    } else {
      ASSERT_STREQ("(2,3 -> T0,T1) (0 -> 3) (T0,T1 -> 0,1) (7 -> 2)",
          resolver.GetMessage().c_str());
    }
  }
}

}  // namespace art
