/**
 * Copyright 2016 Twitter. 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.
 */


package com.twitter.graphjet.bipartite.segment;

import java.util.List;
import java.util.Random;
import java.util.Set;

import com.google.common.collect.Lists;
import com.google.common.collect.Sets;

import org.apache.commons.lang3.tuple.Pair;
import org.junit.Test;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;

import com.twitter.graphjet.stats.NullStatsReceiver;

import it.unimi.dsi.fastutil.longs.LongArrayList;
import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
import it.unimi.dsi.fastutil.longs.LongSet;

import static com.twitter.graphjet.bipartite.GraphConcurrentTestHelper.testConcurrentReadWriteThreads;
import static com.twitter.graphjet.bipartite.GraphConcurrentTestHelper.testRandomConcurrentReadWriteThreads;

public class LeftRegularBipartiteGraphSegmentTest {
  private static final double EPSILON = 0.00001;

  private void addEdges(LeftRegularBipartiteGraphSegment leftRegularBipartiteGraphSegment) {
    leftRegularBipartiteGraphSegment.addEdge(1, 11, (byte) 0);
    leftRegularBipartiteGraphSegment.addEdge(1, 12, (byte) 0);
    leftRegularBipartiteGraphSegment.addEdge(4, 41, (byte) 0);
    leftRegularBipartiteGraphSegment.addEdge(2, 21, (byte) 0);
    leftRegularBipartiteGraphSegment.addEdge(4, 42, (byte) 0);
    leftRegularBipartiteGraphSegment.addEdge(3, 31, (byte) 0);
    leftRegularBipartiteGraphSegment.addEdge(2, 22, (byte) 0);
    leftRegularBipartiteGraphSegment.addEdge(1, 13, (byte) 0);
    leftRegularBipartiteGraphSegment.addEdge(4, 43, (byte) 0);
    leftRegularBipartiteGraphSegment.addEdge(5, 11, (byte) 0);
    // violates the max num nodes assumption
  }

  private void testAndResetGraph(
      LeftRegularBipartiteGraphSegment leftRegularBipartiteGraphSegment) {
    assertEquals(3, leftRegularBipartiteGraphSegment.getLeftNodeDegree(1));
    assertEquals(2, leftRegularBipartiteGraphSegment.getLeftNodeDegree(2));
    assertEquals(1, leftRegularBipartiteGraphSegment.getLeftNodeDegree(3));
    assertEquals(3, leftRegularBipartiteGraphSegment.getLeftNodeDegree(4));
    assertEquals(1, leftRegularBipartiteGraphSegment.getRightNodeDegree(13));
    assertEquals(2, leftRegularBipartiteGraphSegment.getRightNodeDegree(11));

    assertEquals(new LongArrayList(new long[]{11, 12, 13}),
        new LongArrayList(leftRegularBipartiteGraphSegment.getLeftNodeEdges(1)));
    assertEquals(new LongArrayList(new long[]{21, 22}),
        new LongArrayList(leftRegularBipartiteGraphSegment.getLeftNodeEdges(2)));
    assertEquals(new LongArrayList(new long[]{31}),
        new LongArrayList(leftRegularBipartiteGraphSegment.getLeftNodeEdges(3)));
    assertEquals(new LongArrayList(new long[]{41, 42, 43}),
        new LongArrayList(leftRegularBipartiteGraphSegment.getLeftNodeEdges(4)));
    assertEquals(new LongArrayList(new long[]{11}),
        new LongArrayList(leftRegularBipartiteGraphSegment.getLeftNodeEdges(5)));
    assertEquals(new LongArrayList(new long[]{1, 5}),
        new LongArrayList(leftRegularBipartiteGraphSegment.getRightNodeEdges(11)));
    assertEquals(new LongArrayList(new long[]{3}),
        new LongArrayList(leftRegularBipartiteGraphSegment.getRightNodeEdges(31)));

    Random random = new Random(90238490238409L);
    int numSamples = 5;

    assertEquals(new LongArrayList(new long[]{12, 11, 13, 11, 11}),
        new LongArrayList(
            leftRegularBipartiteGraphSegment.getRandomLeftNodeEdges(1, numSamples, random)));
    assertEquals(new LongArrayList(new long[]{22, 22, 22, 21, 21}),
        new LongArrayList(
            leftRegularBipartiteGraphSegment.getRandomLeftNodeEdges(2, numSamples, random)));
    assertEquals(new LongArrayList(new long[]{31, 31, 31, 31, 31}),
        new LongArrayList(
            leftRegularBipartiteGraphSegment.getRandomLeftNodeEdges(3, numSamples, random)));
    assertEquals(new LongArrayList(new long[]{43, 41, 43, 41, 42}),
        new LongArrayList(
            leftRegularBipartiteGraphSegment.getRandomLeftNodeEdges(4, numSamples, random)));
    assertEquals(new LongArrayList(new long[]{11, 11, 11, 11, 11}),
        new LongArrayList(
            leftRegularBipartiteGraphSegment.getRandomLeftNodeEdges(5, numSamples, random)));
    assertEquals(new LongArrayList(new long[]{5, 5, 5, 1, 5}),
        new LongArrayList(
            leftRegularBipartiteGraphSegment.getRandomRightNodeEdges(11, numSamples, random)));
    assertEquals(new LongArrayList(new long[]{2, 2, 2, 2, 2}),
        new LongArrayList(
            leftRegularBipartiteGraphSegment.getRandomRightNodeEdges(21, numSamples, random)));

    RecycleSegmentMemory.recycleLeftRegularBipartiteGraphSegment(leftRegularBipartiteGraphSegment);
  }

