/*
 * 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.phoenix.filter;

import java.io.IOException;

import org.apache.hadoop.hbase.Cell;
import org.apache.hadoop.hbase.KeyValue;
import org.apache.hadoop.hbase.filter.Filter;
import org.apache.hadoop.hbase.filter.Filter.ReturnCode;
import org.apache.hadoop.hbase.util.Bytes;
import org.apache.phoenix.schema.PDatum;
import org.apache.phoenix.schema.RowKeySchema.RowKeySchemaBuilder;
import org.apache.phoenix.schema.SortOrder;
import org.apache.phoenix.schema.types.PChar;
import org.apache.phoenix.schema.types.PDataType;
import org.apache.phoenix.schema.types.PVarchar;
import org.apache.phoenix.util.ByteUtil;

import junit.framework.TestCase;

public class DistinctPrefixFilterTest extends TestCase {
    private DistinctPrefixFilter createFilter(int[] widths, int prefixLength) {
        RowKeySchemaBuilder builder = new RowKeySchemaBuilder(widths.length);
        for (final int width : widths) {
            builder.addField(
                new PDatum() {

                @Override
                public boolean isNullable() {
                    return width <= 0;
                }

                @Override
                public PDataType<?> getDataType() {
                    return width <= 0 ? PVarchar.INSTANCE : PChar.INSTANCE;
                }

                @Override
                public Integer getMaxLength() {
                    return width <= 0 ? null : width;
                }

                @Override
                public Integer getScale() {
                    return null;
                }

                @Override
                public SortOrder getSortOrder() {
                    return SortOrder.getDefault();
                }

            }, width <= 0, SortOrder.getDefault());
        }
        return new DistinctPrefixFilter(builder.build(), prefixLength);
    }

    private void assertInclude(String next, Filter f) throws IOException {
        assertInclude(Bytes.toBytes(next), f);
    }

    private void assertInclude(byte[] next, Filter f) throws IOException {
        Cell c = new KeyValue(next, ByteUtil.EMPTY_BYTE_ARRAY, ByteUtil.EMPTY_BYTE_ARRAY, 0, ByteUtil.EMPTY_BYTE_ARRAY);
        assertTrue(f.filterKeyValue(c) == ReturnCode.INCLUDE);
        assertFalse(f.filterAllRemaining());
    }

    private void assertSeekAndHint(String next, Filter f, String rowHint) throws IOException {
        assertSeekAndHint(next, f, rowHint, false);
    }

    private void assertSeekAndHint(String next, Filter f, String rowHint, boolean filterAll) throws IOException {
        assertSeekAndHint(Bytes.toBytes(next), f, Bytes.toBytes(rowHint), filterAll);
    }

    private void assertSeekAndHint(byte[] next, Filter f, byte[] rowHint, boolean filterAll) throws IOException {
        Cell c = new KeyValue(next, ByteUtil.EMPTY_BYTE_ARRAY, ByteUtil.EMPTY_BYTE_ARRAY, 0, ByteUtil.EMPTY_BYTE_ARRAY);
        assertTrue(f.filterKeyValue(c) == ReturnCode.SEEK_NEXT_USING_HINT);
        Cell h = f.getNextCellHint(c);
        byte[] hintBytes = rowHint;
        assertTrue(Bytes.equals(hintBytes, 0, hintBytes.length, h.getRowArray(), h.getRowOffset(), h.getRowLength()));
        assertEquals(filterAll, f.filterAllRemaining());
    }

    public void testSingleFixedWidth() throws Exception {
        Filter f = createFilter(new int[]{3}, 1);
        assertInclude("000", f);
        assertInclude("001", f);
        assertSeekAndHint("001", f, "002");
        assertInclude("003", f);
        assertInclude("004", f);
        assertInclude("005", f);
        assertSeekAndHint("005", f, "006");

        f = createFilter(new int[]{3}, 1);
        f.setReversed(true);
        assertInclude("005", f);
        assertInclude("004", f);
        assertSeekAndHint(new byte[]{'0','0','4'}, f, new byte[]{'0','0','4'}, false);
        assertInclude("003", f);
        assertInclude("002", f);
        assertInclude("001", f);
        assertSeekAndHint(new byte[]{'0','0','1'}, f, new byte[]{'0','0','1'}, false);
    }

    public void testMultiFixedWidth() throws Exception {
        Filter f = createFilter(new int[]{5,4}, 1);
        assertInclude("00000aaaa", f);
        assertInclude("00001aaaa", f);
        assertSeekAndHint("00001aaaa", f, "00002");
        assertInclude("00003aaaa", f);
        assertInclude("00004aaaa", f);
        assertInclude("00005aaaa", f);
        assertSeekAndHint("00005aaaa", f, "00006");

        f = createFilter(new int[]{5,4}, 2);
        assertInclude("00000aaaa", f);
        assertInclude("00001aaaa", f);
        assertSeekAndHint("00001aaaa", f, "00001aaab");
        assertInclude("00003aaaa", f);
        assertInclude("00004aaaa", f);
        assertInclude("00005aaaa", f);
        assertSeekAndHint("00005aaaa", f, "00005aaab");

        f = createFilter(new int[]{3,2}, 1);
        f.setReversed(true);
        assertInclude("005aa", f);
        assertInclude("004aa", f);
        assertSeekAndHint(new byte[]{'0','0','4','a','a'}, f, new byte[]{'0','0','4'}, false);
        assertInclude("003aa", f);
        assertInclude("002aa", f);
        assertInclude("001aa", f);
        assertSeekAndHint(new byte[]{'0','0','1','a','a'}, f, new byte[]{'0','0','1'}, false);

        f = createFilter(new int[]{3,2}, 2);
        f.setReversed(true);
        assertInclude("005bb", f);
        assertInclude("004bb", f);
        assertInclude("003bb", f);
        assertSeekAndHint(new byte[]{'0','0','3','b','b'}, f, new byte[]{'0','0','3','b','b'}, false);
        assertInclude("003ba", f);
        assertInclude("002bb", f);
        assertInclude("001bb", f);
        assertSeekAndHint(new byte[]{'0','0','1','b','b'}, f, new byte[]{'0','0','1','b','b'}, false);
    }

    public void testSingleVariableWidth() throws Exception {
        Filter f = createFilter(new int[]{-5}, 1);
        assertInclude("00000", f);
        assertInclude("00001", f);
        assertSeekAndHint("00001", f, "00001\01");
        assertInclude("00003", f);
        assertInclude("00004", f);
        assertInclude("00005", f);
        assertSeekAndHint("00005", f, "00005\01");
    }

    public void testVariableWithNull() throws Exception {
        Filter f = createFilter(new int[]{-2,-2}, 1);
        assertInclude("\00aa", f);
        assertSeekAndHint("\00aa", f, "\01");
        assertSeekAndHint("\00aa", f, "\01");

        f = createFilter(new int[]{-2,-2}, 2);
        assertInclude("\00\00", f);
        assertSeekAndHint("\00\00", f, "\00\00\01");
        assertSeekAndHint("\00\00", f, "\00\00\01");
    }

    public void testMultiVariableWidth() throws Exception {
        Filter f = createFilter(new int[]{-5,-4}, 1);
        assertInclude("00000\00aaaa", f);
        assertInclude("00001\00aaaa", f);
        assertSeekAndHint("00001\00aaaa", f, "00001\01");
        assertInclude("00003\00aaaa", f);
        assertInclude("00004\00aaaa", f);
        assertInclude("00005\00aaaa", f);
        assertSeekAndHint("00005\00aaaa", f, "00005\01");

        f = createFilter(new int[]{-5,-4}, 2);
        assertInclude("00000\00aaaa", f);
        assertInclude("00001\00aaaa", f);
        assertSeekAndHint("00001\00aaaa", f, "00001\00aaaa\01");
        assertInclude("00003\00aaaa", f);
        assertInclude("00004\00aaaa", f);
        assertInclude("00005\00aaaa", f);
        assertSeekAndHint("00005\00aaaa", f, "00005\00aaaa\01");

        f = createFilter(new int[]{-3,-2}, 1);
        f.setReversed(true);
        assertInclude("005\00aa", f);
        assertInclude("004\00aa", f);
        assertSeekAndHint(new byte[]{'0','0','4', 0, 'a', 'a'}, f,
                new byte[] {'0','0','4'}, false);

        f = createFilter(new int[]{-3,-2}, 2);
        f.setReversed(true);
        assertInclude("005\00bb", f);
        assertInclude("004\00bb", f);
        assertSeekAndHint(new byte[]{'0','0','4', 0, 'b', 'b'}, f,
                new byte[]{'0','0','4', 0, 'b', 'b'}, false); 
    }

    public void testFixedAfterVariable() throws Exception {
        Filter f = createFilter(new int[]{-5,4}, 1);
        assertInclude("00000\00aaaa", f);
        assertInclude("00001\00aaaa", f);
        assertSeekAndHint("00001\00aaaa", f, "00001\01");
        assertInclude("00003\00aaaa", f);
        assertInclude("00004\00aaaa", f);
        assertInclude("00005\00aaaa", f);
        assertSeekAndHint("00005\00aaaa", f, "00005\01");

        f = createFilter(new int[]{-5,4}, 2);
        assertInclude("00000\00aaaa", f);
        assertInclude("00001\00aaaa", f);
        assertSeekAndHint("00001\00aaaa", f, "00001\00aaab");
        assertInclude("00003\00aaaa", f);
        assertInclude("00004\00aaaa", f);
        assertInclude("00005\00aaaa", f);
        assertSeekAndHint("00005\00aaaa", f, "00005\00aaab");
    }

    public void testVariableAfterFixed() throws Exception {
        Filter f = createFilter(new int[]{5,-4}, 1);
        assertInclude("00000aaaa", f);
        assertInclude("00001aaaa", f);
        assertSeekAndHint("00001aaaa", f, "00002");
        assertInclude("00003aaaa", f);
        assertInclude("00004aaaa", f);
        assertInclude("00005aaaa", f);
        assertSeekAndHint("00005aaaa", f, "00006");

        f = createFilter(new int[]{5,-4}, 2);
        assertInclude("00000aaaa", f);
        assertInclude("00001aaaa", f);
        assertSeekAndHint("00001aaaa", f, "00001aaaa\01");
        assertInclude("00003aaaa", f);
        assertInclude("00004aaaa", f);
        assertInclude("00005aaaa", f);
        assertSeekAndHint("00005aaaa", f, "00005aaaa\01");
    }

    public void testNoNextKey() throws Exception {
        Filter f = createFilter(new int[]{2,2}, 1);
        assertInclude("00cc", f);
        assertInclude(new byte[]{-1,-1,20,20}, f);
        // make sure we end the scan when we cannot increase a fixed length prefix
        assertSeekAndHint(new byte[]{-1,-1,20,20}, f, new byte[]{-1,-1}, true);
        assertSeekAndHint(new byte[]{-1,-1,20,20}, f, new byte[]{-1,-1}, true);

        f = createFilter(new int[]{2,2}, 1);
        f.setReversed(true);
        assertInclude(new byte[]{0,0,1,1}, f);
        assertSeekAndHint(new byte[]{0,0,1,1}, f, new byte[]{0,0}, false);
        assertSeekAndHint(new byte[]{0,0,1,1}, f, new byte[]{0,0}, false);
    }
}