/*
 * 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
 *
 *     https://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.avro.generic;

import org.apache.avro.Schema;

import java.util.Arrays;
import java.util.Collection;

public class PrimitivesArrays {

  /**
   * Create a primitive array if the value type is has an associated optimised
   * implementation, otherwise a generic array is returned. The value type is
   * determined form the convertedElementType if supplied, otherwise the
   * underlying type from the schema
   *
   * @param size      the size of the array to create
   * @param schema    the schema of the array
   * @param valueType the converted elements value type. This may not be the same
   *                  and the schema if for instance there is a logical type, and
   *                  a convertor is use
   * @return an instance of a primitive array or a Generic array if the value type
   *         is does not have an associated optimised implementation.
   */
  public static GenericData.AbstractArray<?> createOptimizedArray(int size, Schema schema, Schema.Type valueType) {

    if (valueType != null)
      switch (valueType) {
      case INT:
        return new PrimitivesArrays.IntArray(size, schema);
      case BOOLEAN:
        return new PrimitivesArrays.BooleanArray(size, schema);
      case LONG:
        return new PrimitivesArrays.LongArray(size, schema);
      case FLOAT:
        return new PrimitivesArrays.FloatArray(size, schema);
      case DOUBLE:
        return new PrimitivesArrays.DoubleArray(size, schema);
      default:
        break;
      }
    return new GenericData.Array<>(size, schema);
  }

  public abstract static class PrimitiveArray<T> extends GenericData.AbstractArray<T> {
    PrimitiveArray(Schema schema) {
      super(schema);
    }

    public abstract Schema.Type valueType();
  }

  public static class IntArray extends PrimitiveArray<Integer> {
    private static final int[] EMPTY = new int[0];

    private int[] elements = EMPTY;

    public IntArray(int capacity, Schema schema) {
      super(schema);
      if (capacity != 0)
        elements = new int[capacity];
    }

    public IntArray(Schema schema, Collection<Integer> c) {
      super(schema);
      if (c != null) {
        elements = new int[c.size()];
        addAll(c);
      }
    }

    @Override
    public void clear() {
      size = 0;
    }

    @Override
    public Integer get(int i) {
      return this.getInt(i);
    }

    /**
     * Direct primitive int access.
     *
     * @param i : index.
     * @return value at index.
     */
    public int getInt(int i) {
      if (i >= size)
        throw new IndexOutOfBoundsException("Index " + i + " out of bounds.");
      return elements[i];
    }

    @Override
    public void add(int location, Integer o) {
      if (o == null) {
        return;
      }
      this.add(location, o.intValue());
    }

    public void add(int location, int o) {
      if (location > size || location < 0) {
        throw new IndexOutOfBoundsException("Index " + location + " out of bounds.");
      }
      if (size == elements.length) {
        // Increase size by 1.5x + 1
        final int newSize = size + (size >> 1) + 1;
        elements = Arrays.copyOf(elements, newSize);
      }
      System.arraycopy(elements, location, elements, location + 1, size - location);
      elements[location] = o;
      size++;
    }

    @Override
    public Integer set(int i, Integer o) {
      if (o == null) {
        return null;
      }
      return this.set(i, o.intValue());
    }

    public int set(int i, int o) {
      if (i >= size)
        throw new IndexOutOfBoundsException("Index " + i + " out of bounds.");
      int response = elements[i];
      elements[i] = o;
      return response;
    }

    @Override
    public Integer remove(int i) {
      if (i >= size)
        throw new IndexOutOfBoundsException("Index " + i + " out of bounds.");
      int result = elements[i];
      --size;
      System.arraycopy(elements, i + 1, elements, i, (size - i));
      return result;
    }

    @Override
    public Integer peek() {
      return (size < elements.length) ? elements[size] : null;
    }

    @Override
    protected void swap(final int index1, final int index2) {
      int tmp = elements[index1];
      elements[index1] = elements[index2];
      elements[index2] = tmp;
    }

    @Override
    public Schema.Type valueType() {
      return Schema.Type.INT;
    }
  }

  public static class LongArray extends PrimitiveArray<Long> {
    private static final long[] EMPTY = new long[0];

    private long[] elements = EMPTY;

    public LongArray(int capacity, Schema schema) {
      super(schema);
      if (capacity != 0)
        elements = new long[capacity];
    }

    public LongArray(Schema schema, Collection<Long> c) {
      super(schema);
      if (c != null) {
        elements = new long[c.size()];
        addAll(c);
      }
    }

    @Override
    public void clear() {
      size = 0;
    }

    @Override
    public Long get(int i) {
      return getLong(i);
    }

    /**
     * Direct primitive int access.
     *
     * @param i : index.
     * @return value at index.
     */
    public long getLong(int i) {
      if (i >= size)
        throw new IndexOutOfBoundsException("Index " + i + " out of bounds.");
      return elements[i];
    }

    @Override
    public void add(int location, Long o) {
      if (o == null) {
        return;
      }
      this.add(location, o.longValue());
    }

    public void add(int location, long o) {
      if (location > size || location < 0) {
        throw new IndexOutOfBoundsException("Index " + location + " out of bounds.");
      }
      if (size == elements.length) {
        // Increase size by 1.5x + 1
        final int newSize = size + (size >> 1) + 1;
        elements = Arrays.copyOf(elements, newSize);
      }
      System.arraycopy(elements, location, elements, location + 1, size - location);
      elements[location] = o;
      size++;
    }

    @Override
    public Long set(int i, Long o) {
      if (o == null) {
        return null;
      }
      return this.set(i, o.longValue());
    }

    public long set(int i, long o) {
      if (i >= size)
        throw new IndexOutOfBoundsException("Index " + i + " out of bounds.");
      long response = elements[i];
      elements[i] = o;
      return response;
    }

    @Override
    public Long remove(int i) {
      if (i >= size)
        throw new IndexOutOfBoundsException("Index " + i + " out of bounds.");
      long result = elements[i];
      --size;
      System.arraycopy(elements, i + 1, elements, i, (size - i));
      return result;
    }

    @Override
    public Long peek() {
      return (size < elements.length) ? elements[size] : null;
    }

    @Override
    protected void swap(final int index1, final int index2) {
      long tmp = elements[index1];
      elements[index1] = elements[index2];
      elements[index2] = tmp;
    }

    @Override
    public Schema.Type valueType() {
      return Schema.Type.LONG;
    }
  }

  public static class BooleanArray extends PrimitiveArray<Boolean> {
    private static final byte[] EMPTY = new byte[0];

    private byte[] elements = EMPTY;

    public BooleanArray(int capacity, Schema schema) {
      super(schema);
      if (capacity != 0)
        elements = new byte[1 + (capacity / Byte.SIZE)];
    }

    public BooleanArray(Schema schema, Collection<Boolean> c) {
      super(schema);

      if (c != null) {
        elements = new byte[1 + (c.size() / 8)];
        if (c instanceof BooleanArray) {
          BooleanArray other = (BooleanArray) c;
          this.size = other.size;
          System.arraycopy(other.elements, 0, this.elements, 0, other.elements.length);
        } else {
          addAll(c);
        }
      }
    }

    @Override
    public void clear() {
      size = 0;
    }

    @Override
    public Boolean get(int i) {
      return this.getBoolean(i);
    }

    /**
     * Direct primitive int access.
     *
     * @param i : index.
     * @return value at index.
     */
    public boolean getBoolean(int i) {
      if (i >= size)
        throw new IndexOutOfBoundsException("Index " + i + " out of bounds.");
      return (elements[i / 8] & (1 << (i % 8))) > 0;
    }

    @Override
    public boolean add(final Boolean o) {
      if (o == null) {
        return false;
      }
      return this.add(o.booleanValue());
    }

    public boolean add(final boolean o) {
      if (this.size == elements.length * 8) {
        final int newLength = elements.length + (elements.length >> 1) + 1;
        elements = Arrays.copyOf(elements, newLength);
      }
      this.size++;
      this.set(this.size - 1, o);
      return true;
    }

    @Override
    public void add(int location, Boolean o) {
      if (o == null) {
        return;
      }
      this.add(location, o.booleanValue());
    }

    public void add(int location, boolean o) {
      if (location > size || location < 0) {
        throw new IndexOutOfBoundsException("Index " + location + " out of bounds.");
      }
      if (size == elements.length * 8) {
        // Increase size by 1.5x + 1
        final int newLength = elements.length + (elements.length >> 1) + 1;
        elements = Arrays.copyOf(elements, newLength);
      }
      size++;
      for (int index = this.size / 8; index > (location / 8); index--) {
        elements[index] <<= 1;
        if ((elements[index - 1] & (1 << Byte.SIZE)) > 0) {
          elements[index] |= 1;
        }
      }
      byte pos = (byte) (1 << (location % Byte.SIZE));
      byte highbits = (byte) ~(pos + (pos - 1));
      byte lowbits = (byte) (pos - 1);
      byte currentHigh = (byte) ((elements[location / 8] & highbits) << 1);
      byte currentLow = (byte) (elements[location / 8] & lowbits);
      if (o) {
        elements[location / 8] = (byte) (currentHigh | currentLow | pos);
      } else {
        elements[location / 8] = (byte) (currentHigh | currentLow);
      }

    }

    @Override
    public Boolean set(int i, Boolean o) {
      if (o == null) {
        return null;
      }
      return this.set(i, o.booleanValue());
    }

    public boolean set(int i, boolean o) {
      if (i >= size)
        throw new IndexOutOfBoundsException("Index " + i + " out of bounds.");
      boolean response = (elements[i / 8] & (1 << (i % 8))) > 0;
      if (o) {
        elements[i / 8] |= 1 << (i % 8);
      } else {
        elements[i / 8] &= 0xFF - (1 << (i % 8));
      }
      return response;
    }

    @Override
    public Boolean remove(int i) {
      if (i >= size)
        throw new IndexOutOfBoundsException("Index " + i + " out of bounds.");
      boolean result = (elements[(i / 8)] & (1 << (i % 8))) > 0;
      --size;

      byte memo = 0;
      if ((i / 8) + 1 < elements.length) {
        memo = (byte) ((1 & (elements[(i / 8) + 1])) << 7);
      }
      for (int index = (i / 8) + 1; index <= (size / 8); index++) {
        elements[index] = (byte) ((elements[index] & 0xff) >>> 1);
        if (index + 1 < elements.length && (elements[index + 1] & 1) == 1) {
          elements[index] |= 1 << (Byte.SIZE - 1);
        }
      }
      // 87654321 => <memo>8764321
      byte start = (byte) ((1 << ((i + 1) % 8)) - 1);
      byte end = (byte) ~start;
      elements[i / 8] = (byte) ((((start & 0xff) >>> 1) & elements[i / 8]) // 1234
          | (end & (elements[i / 8] >> 1)) // 876
          | memo);

      return result;
    }

    @Override
    public Boolean peek() {
      return (size < elements.length * Byte.SIZE) ? (elements[(size / 8)] & (1 << (size % 8))) > 0 : null;
    }

    @Override
    protected void swap(final int index1, final int index2) {
      boolean tmp = this.get(index1);
      this.set(index1, this.get(index2));
      this.set(index2, tmp);
    }

    @Override
    public Schema.Type valueType() {
      return Schema.Type.BOOLEAN;
    }
  }

  public static class FloatArray extends PrimitiveArray<Float> {
    private static final float[] EMPTY = new float[0];

    private float[] elements = EMPTY;

    public FloatArray(int capacity, Schema schema) {
      super(schema);
      if (capacity != 0)
        elements = new float[capacity];
    }

    public FloatArray(Schema schema, Collection<Float> c) {
      super(schema);
      if (c != null) {
        elements = new float[c.size()];
        addAll(c);
      }
    }

    @Override
    public void clear() {
      size = 0;
    }

    @Override
    public Float get(int i) {
      return this.getFloat(i);
    }

    /**
     * Direct primitive int access.
     *
     * @param i : index.
     * @return value at index.
     */
    public float getFloat(int i) {
      if (i >= size)
        throw new IndexOutOfBoundsException("Index " + i + " out of bounds.");
      return elements[i];
    }

    @Override
    public void add(int location, Float o) {
      if (o == null) {
        return;
      }
      this.add(location, o.floatValue());
    }

    public void add(int location, float o) {
      if (location > size || location < 0) {
        throw new IndexOutOfBoundsException("Index " + location + " out of bounds.");
      }
      if (size == elements.length) {
        // Increase size by 1.5x + 1
        final int newSize = size + (size >> 1) + 1;
        elements = Arrays.copyOf(elements, newSize);
      }
      System.arraycopy(elements, location, elements, location + 1, size - location);
      elements[location] = o;
      size++;
    }

    @Override
    public Float set(int i, Float o) {
      if (o == null) {
        return null;
      }
      return this.set(i, o.floatValue());
    }

    public float set(int i, float o) {
      if (i >= size)
        throw new IndexOutOfBoundsException("Index " + i + " out of bounds.");
      float response = elements[i];
      elements[i] = o;
      return response;
    }

    @Override
    public Float remove(int i) {
      if (i >= size)
        throw new IndexOutOfBoundsException("Index " + i + " out of bounds.");
      float result = elements[i];
      --size;
      System.arraycopy(elements, i + 1, elements, i, (size - i));
      return result;
    }

    @Override
    public Float peek() {
      return (size < elements.length) ? elements[size] : null;
    }

    @Override
    protected void swap(final int index1, final int index2) {
      float tmp = this.get(index1);
      this.set(index1, this.get(index2));
      this.set(index2, tmp);
    }

    @Override
    public Schema.Type valueType() {
      return Schema.Type.FLOAT;
    }
  }

  public static class DoubleArray extends PrimitiveArray<Double> {
    private static final double[] EMPTY = new double[0];

    private double[] elements = EMPTY;

    public DoubleArray(int capacity, Schema schema) {
      super(schema);
      if (capacity != 0)
        elements = new double[capacity];
    }

    public DoubleArray(Schema schema, Collection<Double> c) {
      super(schema);
      if (c != null) {
        elements = new double[c.size()];
        addAll(c);
      }
    }

    @Override
    public void clear() {
      size = 0;
    }

    @Override
    public Double get(int i) {
      return this.getDouble(i);
    }

    /**
     * Direct primitive int access.
     *
     * @param i : index.
     * @return value at index.
     */
    public double getDouble(int i) {
      if (i >= size)
        throw new IndexOutOfBoundsException("Index " + i + " out of bounds.");
      return elements[i];
    }

    @Override
    public void add(int location, Double o) {
      if (o == null) {
        return;
      }
      this.add(location, o.doubleValue());
    }

    public void add(int location, double o) {
      if (location > size || location < 0) {
        throw new IndexOutOfBoundsException("Index " + location + " out of bounds.");
      }
      if (size == elements.length) {
        // Increase size by 1.5x + 1
        final int newSize = size + (size >> 1) + 1;
        elements = Arrays.copyOf(elements, newSize);
      }
      System.arraycopy(elements, location, elements, location + 1, size - location);
      elements[location] = o;
      size++;
    }

    @Override
    public Double set(int i, Double o) {
      if (o == null) {
        return null;
      }
      return this.set(i, o.doubleValue());
    }

    public double set(int i, double o) {
      if (i >= size)
        throw new IndexOutOfBoundsException("Index " + i + " out of bounds.");
      double response = elements[i];
      elements[i] = o;
      return response;
    }

    @Override
    public Double remove(int i) {
      if (i >= size)
        throw new IndexOutOfBoundsException("Index " + i + " out of bounds.");
      double result = elements[i];
      --size;
      System.arraycopy(elements, i + 1, elements, i, (size - i));
      return result;
    }

    @Override
    public Double peek() {
      return (size < elements.length) ? elements[size] : null;
    }

    @Override
    protected void swap(final int index1, final int index2) {
      double tmp = this.get(index1);
      this.set(index1, this.get(index2));
      this.set(index2, tmp);
    }

    @Override
    public Schema.Type valueType() {
      return Schema.Type.DOUBLE;
    }
  }

}
