package com.mp4parser.iso14496.part15;

import com.coremedia.iso.IsoTypeReader;
import com.coremedia.iso.IsoTypeWriter;

import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.ListIterator;

public class HevcDecoderConfigurationRecord {
    int configurationVersion;

    int general_profile_space;
    boolean general_tier_flag;
    int general_profile_idc;

    long general_profile_compatibility_flags;
    long general_constraint_indicator_flags;

    int general_level_idc;

    int reserved1 = 0xF;
    int min_spatial_segmentation_idc;

    int reserved2 = 0x3F;
    int parallelismType;

    int reserved3 = 0x3F;
    int chromaFormat;

    int reserved4 = 0x1F;
    int bitDepthLumaMinus8;

    int reserved5 = 0x1F;
    int bitDepthChromaMinus8;

    int avgFrameRate;

    int constantFrameRate;
    int numTemporalLayers;
    boolean temporalIdNested;
    int lengthSizeMinusOne;

    List<Array> arrays;


    public HevcDecoderConfigurationRecord() {
    }


    public void parse(ByteBuffer content) {
        configurationVersion = IsoTypeReader.readUInt8(content);

        /*
         * unsigned int(2) general_profile_space;
         * unsigned int(1) general_tier_flag;
         * unsigned int(5) general_profile_idc;
         */
        int a = IsoTypeReader.readUInt8(content);
        general_profile_space = (a & 0xC0) >> 6;
        general_tier_flag = (a & 0x20) > 0;
        general_profile_idc = (a & 0x1F);

        /* unsigned int(32) general_profile_compatibility_flags; */
        general_profile_compatibility_flags = IsoTypeReader.readUInt32(content);

         /* unsigned int(48) general_constraint_indicator_flags; */
        general_constraint_indicator_flags = IsoTypeReader.readUInt48(content);

        /* unsigned int(8) general_level_idc; */
        general_level_idc = IsoTypeReader.readUInt8(content);

        /*
         * bit(4) reserved = ‘1111’b;
         * unsigned int(12) min_spatial_segmentation_idc;
         */
        a = IsoTypeReader.readUInt16(content);
        reserved1 = (a & 0xF000) >> 12;
        min_spatial_segmentation_idc = a & 0xFFF;

        a = IsoTypeReader.readUInt8(content);
        reserved2 = (a & 0xFC) >> 2;
        parallelismType = a & 0x3;

        a = IsoTypeReader.readUInt8(content);
        reserved3 = (a & 0xFC) >> 2;
        chromaFormat = a & 0x3;

        a = IsoTypeReader.readUInt8(content);
        reserved4 = (a & 0xF8) >> 3;
        bitDepthLumaMinus8 = a & 0x7;

        a = IsoTypeReader.readUInt8(content);
        reserved5 = (a & 0xF8) >> 3;
        bitDepthChromaMinus8 = a & 0x7;

        avgFrameRate = IsoTypeReader.readUInt16(content);

        a = IsoTypeReader.readUInt8(content);
        constantFrameRate = (a & 0xC0) >> 6;
        numTemporalLayers = (a & 0x38) >> 3;
        temporalIdNested = (a & 0x4) > 0;
        lengthSizeMinusOne = a & 0x3;


        int numOfArrays = IsoTypeReader.readUInt8(content);
        this.arrays = new ArrayList<Array>();
        for (int i = 0; i < numOfArrays; i++) {
            Array array = new Array();

            a = IsoTypeReader.readUInt8(content);
            array.array_completeness = (a & 0x80) > 0;
            array.reserved = (a & 0x40) > 0;
            array.nal_unit_type = a & 0x3F;

            int numNalus = IsoTypeReader.readUInt16(content);
            array.nalUnits = new ArrayList<byte[]>();
            for (int j = 0; j < numNalus; j++) {
                int nalUnitLength = IsoTypeReader.readUInt16(content);
                byte[] nal = new byte[nalUnitLength];
                content.get(nal);
                array.nalUnits.add(nal);
            }
            arrays.add(array);
        }
    }

