/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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 org.apache.drill.test.rowSet;

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

import java.math.BigDecimal;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.Map;

import org.apache.drill.common.types.TypeProtos.MajorType;
import org.apache.drill.common.types.TypeProtos.MinorType;
import org.apache.drill.exec.physical.rowSet.RowSet;
import org.apache.drill.exec.physical.rowSet.RowSetWriter;
import org.apache.drill.exec.record.BatchSchema;
import org.apache.drill.exec.record.MaterializedField;
import org.apache.drill.exec.record.metadata.MetadataUtils;
import org.apache.drill.exec.record.metadata.TupleMetadata;
import org.apache.drill.exec.record.selection.SelectionVector2;
import org.apache.drill.exec.vector.DateUtilities;
import org.apache.drill.exec.vector.accessor.ScalarWriter;
import org.apache.drill.exec.vector.accessor.ValueType;
import org.joda.time.Duration;
import org.joda.time.Period;

/**
 * Various utilities useful for working with row sets, especially for testing.
 */
public class RowSetUtilities {

  private RowSetUtilities() { }

  /**
   * Reverse a row set by reversing the entries in an SV2. This is a quick
   * and easy way to reverse the sort order of an expected-value row set.
   * @param sv2 the SV2 which is reversed in place
   */
  public static void reverse(SelectionVector2 sv2) {
    int count = sv2.getCount();
    for (int i = 0; i < count / 2; i++) {
      char temp = sv2.getIndex(i);
      int dest = count - 1 - i;
      sv2.setIndex(i, sv2.getIndex(dest));
      sv2.setIndex(dest, temp);
    }
  }

  /**
   * Set a test data value from an int. Uses the type information of the
   * column to handle interval types. Else, uses the value type of the
   * accessor. The value set here is purely for testing; the mapping
   * from ints to intervals has no real meaning.
   *
   * @param rowWriter writer where value will be written to
   * @param index     target index
   * @param value     value to write
   */
  public static void setFromInt(RowSetWriter rowWriter, int index, int value) {
    ScalarWriter writer = rowWriter.scalar(index);
    MaterializedField field = rowWriter.tupleSchema().column(index);
    writer.setObject(testDataFromInt(writer.valueType(), field.getType(), value));
  }

  /**
   * Create a test value that can be passed to setObject(). This value matches the
   * value type for a writer.
   */
  public static Object testDataFromInt(ValueType valueType, MajorType dataType, int value) {
    switch (valueType) {
    case BYTES:
      return Integer.toHexString(value).getBytes(StandardCharsets.UTF_8);
    case FLOAT:
      return (float) value;
    case DOUBLE:
      return (double) value;
    case INTEGER:
      switch (dataType.getMinorType()) {
      case BIT:
        return value & 0x01;
      case SMALLINT:
        return value & 0x7FFF;
      case UINT2:
        return value & 0xFFFF;
      case TINYINT:
        return value & 0x7F;
      case UINT1:
        return value & 0xFF;
      default:
        return value;
      }
    case LONG:
      return (long) value;
    case STRING:
      return Integer.toString(value);
    case DECIMAL:
      return BigDecimal.valueOf(value, dataType.getScale());
    case PERIOD:
      return periodFromInt(dataType.getMinorType(), value);
    case DATE:
      return DateUtilities.fromDrillDate(value);
    case TIME:
      return DateUtilities.fromDrillTime(value);
    case TIMESTAMP:
      return DateUtilities.fromDrillTimestamp(value);
    default:
      throw new IllegalStateException("Unknown writer type: " + valueType);
    }
  }

  /**
   * Ad-hoc, test-only method to set a Period from an integer. Periods are made up of
   * months and millseconds. There is no mapping from one to the other, so a period
   * requires at least two number. Still, we are given just one (typically from a test
   * data generator.) Use that int value to "spread" some value across the two kinds
   * of fields. The result has no meaning, but has the same comparison order as the
   * original ints.
   *
   * @param minorType the Drill data type
   * @param value the integer value to apply
   */
  public static Period periodFromInt(MinorType minorType, int value) {
    switch (minorType) {
    case INTERVAL:
      return Duration.millis(value).toPeriod();
    case INTERVALYEAR:
      return Period.years(value / 12).withMonths(value % 12);
    case INTERVALDAY:
      int sec = value % 60;
      value = value / 60;
      int min = value % 60;
      value = value / 60;
      return Period.days(value).withMinutes(min).withSeconds(sec);
    default:
      throw new IllegalArgumentException("Writer is not an interval: " + minorType);
    }
  }

  public static void assertEqualValues(ValueType type, Object expectedObj, Object actualObj) {
    assertEqualValues(type.toString(), type, expectedObj, actualObj);
  }

