/*
 * Licensed 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 com.facebook.presto.orc.stream;

import com.facebook.presto.orc.OrcCorruptionException;
import com.facebook.presto.orc.checkpoint.DecimalStreamCheckpoint;
import com.facebook.presto.spi.block.BlockBuilder;
import com.facebook.presto.spi.type.DecimalType;
import com.facebook.presto.spi.type.Decimals;
import com.facebook.presto.spi.type.UnscaledDecimal128Arithmetic;
import io.airlift.slice.Slice;

import java.io.IOException;

import static com.facebook.presto.spi.type.UnscaledDecimal128Arithmetic.rescale;
import static java.lang.Long.MAX_VALUE;

public class DecimalInputStream
        implements ValueInputStream<DecimalStreamCheckpoint>
{
    private final OrcInputStream input;

    public DecimalInputStream(OrcInputStream input)
    {
        this.input = input;
    }

    @Override
    public Class<? extends DecimalStreamCheckpoint> getCheckpointType()
    {
        return DecimalStreamCheckpoint.class;
    }

    @Override
    public void seekToCheckpoint(DecimalStreamCheckpoint checkpoint)
            throws IOException
    {
        input.seekToCheckpoint(checkpoint.getInputStreamCheckpoint());
    }

    public void nextLongDecimal(Slice result)
            throws IOException
    {
        long b;
        long offset = 0;
        long low = 0;
        long high = 0;
        do {
            b = input.read();
            if (offset == 126 && ((b & 0x80) > 0 || (b & 0x7f) > 3)) {
                throw new OrcCorruptionException(input.getOrcDataSourceId(), "Decimal exceeds 128 bits");
            }

            if (offset < 63) {
                low |= (b & 0x7f) << offset;
            }
            else if (offset == 63) {
                low |= (b & 0x01) << offset;
                high |= (b & 0x7f) >>> 1;
            }
            else {
                high |= (b & 0x7f) << (offset - 64);
            }
            offset += 7;
        }
        while ((b & 0x80) > 0);

        boolean negative = (low & 0x01) == 1;

        // drop sign bit
        low >>>= 1;
        low |= ((high & 0x1) << 63);
        high >>>= 1;

        // increment for negative values
        if (negative) {
            if (low == 0xFFFFFFFFFFFFFFFFL) {
                low = 0;
                high += 1;
            }
            else {
                low += 1;
            }
        }

        UnscaledDecimal128Arithmetic.pack(low, high, negative, result);
    }

    public void nextLongDecimalVector(int items, BlockBuilder builder, DecimalType targetType, long[] sourceScale)
            throws IOException
    {
        Slice decimal = UnscaledDecimal128Arithmetic.unscaledDecimal();
        Slice rescaledDecimal = UnscaledDecimal128Arithmetic.unscaledDecimal();
        for (int i = 0; i < items; i++) {
            nextLongDecimal(decimal);
            rescale(decimal, (int) (targetType.getScale() - sourceScale[i]), rescaledDecimal);
            targetType.writeSlice(builder, rescaledDecimal);
        }
    }

    public void nextLongDecimalVector(int items, BlockBuilder builder, DecimalType targetType, long[] sourceScale, boolean[] isNull)
            throws IOException
    {
        Slice decimal = UnscaledDecimal128Arithmetic.unscaledDecimal();
        Slice rescaledDecimal = UnscaledDecimal128Arithmetic.unscaledDecimal();
        for (int i = 0; i < items; i++) {
            if (!isNull[i]) {
                nextLongDecimal(decimal);
                rescale(decimal, (int) (targetType.getScale() - sourceScale[i]), rescaledDecimal);
                targetType.writeSlice(builder, rescaledDecimal);
            }
            else {
                builder.appendNull();
            }
        }
    }

    public long nextLong()
            throws IOException
    {
        long result = 0;
        int offset = 0;
        long b;
        do {
            b = input.read();
            if (b == -1) {
                throw new OrcCorruptionException(input.getOrcDataSourceId(), "Reading BigInteger past EOF");
            }
            long work = 0x7f & b;
            if (offset >= 63 && (offset != 63 || work > 1)) {
                throw new OrcCorruptionException(input.getOrcDataSourceId(), "Decimal does not fit long (invalid table schema?)");
            }
            result |= work << offset;
            offset += 7;
        }
        while (b >= 0x80);
        boolean isNegative = (result & 0x01) != 0;
        if (isNegative) {
            result += 1;
            result = -result;
            result = result >> 1;
            result |= 0x01L << 63;
        }
        else {
            result = result >> 1;
            result &= MAX_VALUE;
        }
        return result;
    }

    public void nextShortDecimalVector(int items, BlockBuilder builder, DecimalType targetType, long[] sourceScale)
            throws IOException
    {
        for (int i = 0; i < items; i++) {
            long value = nextLong();
            long rescaledDecimal = Decimals.rescale(value, (int) sourceScale[i], targetType.getScale());
            targetType.writeLong(builder, rescaledDecimal);
        }
    }

    public void nextShortDecimalVector(int items, BlockBuilder builder, DecimalType targetType, long[] sourceScale, boolean[] isNull)
            throws IOException
    {
        for (int i = 0; i < items; i++) {
            if (!isNull[i]) {
                long rescaledDecimal = Decimals.rescale(nextLong(), (int) sourceScale[i], targetType.getScale());
                targetType.writeLong(builder, rescaledDecimal);
            }
            else {
                builder.appendNull();
            }
        }
    }

    @Override
    public void skip(long items)
            throws IOException
    {
        while (items-- > 0) {
            int b;
            do {
                b = input.read();
                if (b == -1) {
                    throw new OrcCorruptionException(input.getOrcDataSourceId(), "Reading BigInteger past EOF");
                }
            }
            while (b >= 0x80);
        }
    }
}
