package org.infinispan.objectfilter.impl.aggregation;

import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;

/**
 * Groups rows by their grouping fields and computes aggregates.
 *
 * @author anistor@redhat.com
 * @since 8.0
 */
public final class RowGrouper {

   /**
    * The number of columns at the beginning of the row that are used for grouping.
    */
   private final int noOfGroupingColumns;

   private final FieldAccumulator[] accumulators;

   private final boolean twoPhaseAcc;

   private final int inRowLength;

   private final int outRowLength;

   /**
    * Store the a row for each group. This is used only if we have at least one grouping column.
    */
   private final LinkedHashMap<GroupRowKey, Object[]> groups;

   /**
    * The single row used for computing global aggregations (the are no grouping columns defined).
    */
   private final Object[] globalGroup;

   private final class GroupRowKey {

      private final Object[] row;

      GroupRowKey(Object[] row) {
         this.row = row;
      }

      @Override
      public boolean equals(Object o) {
         if (this == o) return true;
         if (o == null || getClass() != o.getClass()) return false;
         GroupRowKey other = (GroupRowKey) o;
         int n = noOfGroupingColumns > 0 ? noOfGroupingColumns : row.length;
         for (int i = 0; i < n; i++) {
            Object o1 = row[i];
            Object o2 = other.row[i];
            if (!(o1 == null ? o2 == null : o1.equals(o2))) {
               return false;
            }
         }
         return true;
      }

      @Override
      public int hashCode() {
         int result = 1;
         int n = noOfGroupingColumns > 0 ? noOfGroupingColumns : row.length;
         for (int i = 0; i < n; i++) {
            Object e = row[i];
            result = 31 * result + (e == null ? 0 : e.hashCode());
         }
         return result;
      }
   }

   /**
    * noOfGroupingColumns and accumulators must not have overlapping indices.
    */
   public RowGrouper(int noOfGroupingColumns, FieldAccumulator[] accumulators, boolean twoPhaseAcc) {
      this.noOfGroupingColumns = noOfGroupingColumns;
      this.accumulators = accumulators != null && accumulators.length != 0 ? accumulators : null;
      this.twoPhaseAcc = twoPhaseAcc;
      inRowLength = findInRowLength(noOfGroupingColumns, accumulators);
      if (inRowLength == 0) {
         throw new IllegalArgumentException("Must have at least one grouping or aggregated column");
      }
      outRowLength = noOfGroupingColumns + (accumulators != null ? accumulators.length : 0);
      if (noOfGroupingColumns > 0) {
         groups = new LinkedHashMap<>();
         globalGroup = null;
      } else {
         groups = null;
         // we have global aggregations only
         globalGroup = new Object[outRowLength];
         for (FieldAccumulator acc : accumulators) {
            acc.init(globalGroup);
         }
      }
   }

   private int findInRowLength(int noOfGroupingColumns, FieldAccumulator[] accumulators) {
      int l = noOfGroupingColumns;
      if (accumulators != null) {
         for (FieldAccumulator acc : accumulators) {
            if (acc.inPos + 1 > l) {
               l = acc.inPos + 1;
            }
         }
      }
      return l;
   }

   public void addRow(Object[] row) {
      if (row.length != inRowLength) {
         throw new IllegalArgumentException("Row length mismatch");
      }
      if (noOfGroupingColumns > 0) {
         // compute grouping and aggregations
         GroupRowKey groupRowKey = new GroupRowKey(row);
         Object[] existingGroup = groups.get(groupRowKey);
         if (existingGroup == null) {
            existingGroup = new Object[outRowLength];
            System.arraycopy(row, 0, existingGroup, 0, noOfGroupingColumns);
            if (accumulators != null) {
               FieldAccumulator.init(existingGroup, accumulators);
               for (FieldAccumulator acc : accumulators) {
                  acc.init(existingGroup);
               }
            }
            groups.put(groupRowKey, existingGroup);
         }
         if (accumulators != null) {
            if (twoPhaseAcc) {
               FieldAccumulator.merge(row, existingGroup, accumulators);
            } else {
               FieldAccumulator.update(row, existingGroup, accumulators);
            }
         }
      } else {
         // we have global aggregations only
         if (twoPhaseAcc) {
            FieldAccumulator.merge(row, globalGroup, accumulators);
         } else {
            FieldAccumulator.update(row, globalGroup, accumulators);
         }
      }
   }

   public Iterator<Object[]> finish() {
      if (groups != null) {
         return new Iterator<Object[]>() {

            private final Iterator<Object[]> iterator = groups.values().iterator();

            @Override
            public boolean hasNext() {
               return iterator.hasNext();
            }

            @Override
            public Object[] next() {
               Object[] row = iterator.next();
               if (accumulators != null) {
                  FieldAccumulator.finish(row, accumulators);
               }
               return row;
            }
         };
      } else {
         FieldAccumulator.finish(globalGroup, accumulators);
         return Collections.singleton(globalGroup).iterator();
      }
   }

   @Override
   public String toString() {
      return "RowGrouper{" +
            "noOfGroupingColumns=" + noOfGroupingColumns +
            ", inRowLength=" + inRowLength +
            ", outRowLength=" + outRowLength +
            ", accumulators=" + Arrays.toString(accumulators) +
            ", number of groups=" + groups.size() +
            '}';
   }
}
