package co.nstant.in.cbor.encoder;

import java.io.IOException;
import java.io.OutputStream;
import java.math.BigInteger;

import co.nstant.in.cbor.CborEncoder;
import co.nstant.in.cbor.CborException;
import co.nstant.in.cbor.model.AdditionalInformation;
import co.nstant.in.cbor.model.ByteString;
import co.nstant.in.cbor.model.MajorType;
import co.nstant.in.cbor.model.Tag;

public abstract class AbstractEncoder<T> {

    private final OutputStream outputStream;
    protected final CborEncoder encoder;

    public AbstractEncoder(CborEncoder encoder, OutputStream outputStream) {
        this.encoder = encoder;
        this.outputStream = outputStream;
    }

    public abstract void encode(T dataItem) throws CborException;

    protected void encodeTypeChunked(MajorType majorType) throws CborException {
        int symbol = majorType.getValue() << 5;
        symbol |= AdditionalInformation.INDEFINITE.getValue();
        try {
            outputStream.write(symbol);
        } catch (IOException ioException) {
            throw new CborException(ioException);
        }
    }

    protected void encodeTypeAndLength(MajorType majorType, long length) throws CborException {
        int symbol = majorType.getValue() << 5;
        if (length <= 23L) {
            write((byte) (symbol | length));
        } else if (length <= 255L) {
            symbol |= AdditionalInformation.ONE_BYTE.getValue();
            write((byte) symbol, (byte) length);
        } else if (length <= 65535L) {
            symbol |= AdditionalInformation.TWO_BYTES.getValue();
            write((byte) symbol, (byte) (length >> 8), (byte) (length & 0xFF));
        } else if (length <= 4294967295L) {
            symbol |= AdditionalInformation.FOUR_BYTES.getValue();
            write((byte) symbol, (byte) ((length >> 24) & 0xFF), (byte) ((length >> 16) & 0xFF),
                (byte) ((length >> 8) & 0xFF), (byte) (length & 0xFF));
        } else {
            symbol |= AdditionalInformation.EIGHT_BYTES.getValue();
            write((byte) symbol, (byte) ((length >> 56) & 0xFF), (byte) ((length >> 48) & 0xFF),
                (byte) ((length >> 40) & 0xFF), (byte) ((length >> 32) & 0xFF), (byte) ((length >> 24) & 0xFF),
                (byte) ((length >> 16) & 0xFF), (byte) ((length >> 8) & 0xFF), (byte) (length & 0xFF));
        }
    }

    protected void encodeTypeAndLength(MajorType majorType, BigInteger length) throws CborException {
        boolean negative = majorType == MajorType.NEGATIVE_INTEGER;
        int symbol = majorType.getValue() << 5;
        if (length.compareTo(BigInteger.valueOf(24)) == -1) {
            write(symbol | length.intValue());
        } else if (length.compareTo(BigInteger.valueOf(256)) == -1) {
            symbol |= AdditionalInformation.ONE_BYTE.getValue();
            write((byte) symbol, (byte) length.intValue());
        } else if (length.compareTo(BigInteger.valueOf(65536L)) == -1) {
            symbol |= AdditionalInformation.TWO_BYTES.getValue();
            long twoByteValue = length.longValue();
            write((byte) symbol, (byte) (twoByteValue >> 8), (byte) (twoByteValue & 0xFF));
        } else if (length.compareTo(BigInteger.valueOf(4294967296L)) == -1) {
            symbol |= AdditionalInformation.FOUR_BYTES.getValue();
            long fourByteValue = length.longValue();
            write((byte) symbol, (byte) ((fourByteValue >> 24) & 0xFF), (byte) ((fourByteValue >> 16) & 0xFF),
                (byte) ((fourByteValue >> 8) & 0xFF), (byte) (fourByteValue & 0xFF));
        } else if (length.compareTo(new BigInteger("18446744073709551616")) == -1) {
            symbol |= AdditionalInformation.EIGHT_BYTES.getValue();
            BigInteger mask = BigInteger.valueOf(0xFF);
            write((byte) symbol, length.shiftRight(56).and(mask).byteValue(),
                length.shiftRight(48).and(mask).byteValue(), length.shiftRight(40).and(mask).byteValue(),
                length.shiftRight(32).and(mask).byteValue(), length.shiftRight(24).and(mask).byteValue(),
                length.shiftRight(16).and(mask).byteValue(), length.shiftRight(8).and(mask).byteValue(),
                length.and(mask).byteValue());
        } else {
            if (negative) {
                encoder.encode(new Tag(3));
            } else {
                encoder.encode(new Tag(2));
            }
            encoder.encode(new ByteString(length.toByteArray()));
        }
    }

    protected void write(int b) throws CborException {
        try {
            outputStream.write(b);
        } catch (IOException ioException) {
            throw new CborException(ioException);
        }
    }

    protected void write(byte... bytes) throws CborException {
        try {
            outputStream.write(bytes);
        } catch (IOException ioException) {
            throw new CborException(ioException);
        }
    }

}