    public void write(ByteBuffer byteBuffer) {
        IsoTypeWriter.writeUInt8(byteBuffer, configurationVersion);


        IsoTypeWriter.writeUInt8(byteBuffer, (general_profile_space << 6) + (general_tier_flag ? 0x20 : 0) + general_profile_idc);

        IsoTypeWriter.writeUInt32(byteBuffer, general_profile_compatibility_flags);
        IsoTypeWriter.writeUInt48(byteBuffer, general_constraint_indicator_flags);


        IsoTypeWriter.writeUInt8(byteBuffer, general_level_idc);

        IsoTypeWriter.writeUInt16(byteBuffer, (reserved1 << 12) + min_spatial_segmentation_idc);

        IsoTypeWriter.writeUInt8(byteBuffer, (reserved2 << 2) + parallelismType);

        IsoTypeWriter.writeUInt8(byteBuffer, (reserved3 << 2) + chromaFormat);

        IsoTypeWriter.writeUInt8(byteBuffer, (reserved4 << 3) + bitDepthLumaMinus8);

        IsoTypeWriter.writeUInt8(byteBuffer, (reserved5 << 3) + bitDepthChromaMinus8);

        IsoTypeWriter.writeUInt16(byteBuffer, avgFrameRate);

        IsoTypeWriter.writeUInt8(byteBuffer, (constantFrameRate << 6) + (numTemporalLayers << 3) + (temporalIdNested ? 0x4 : 0) + lengthSizeMinusOne);

        IsoTypeWriter.writeUInt8(byteBuffer, arrays.size());

        for (Array array : arrays) {
            IsoTypeWriter.writeUInt8(byteBuffer, (array.array_completeness ? 0x80 : 0) + (array.reserved ? 0x40 : 0) + array.nal_unit_type);

            IsoTypeWriter.writeUInt16(byteBuffer, array.nalUnits.size());
            for (byte[] nalUnit : array.nalUnits) {
                IsoTypeWriter.writeUInt16(byteBuffer, nalUnit.length);
                byteBuffer.put(nalUnit);
            }
        }
    }

    public int getSize() {
        int size = 23;
        for (Array array : arrays) {
            size += 3;
            for (byte[] nalUnit : array.nalUnits) {
                size += 2;
                size += nalUnit.length;
            }
        }
        return size;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        HevcDecoderConfigurationRecord that = (HevcDecoderConfigurationRecord) o;

        if (avgFrameRate != that.avgFrameRate) return false;
        if (bitDepthChromaMinus8 != that.bitDepthChromaMinus8) return false;
        if (bitDepthLumaMinus8 != that.bitDepthLumaMinus8) return false;
        if (chromaFormat != that.chromaFormat) return false;
        if (configurationVersion != that.configurationVersion) return false;
        if (constantFrameRate != that.constantFrameRate) return false;
        if (general_constraint_indicator_flags != that.general_constraint_indicator_flags) return false;
        if (general_level_idc != that.general_level_idc) return false;
        if (general_profile_compatibility_flags != that.general_profile_compatibility_flags) return false;
        if (general_profile_idc != that.general_profile_idc) return false;
        if (general_profile_space != that.general_profile_space) return false;
        if (general_tier_flag != that.general_tier_flag) return false;
        if (lengthSizeMinusOne != that.lengthSizeMinusOne) return false;
        if (min_spatial_segmentation_idc != that.min_spatial_segmentation_idc) return false;
        if (numTemporalLayers != that.numTemporalLayers) return false;
        if (parallelismType != that.parallelismType) return false;
        if (reserved1 != that.reserved1) return false;
        if (reserved2 != that.reserved2) return false;
        if (reserved3 != that.reserved3) return false;
        if (reserved4 != that.reserved4) return false;
        if (reserved5 != that.reserved5) return false;
        if (temporalIdNested != that.temporalIdNested) return false;
        if (arrays != null ? !arrays.equals(that.arrays) : that.arrays != null) return false;

        return true;
    }