  /**
   * Build a random left-regular bipartite graph of given left and right sizes.
   *
   * @param leftSize   is the left hand size of the bipartite graph
   * @param rightSize  is the right hand size of the bipartite graph
   * @param leftDegree is the degree of the left hand side
   * @param random     is the random number generator to use for constructing the graph
   * @return a random bipartite graph
   */
  public static LeftRegularBipartiteGraphSegment buildRandomLeftRegularBipartiteGraph(
      int leftSize, int rightSize, int leftDegree, Random random) {
    LeftRegularBipartiteGraphSegment leftRegularBipartiteGraphSegment =
        new LeftRegularBipartiteGraphSegment(
            leftSize / 2,
            leftDegree,
            rightSize / 2,
            leftSize / 2,
            2.0,
            Integer.MAX_VALUE,
            new IdentityEdgeTypeMask(),
            new NullStatsReceiver());
    LongSet addedIds = new LongOpenHashSet(leftDegree);
    for (int i = 0; i < leftSize; i++) {
      addedIds.clear();
      for (int j = 0; j < leftDegree; j++) {
        long idToAdd;
        do {
          idToAdd = random.nextInt(rightSize);
        } while (addedIds.contains(idToAdd));
        addedIds.add(idToAdd);
        leftRegularBipartiteGraphSegment.addEdge(i, idToAdd, (byte) 0);
      }
    }

    return leftRegularBipartiteGraphSegment;
  }

  @Test
  public void testSegmentConstruction() throws Exception {
    LeftRegularBipartiteGraphSegment leftRegularBipartiteGraphSegment =
        new LeftRegularBipartiteGraphSegment(
            4, 3, 2, 1, 2.0, Integer.MAX_VALUE, new IdentityEdgeTypeMask(),
            new NullStatsReceiver());

    for (int i = 0; i < 3; i++) {
      addEdges(leftRegularBipartiteGraphSegment);
      testAndResetGraph(leftRegularBipartiteGraphSegment);
    }
  }

