/*
 * 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.kylin.invertedindex.model;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;

import org.apache.hadoop.hbase.io.ImmutableBytesWritable;
import org.apache.hadoop.hbase.util.Pair;

import com.google.common.collect.Lists;

import org.apache.kylin.common.util.BytesUtil;
import org.apache.kylin.invertedindex.index.*;

/**
 * @author yangli9
 */
public class IIKeyValueCodec {

	public static final int SHARD_LEN = 2;
	public static final int TIMEPART_LEN = 8;
	public static final int COLNO_LEN = 2;

	private TableRecordInfoDigest infoDigest;

	public IIKeyValueCodec(TableRecordInfoDigest digest) {
		this.infoDigest = digest;
	}

	public Collection<Pair<ImmutableBytesWritable, ImmutableBytesWritable>> encodeKeyValue(
			Slice slice) {
		ArrayList<Pair<ImmutableBytesWritable, ImmutableBytesWritable>> result = Lists
				.newArrayList();
		ColumnValueContainer[] containers = slice.getColumnValueContainers();
		for (int col = 0; col < containers.length; col++) {
			if (containers[col] instanceof BitMapContainer) {
				collectKeyValues(slice, col, (BitMapContainer) containers[col],
						result);
			} else if (containers[col] instanceof CompressedValueContainer) {
				collectKeyValues(slice, col,
						(CompressedValueContainer) containers[col], result);
			} else {
				throw new IllegalArgumentException("Unkown container class "
						+ containers[col].getClass());
			}
		}
		return result;
	}

	private void collectKeyValues(Slice slice,
			int col,
			CompressedValueContainer container, //
			ArrayList<Pair<ImmutableBytesWritable, ImmutableBytesWritable>> result) {
		ImmutableBytesWritable key = encodeKey(slice.getShard(),
				slice.getTimestamp(), col, -1);
		ImmutableBytesWritable value = container.toBytes();
		result.add(new Pair<ImmutableBytesWritable, ImmutableBytesWritable>(
				key, value));
	}

	private void collectKeyValues(Slice slice,
			int col,
			BitMapContainer container, //
			ArrayList<Pair<ImmutableBytesWritable, ImmutableBytesWritable>> result) {
		List<ImmutableBytesWritable> values = container.toBytes();
		for (int v = 0; v < values.size(); v++) {
			ImmutableBytesWritable key = encodeKey(slice.getShard(),
					slice.getTimestamp(), col, v);
			result.add(new Pair<ImmutableBytesWritable, ImmutableBytesWritable>(
					key, values.get(v)));
		}
	}

	ImmutableBytesWritable encodeKey(short shard, long timestamp, int col,
			int colValue) {
		byte[] bytes = new byte[20];
		int len = encodeKey(shard, timestamp, col, colValue, bytes, 0);
		return new ImmutableBytesWritable(bytes, 0, len);
	}

	int encodeKey(short shard, long timestamp, int col, int colValue,
			byte[] buf, int offset) {
		int i = offset;

		BytesUtil.writeUnsigned(shard, buf, i, SHARD_LEN);
		i += SHARD_LEN;
		BytesUtil.writeLong(timestamp, buf, i, TIMEPART_LEN);
		i += TIMEPART_LEN;

		BytesUtil.writeUnsigned(col, buf, i, COLNO_LEN);
		i += COLNO_LEN;

		if (colValue >= 0) {
			int colLen = infoDigest.length(col);
			BytesUtil.writeUnsigned(colValue, buf, i, colLen);
			i += colLen;
		}

		return i - offset;
	}

	public Iterable<Slice> decodeKeyValue(
			Iterable<Pair<ImmutableBytesWritable, ImmutableBytesWritable>> kvs) {
		return new Decoder(infoDigest, kvs);
	}

	private static class Decoder implements Iterable<Slice> {

		TableRecordInfoDigest info;
		Iterator<Pair<ImmutableBytesWritable, ImmutableBytesWritable>> iterator;