    @Override
    public int hashCode() {
        int result = configurationVersion;
        result = 31 * result + general_profile_space;
        result = 31 * result + (general_tier_flag ? 1 : 0);
        result = 31 * result + general_profile_idc;
        result = 31 * result + (int) (general_profile_compatibility_flags ^ (general_profile_compatibility_flags >>> 32));
        result = 31 * result + (int) (general_constraint_indicator_flags ^ (general_constraint_indicator_flags >>> 32));
        result = 31 * result + general_level_idc;
        result = 31 * result + reserved1;
        result = 31 * result + min_spatial_segmentation_idc;
        result = 31 * result + reserved2;
        result = 31 * result + parallelismType;
        result = 31 * result + reserved3;
        result = 31 * result + chromaFormat;
        result = 31 * result + reserved4;
        result = 31 * result + bitDepthLumaMinus8;
        result = 31 * result + reserved5;
        result = 31 * result + bitDepthChromaMinus8;
        result = 31 * result + avgFrameRate;
        result = 31 * result + constantFrameRate;
        result = 31 * result + numTemporalLayers;
        result = 31 * result + (temporalIdNested ? 1 : 0);
        result = 31 * result + lengthSizeMinusOne;
        result = 31 * result + (arrays != null ? arrays.hashCode() : 0);
        return result;
    }

    @Override
    public String toString() {
        return "HEVCDecoderConfigurationRecord{" +
                "configurationVersion=" + configurationVersion +
                ", general_profile_space=" + general_profile_space +
                ", general_tier_flag=" + general_tier_flag +
                ", general_profile_idc=" + general_profile_idc +
                ", general_profile_compatibility_flags=" + general_profile_compatibility_flags +
                ", general_constraint_indicator_flags=" + general_constraint_indicator_flags +
                ", general_level_idc=" + general_level_idc +
                (reserved1 != 0xf ? (", reserved1=" + reserved1) : "") +
                ", min_spatial_segmentation_idc=" + min_spatial_segmentation_idc +
                (reserved2 != 0x3F ? (", reserved2=" + reserved2) : "") +
                ", parallelismType=" + parallelismType +
                (reserved3 != 0x3F ? (", reserved3=" + reserved3) : "") +
                ", chromaFormat=" + chromaFormat +
                (reserved4 != 0x1F ? (", reserved4=" + reserved4) : "") +
                ", bitDepthLumaMinus8=" + bitDepthLumaMinus8 +
                (reserved5 != 0x1F ? (", reserved5=" + reserved5) : "") +
                ", bitDepthChromaMinus8=" + bitDepthChromaMinus8 +
                ", avgFrameRate=" + avgFrameRate +
                ", constantFrameRate=" + constantFrameRate +
                ", numTemporalLayers=" + numTemporalLayers +
                ", temporalIdNested=" + temporalIdNested +
                ", lengthSizeMinusOne=" + lengthSizeMinusOne +
                ", arrays=" + arrays +
                '}';
    }

    public int getConfigurationVersion() {
        return configurationVersion;
    }

    public void setConfigurationVersion(int configurationVersion) {
        this.configurationVersion = configurationVersion;
    }

    public int getGeneral_profile_space() {
        return general_profile_space;
    }

    public void setGeneral_profile_space(int general_profile_space) {
        this.general_profile_space = general_profile_space;
    }

    public boolean isGeneral_tier_flag() {
        return general_tier_flag;
    }

    public void setGeneral_tier_flag(boolean general_tier_flag) {
        this.general_tier_flag = general_tier_flag;
    }

    public int getGeneral_profile_idc() {
        return general_profile_idc;
    }

    public void setGeneral_profile_idc(int general_profile_idc) {
        this.general_profile_idc = general_profile_idc;
    }

    public long getGeneral_profile_compatibility_flags() {
        return general_profile_compatibility_flags;
    }

    public void setGeneral_profile_compatibility_flags(long general_profile_compatibility_flags) {
        this.general_profile_compatibility_flags = general_profile_compatibility_flags;
    }

    public long getGeneral_constraint_indicator_flags() {
        return general_constraint_indicator_flags;
    }