  @Test
  public void testRandomSegmentConstruction() throws Exception {
    int leftSize = 100;
    int rightSize = 10000;
    int leftDegree = 200;
    int numSamples = 10;

    for (int i = 0; i < 3; i++) {
      Random random = new Random(8904572034987501L);
      LeftRegularBipartiteGraphSegment leftRegularBipartiteGraphSegment =
          buildRandomLeftRegularBipartiteGraph(leftSize, rightSize, leftDegree, random);

      assertEquals(leftDegree, leftRegularBipartiteGraphSegment.getLeftNodeDegree(10));
      Set<Long> leftNodeEdgeSet =
          Sets.newHashSet(leftRegularBipartiteGraphSegment.getLeftNodeEdges(10));
      assertEquals(leftDegree, leftNodeEdgeSet.size());
      List<Long> leftNodeRandomEdgeSample = Lists.newArrayList(
          leftRegularBipartiteGraphSegment.getRandomLeftNodeEdges(10, numSamples, random));
      assertEquals(numSamples, leftNodeRandomEdgeSample.size());
      for (Long id : leftNodeRandomEdgeSample) {
        assertTrue(leftNodeEdgeSet.contains(id));
      }
      assertEquals(2, leftRegularBipartiteGraphSegment.getRightNodeDegree(395));
      Set<Long> rightNodeEdgeSet =
          Sets.newHashSet(leftRegularBipartiteGraphSegment.getRightNodeEdges(395));
      assertEquals(2, rightNodeEdgeSet.size());
      List<Long> rightNodeRandomEdgeSample = Lists.newArrayList(
          leftRegularBipartiteGraphSegment.getRandomRightNodeEdges(395, numSamples, random));
      assertEquals(numSamples, rightNodeRandomEdgeSample.size());
      for (Long id : rightNodeRandomEdgeSample) {
        assertTrue(rightNodeEdgeSet.contains(id));
      }

      // This is not a power-law on the right, so the RHS fill percentage should be pretty poor
      assertEquals(15.258789063,
          leftRegularBipartiteGraphSegment.getLeftNodeEdgePoolFillPercentage(), EPSILON);
      assertEquals(3.0517578125,
          leftRegularBipartiteGraphSegment.getRightNodeEdgePoolFillPercentage(), EPSILON);

      RecycleSegmentMemory
          .recycleLeftRegularBipartiteGraphSegment(leftRegularBipartiteGraphSegment);
    }
  }

  @Test
  public void testConcurrentReadWrites() throws Exception {
    LeftRegularBipartiteGraphSegment leftRegularBipartiteGraphSegment =
        new LeftRegularBipartiteGraphSegment(
            4, 3, 2, 1, 2.0, Integer.MAX_VALUE, new IdentityEdgeTypeMask(),
            new NullStatsReceiver());

    @SuppressWarnings("unchecked")
    List<Pair<Long, Long>> edgesToAdd = Lists.newArrayList(
        Pair.of(1L, 11L),
        Pair.of(1L, 12L),
        Pair.of(4L, 41L),
        Pair.of(2L, 21L),
        Pair.of(4L, 42L),
        Pair.of(3L, 31L),
        Pair.of(2L, 22L),
        Pair.of(1L, 13L),
        Pair.of(4L, 43L),
        Pair.of(5L, 51L) // violates the max num nodes assumption
    );

    testConcurrentReadWriteThreads(leftRegularBipartiteGraphSegment, edgesToAdd);
  }

  @Test
  public void testRandomConcurrentReadWrites() throws Exception {
    int numLeftNodes = 10;
    int numRightNodes = 100;
    LeftRegularBipartiteGraphSegment leftRegularBipartiteGraphSegment =
        new LeftRegularBipartiteGraphSegment(
            numLeftNodes,
            numRightNodes,
            numRightNodes,
            numRightNodes,
            2.0,
            Integer.MAX_VALUE,
            new IdentityEdgeTypeMask(),
            new NullStatsReceiver());

    // Sets up a concurrent read-write situation with the given pool and edges
    Random random = new Random(89234758923475L);

    testRandomConcurrentReadWriteThreads(
        leftRegularBipartiteGraphSegment, 3, 10 * numLeftNodes, numRightNodes, 0.1, random);
  }
}