		Slice next = null;
		short curShard = Short.MIN_VALUE;
		long curSliceTimestamp = Long.MIN_VALUE;
		int curCol = -1;
		int curColValue = -1;
		short lastShard = Short.MIN_VALUE;
		long lastSliceTimestamp = Long.MIN_VALUE;
		int lastCol = -1;
		ColumnValueContainer[] containers = null;
		List<ImmutableBytesWritable> bitMapValues = Lists.newArrayList();

		Decoder(TableRecordInfoDigest info,
				Iterable<Pair<ImmutableBytesWritable, ImmutableBytesWritable>> kvs) {
			this.info = info;
			this.iterator = kvs.iterator();
		}

		private void goToNext() {
			if (next != null) { // was not fetched
				return;
			}

			// NOTE the input keys are ordered
			while (next == null && iterator.hasNext()) {
				Pair<ImmutableBytesWritable, ImmutableBytesWritable> kv = iterator
						.next();
				ImmutableBytesWritable k = kv.getFirst();
				ImmutableBytesWritable v = kv.getSecond();
				decodeKey(k);

				if (curShard != lastShard
						|| curSliceTimestamp != lastSliceTimestamp) {
					makeNext();
				}
				consumeCurrent(v);
			}
			if (next == null) {
				makeNext();
			}
		}

		private void decodeKey(ImmutableBytesWritable k) {
			byte[] buf = k.get();
			int i = k.getOffset();

			curShard = (short) BytesUtil.readUnsigned(buf, i, SHARD_LEN);
			i += SHARD_LEN;
			curSliceTimestamp = BytesUtil.readLong(buf, i, TIMEPART_LEN);
			i += TIMEPART_LEN;

			curCol = BytesUtil.readUnsigned(buf, i, COLNO_LEN);
			i += COLNO_LEN;

			if (i - k.getOffset() < k.getLength()) {
				// bitmap
				int colLen = info.length(curCol);
				curColValue = BytesUtil.readUnsigned(buf, i, colLen);
				i += colLen;
			} else {
				// value list
				curColValue = -1;
			}
		}

		private void consumeCurrent(ImmutableBytesWritable v) {
			if (curCol != lastCol && bitMapValues.size() > 0) { // end of a
																// bitmap
																// container
				addBitMapContainer(lastCol);
			}
			if (curColValue < 0) {
				CompressedValueContainer c = new CompressedValueContainer(info,
						curCol, 0);
				c.fromBytes(v);
				addContainer(curCol, c);
			} else {
				assert curColValue == bitMapValues.size();
				// make a copy, the value object from caller is typically reused
				// through iteration
				bitMapValues.add(new ImmutableBytesWritable(v));
			}

			lastShard = curShard;
			lastSliceTimestamp = curSliceTimestamp;
			lastCol = curCol;
		}

		private void makeNext() {
			if (bitMapValues.isEmpty() == false) {
				addBitMapContainer(lastCol);
			}
			if (containers != null) {
				next = new Slice(info, lastShard, lastSliceTimestamp,
						containers);
			}
			lastSliceTimestamp = Long.MIN_VALUE;
			lastCol = -1;
			containers = null;
			bitMapValues.clear();
		}

		private void addBitMapContainer(int col) {
			BitMapContainer c = new BitMapContainer(info, col);
			c.fromBytes(bitMapValues);
			addContainer(col, c);
			bitMapValues.clear();
		}

		private void addContainer(int col, ColumnValueContainer c) {
			if (containers == null) {
				containers = new ColumnValueContainer[info.getColumnCount()];
			}
			containers[col] = c;
		}

		@Override
		public Iterator<Slice> iterator() {
			return new Iterator<Slice>() {
				@Override
				public boolean hasNext() {
					goToNext();
					return next != null;
				}

				@Override
				public Slice next() {
					Slice result = next;
					next = null;
					return result;
				}

				@Override
				public void remove() {
					throw new UnsupportedOperationException();
				}
			};
		}

	}

}