    public void setGeneral_constraint_indicator_flags(long general_constraint_indicator_flags) {
        this.general_constraint_indicator_flags = general_constraint_indicator_flags;
    }

    public int getGeneral_level_idc() {
        return general_level_idc;
    }

    public void setGeneral_level_idc(int general_level_idc) {
        this.general_level_idc = general_level_idc;
    }

    public int getMin_spatial_segmentation_idc() {
        return min_spatial_segmentation_idc;
    }

    public void setMin_spatial_segmentation_idc(int min_spatial_segmentation_idc) {
        this.min_spatial_segmentation_idc = min_spatial_segmentation_idc;
    }

    public int getParallelismType() {
        return parallelismType;
    }

    public void setParallelismType(int parallelismType) {
        this.parallelismType = parallelismType;
    }

    public int getChromaFormat() {
        return chromaFormat;
    }

    public void setChromaFormat(int chromaFormat) {
        this.chromaFormat = chromaFormat;
    }

    public int getBitDepthLumaMinus8() {
        return bitDepthLumaMinus8;
    }

    public void setBitDepthLumaMinus8(int bitDepthLumaMinus8) {
        this.bitDepthLumaMinus8 = bitDepthLumaMinus8;
    }

    public int getBitDepthChromaMinus8() {
        return bitDepthChromaMinus8;
    }

    public void setBitDepthChromaMinus8(int bitDepthChromaMinus8) {
        this.bitDepthChromaMinus8 = bitDepthChromaMinus8;
    }

    public int getAvgFrameRate() {
        return avgFrameRate;
    }

    public void setAvgFrameRate(int avgFrameRate) {
        this.avgFrameRate = avgFrameRate;
    }

    public int getNumTemporalLayers() {
        return numTemporalLayers;
    }

    public void setNumTemporalLayers(int numTemporalLayers) {
        this.numTemporalLayers = numTemporalLayers;
    }

    public int getLengthSizeMinusOne() {
        return lengthSizeMinusOne;
    }

    public void setLengthSizeMinusOne(int lengthSizeMinusOne) {
        this.lengthSizeMinusOne = lengthSizeMinusOne;
    }

    public boolean isTemporalIdNested() {
        return temporalIdNested;
    }

    public void setTemporalIdNested(boolean temporalIdNested) {
        this.temporalIdNested = temporalIdNested;
    }

    public int getConstantFrameRate() {
        return constantFrameRate;
    }

    public void setConstantFrameRate(int constantFrameRate) {
        this.constantFrameRate = constantFrameRate;
    }

    public List<Array> getArrays() {
        return arrays;
    }

    public void setArrays(List<Array> arrays) {
        this.arrays = arrays;
    }

    public static class Array {

        public boolean array_completeness;
        public boolean reserved;
        public int nal_unit_type;
        public List<byte[]> nalUnits;


        public Array() {

        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;

            Array array = (Array) o;

            if (array_completeness != array.array_completeness) return false;
            if (nal_unit_type != array.nal_unit_type) return false;
            if (reserved != array.reserved) return false;
            ListIterator<byte[]> e1 = nalUnits.listIterator();
            ListIterator<byte[]> e2 = (array.nalUnits).listIterator();
            while (e1.hasNext() && e2.hasNext()) {
                byte[] o1 = e1.next();
                byte[] o2 = e2.next();

                if (!(o1 == null ? o2 == null : Arrays.equals(o1, o2)))
                    return false;
            }
            return !(e1.hasNext() || e2.hasNext());
        }

        @Override
        public int hashCode() {
            int result = (array_completeness ? 1 : 0);
            result = 31 * result + (reserved ? 1 : 0);
            result = 31 * result + nal_unit_type;
            result = 31 * result + (nalUnits != null ? nalUnits.hashCode() : 0);
            return result;
        }

        @Override
        public String toString() {
            return "Array{" +
                    "nal_unit_type=" + nal_unit_type +
                    ", reserved=" + reserved +
                    ", array_completeness=" + array_completeness +
                    ", num_nals=" + nalUnits.size() +
                    '}';
        }
    }
}
