/*  Copyright (C) 2022-2024 José Rebelo

    This file is part of Gadgetbridge.

    Gadgetbridge is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published
    by the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    Gadgetbridge is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program.  If not, see <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.huami;

import androidx.annotation.Nullable;

import org.apache.commons.lang3.ArrayUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.List;

import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
import nodomain.freeyourgadget.gadgetbridge.util.CheckSums;

public class UIHHContainer {
    private static final Logger LOG = LoggerFactory.getLogger(UIHHContainer.class);

    public static final byte[] UIHH_HEADER = new byte[]{
            'U', 'I', 'H', 'H'
    };

    public List<FileEntry> files = new ArrayList<>();

    public List<FileEntry> getFiles() {
        return files;
    }

    public byte[] getFile(final FileType fileType) {
        for (final FileEntry file : files) {
            if (file.getType() == fileType) {
                return file.getContent();
            }
        }

        return null;
    }

    public void addFile(final FileType type, final byte[] bytes) {
        files.add(new FileEntry(type, bytes));
    }

    @Nullable
    public byte[] toRawBytes() {
        final ByteArrayOutputStream baos = new ByteArrayOutputStream();

        for (final FileEntry file : files) {
            try {
                baos.write(file.buildHeader());
                baos.write(file.getContent());
            } catch (final IOException e) {
                LOG.error("Failed to generate UIHH bytes", e);
                return null;
            }
        }

        final byte[] contentBytes = baos.toByteArray();
        final byte[] headerBytes = buildHeader(contentBytes);

        return ArrayUtils.addAll(headerBytes, contentBytes);
    }

    @Nullable
    public static UIHHContainer fromRawBytes(final byte[] bytes) {
        if (bytes.length < 32) {
            LOG.error("bytes array too small {}", bytes.length);
            return null;
        }

        if (!nodomain.freeyourgadget.gadgetbridge.util.ArrayUtils.startsWith(bytes, UIHH_HEADER)) {
            LOG.error("UIHH header not found");
            return null;
        }

        final int crc32 = BLETypeConversions.toUint32(ArrayUtils.subarray(bytes, 12, 12 + 4));
        final int length = BLETypeConversions.toUint32(ArrayUtils.subarray(bytes, 22, 22 + 4));

        if (length + 32 != bytes.length) {
            LOG.error("Length mismatch between header and bytes: {}/{}", length, bytes.length);
            return null;
        }

        if (crc32 != CheckSums.getCRC32(ArrayUtils.subarray(bytes, 32, bytes.length))) {
            LOG.error("CRC mismatch for content");
            return null;
        }

        int i = 32;

        final UIHHContainer ret = new UIHHContainer();

        while (i < bytes.length) {
            if (i + 10 >= bytes.length) {
                LOG.error("Not enough bytes remaining");
                return null;
            }

            if (bytes[i] != 1) {
                LOG.error("Expected 1 at position {}", i);
                return null;
            }
            i++;

            final FileType type = FileType.fromValue(bytes[i]);
            if (type == null) {
                LOG.error("Unknown type byte {} at position {}", String.format("0x%x", bytes[i], i));
                return null;
            }
            i++;

            final int fileLength = BLETypeConversions.toUint32(ArrayUtils.subarray(bytes, i, i + 4));
            i += 4;
            final int fileCrc32 = BLETypeConversions.toUint32(ArrayUtils.subarray(bytes, i, i + 4));
            i += 4;

            if (i + fileLength > bytes.length) {
                LOG.error("Not enough bytes remaining to read a {} byte file", fileLength);
                return null;
            }

            final byte[] fileContent = ArrayUtils.subarray(bytes, i, i + fileLength);
            if (fileCrc32 != CheckSums.getCRC32(fileContent)) {
                LOG.error("CRC mismatch for {}", type);
                return null;
            }
            i += fileLength;

            ret.getFiles().add(new FileEntry(type, fileContent));
        }

        return ret;
    }

    private static byte[] buildHeader(final byte[] content) {
        final ByteBuffer buf = ByteBuffer.allocate(32);
        buf.order(ByteOrder.LITTLE_ENDIAN);

        buf.put(UIHH_HEADER);
        buf.put(new byte[]{0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01});
        buf.putInt(CheckSums.getCRC32(content));
        buf.put(new byte[]{0x00, 0x00, 0x00, 0x00, 0x00, 0x00});
        buf.putInt(content.length);
        buf.put(new byte[]{0x00, 0x00, 0x00, 0x00, 0x00, 0x00});

        return buf.array();
    }

    public enum FileType {
        FIRMWARE_ZIP(HuamiFirmwareType.FIRMWARE.getValue(), "firmware.zip"),
        FIRMWARE_CHANGELOG(0x10, "changelog.txt"),
        GPS_ALM_BIN(HuamiFirmwareType.GPS_ALMANAC.getValue(), "gps_alm.bin"),
        GLN_ALM_BIN(0x0f, "gln_alm.bin"),
        LLE_BDS_LLE(0x86, "lle_bds.lle"),
        LLE_GPS_LLE(0x87, "lle_gps.lle"),
        LLE_GLO_LLE(0x88, "lle_glo.lle"),
        LLE_GAL_LLE(0x89, "lle_gal.lle"),
        LLE_QZSS_LLE(0x8a, "lle_qzss.lle"),
        AGPS_EPO_GR_3(-116, "EPO_GR_3.DAT"),
        AGPS_EPO_GAL_7(-115, "EPO_GAL_7.DAT"),
        AGPS_EPO_BDS_3(-114, "EPO_BDS_3.DAT"),
        ;

        private final byte value;
        private final String name;

        FileType(final int value, final String name) {
            this.value = (byte) value;
            this.name = name;
        }

        public byte getValue() {
            return value;
        }

        public static FileType fromValue(final byte value) {
            for (final FileType fileType : values()) {
                if (fileType.getValue() == value) {
                    return fileType;
                }
            }

            return null;
        }
    }

    public static class FileEntry {
        private final FileType type;
        private final byte[] content;

        public FileEntry(final FileType type, final byte[] content) {
            this.type = type;
            this.content = content;
        }

        public FileType getType() {
            return type;
        }

        public byte[] getContent() {
            return content;
        }

        public byte[] buildHeader() {
            final ByteBuffer buf = ByteBuffer.allocate(10);
            buf.order(ByteOrder.LITTLE_ENDIAN);

            buf.put((byte) 0x01);
            buf.put(type.getValue());
            buf.putInt(content.length);
            buf.putInt(CheckSums.getCRC32(content));

            return buf.array();
        }
    }
}