  public static void assertEqualValues(String msg, ValueType type, Object expectedObj, Object actualObj) {
    switch (type) {
    case BYTES: {
        byte[] expected = (byte[]) expectedObj;
        byte[] actual = (byte[]) actualObj;
        assertEquals(msg + " - byte lengths differ", expected.length, actual.length);
        assertTrue(msg, Arrays.equals(expected, actual));
        break;
     }
      case FLOAT:
       assertEquals(msg, (float) expectedObj, (float) actualObj, 0.0001);
       break;
     case DOUBLE:
       assertEquals(msg, (double) expectedObj, (double) actualObj, 0.0001);
       break;
     case INTEGER:
     case LONG:
     case STRING:
     case DECIMAL:
     case DATE:
     case TIME:
     case TIMESTAMP:
       assertEquals(msg, expectedObj, actualObj);
       break;
     case PERIOD: {
       Period expected = (Period) expectedObj;
       Period actual = (Period) actualObj;
       assertEquals(msg, expected.normalizedStandard(), actual.normalizedStandard());
       break;
     }
     default:
        throw new IllegalStateException( "Unexpected type: " + type);
    }
  }

  public static Object[] mapValue(Object... members) {
    return members;
  }

  public static Object[] singleMap(Object member) {
    return new Object[] { member };
  }

  public static byte[] byteArray(Integer... elements) {
    byte[] array = new byte[elements.length];
    for (int i = 0; i < elements.length; i++) {
      array[i] = (byte) (int) elements[i];
    }
    return array;
  }

  public static Long[] longArray(Long... elements) {
    return elements;
  }

  public static double[] doubleArray(Double... elements) {
    double[] array = new double[elements.length];
    for (int i = 0; i < elements.length; i++) {
      array[i] = elements[i];
    }
    return array;
  }

  public static float[] floatArray(Float... elements) {
    float[] array = new float[elements.length];
    for (int i = 0; i < elements.length; i++) {
      array[i] = elements[i];
    }
    return array;
  }

  public static boolean[] boolArray(Boolean... elements) {
    boolean[] array = new boolean[elements.length];
    for (int i = 0; i < elements.length; i++) {
      array[i] = elements[i];
    }
    return array;
  }

  public static String[] strArray(String... elements) {
    return elements;
  }

  public static BigDecimal[] decArray(BigDecimal... elements) {
    return elements;
  }

  public static byte[][] binArray(byte[]... elements) {
    return elements;
  }

  public static int[] intArray(Integer... elements) {
    int[] array = new int[elements.length];
    for (int i = 0; i < elements.length; i++) {
      array[i] = elements[i];
    }
    return array;
  }

  public static short[] shortArray(Short... elements) {
    short[] array = new short[elements.length];
    for (int i = 0; i < elements.length; i++) {
      array[i] = elements[i];
    }
    return array;
  }

  public static Object[][] mapArray(Object[]... elements) {
    return elements;
  }

  public static Object[] variantArray(Object... elements) {
    return elements;
  }

  public static Object[] listValue(Object... elements) {
    return elements;
  }

  public static Object[] singleList(Object element) {
    return new Object[] { element };
  }

  public static Object[] objArray(Object... elements) {
    return elements;
  }

  public static Object[] singleObjArray(Object element) {
    return new Object[] {element};
  }

  /**
   * Verify the actual results, then free memory
   * for both the expected and actual result sets.
   * @param expected The expected results.
   * @param actual the actual results to verify.
   */
  public static void verify(RowSet expected, RowSet actual) {
    new RowSetComparison(expected).verifyAndClearAll(actual);
  }

  public static void verify(RowSet expected, RowSet actual, int rowCount) {
    new RowSetComparison(expected).span(rowCount).verifyAndClearAll(actual);
  }

  public static BigDecimal dec(String value) {
    return new BigDecimal(value);
  }

  /**
   * Bootstrap a map object given key-value sequence.
   *
   * @param entry key-value sequence
   * @return map containing key-value pairs from passed sequence
   */
  public static Map<Object, Object> map(Object... entry) {
    assert entry.length % 2 == 0 : "Array length should be even.";

    // LinkedHashMap is chosen to preserve entry order
    Map<Object, Object> map = new LinkedHashMap<>();
    for (int i = 0; i < entry.length; i += 2) {
      map.put(entry[i], entry[i + 1]);
    }
    return map;
  }

  public static void assertSchemasEqual(TupleMetadata expected, BatchSchema actual) {
    assertTrue(expected.isEquivalent(MetadataUtils.fromFields(actual)));
  }
}
