#!/usr/bin/env python
#
# SPDX-FileCopyrightText: 2022-2023 Espressif Systems (Shanghai) CO LTD
#
# SPDX-License-Identifier: Apache-2.0

import sys
import struct
import argparse
import binascii
import hashlib
import os
import subprocess
import shutil
import json
import lzma

# src_file = 'hello-world.bin'
# compressed_file = 'hello-world.bin.xz'
# packed_compressed_file = 'hello-world.bin.xz.packed'
# siged_packed_compressed_file = 'hello-world.bin.xz.packed.signed'

binary_compress_type = {'none': 0, 'xz':1}
header_version = {'v1': 1, 'v2': 2, 'v3': 3}

SCRIPT_VERSION = '1.0.0'
ORIGIN_APP_IMAGE_HEADER_LEN = 288   # sizeof(esp_image_header_t) + sizeof(esp_image_segment_header_t) + sizeof(esp_app_desc_t). See esp_app_format.h
# At present, we calculate the checksum of the first 4KB data of the old app.
OLD_APP_CHECK_DATA_SIZE = 4096

# v1 compressed data header:
# Note: Encryption_type field is deprecated, the field is reserved for compatibility.
# |------|---------|---------|----------|------------|------------|-----------|----------|----------------|----------------|--------------|------------|
# |      | Magic   | header  | Compress | delta      | Encryption | Reserved  | Firmware | The length of  | The MD5 of     | The CRC32 for| compressed |
# |      | number  | version | type     | type       | type       |           | version  | compressed data| compressed data| the header   | data       |
# |------|---------|---------|----------|------------|------------|-----------|----------|----------------|----------------|--------------|------------|
# | Size | 4 bytes | 1 byte  | 4 bits	| 4 bits     | 1 bytes	  | 1 bytes   | 32 bytes |  4 bytes	      | 32 bytes	   |  4 bytes     |            |
# |------|---------|---------|----------|------------|------------|-----------|----------|----------------|----------------|--------------|------------|
# | Data | String  |         |          |            |            |           | String   | little-endian  |   byte string  | little-endian|            |
# | type | ended   |         |          |            |            |           | ended    |   integer      |                |    integer   |            |
# |      |with ‘\0’|         |          |            |            |           | with ‘\0’|                |                |              | Binary data|
# |------|---------|---------|----------|------------|------------|-----------|----------|----------------|----------------|--------------|------------|
# | Data | “ESP”   |    1    |   0/1    | 0/1        |	          |           |          |                |                |              |            |
# |------|---------|---------|----------|------------|------------|-----------|----------|----------------|----------------|--------------|------------|

# v2 compressed data header
# Note: Encryption_type field is deprecated, the field is reserved for compatibility.
# |------|---------|---------|----------|------------|------------|-----------|----------|----------------|----------------|---------------|---------------|--------------|------------|
# |      | Magic   | header  | Compress | delta      | Encryption | Reserved  | Firmware | The length of  | The MD5 of     | base app check| The CRC32 for | The CRC32 for| compressed |
# |      | number  | version | type     | type       | type       |           | version  | compressed data| compressed data| data len      | base app data | the header   | data       |
# |------|---------|---------|----------|------------|------------|-----------|----------|----------------|----------------|---------------|---------------|--------------|------------|
# | Size | 4 bytes | 1 byte  | 4 bits	| 4 bits     | 1 bytes	  | 1 bytes   | 32 bytes |  4 bytes	      | 32 bytes	   |  4 bytes      |  4 bytes      |  4 bytes     |            |
# |------|---------|---------|----------|------------|------------|-----------|----------|----------------|----------------|---------------|---------------|--------------|------------|
# | Data | String  |         |          |            |            |           | String   | little-endian  |   byte string  | little-endian | little-endian | little-endian|            |
# | type | ended   |         |          |            |            |           | ended    |   integer      |                | integer       |    integer    |    integer   |            |
# |      |with ‘\0’|         |          |            |            |           | with ‘\0’|                |                |               |               |              | Binary data|
# |------|---------|---------|----------|------------|------------|-----------|----------|----------------|----------------|---------------|---------------|--------------|------------|
# | Data | “ESP”   |    1    |   0/1    | 0/1        |	          |           |          |                |                |               |               |              |            |
# |------|---------|---------|----------|------------|------------|-----------|----------|----------------|----------------|---------------|---------------|--------------|------------|

# v3 compressed data header:
# |------|---------|---------|----------|------------|-----------|----------------|----------------|--------------|------------|
# |      | Magic   | header  | Compress | Reserved   | Reserved  | The length of  | The MD5 of     | The CRC32 for| compressed |
# |      | number  | version | type     |            |           | compressed data| compressed data| the header   | data       |
# |------|---------|---------|----------|------------|-----------|----------------|----------------|--------------|------------|
# | Size | 4 bytes | 1 byte  | 4 bits	| 4 bits     | 8 bytes   |  4 bytes	      |   16 bytes	   |  4 bytes     |            |
# |------|---------|---------|----------|------------|-----------|----------------|----------------|--------------|------------|
# | Data | String  | integer |          |            |           | little-endian  |   byte string  | little-endian|            |
# | type | ended   |         |          |            |           |   integer      |                |    integer   |            |
# |      |with ‘\0’|         |          |            |           |                |                |              | Binary data|
# |------|---------|---------|----------|------------|-----------|----------------|----------------|--------------|------------|
# | Data | “ESP”   |     3   |   0/1    |            |           |                |                |              |            |
# |------|---------|---------|----------|------------|-----------|----------------|----------------|--------------|------------|

def xz_compress(store_directory, in_file):
    compressed_file = ''.join([in_file,'.xz'])
    if(os.path.exists(compressed_file)):
        os.remove(compressed_file)

    xz_compressor_filter = [
        {'id': lzma.FILTER_LZMA2, 'preset': 6, 'dict_size': 64*1024},
    ]
    with open(in_file, 'rb') as src_f:
        data = src_f.read()
        with lzma.open(compressed_file, 'wb', format=lzma.FORMAT_XZ, check=lzma.CHECK_CRC32, filters=xz_compressor_filter) as f:
            f.write(data)
            f.close()

    if not os.path.exists(os.path.join(store_directory, os.path.split(compressed_file)[1])):
        shutil.copy(compressed_file, store_directory)
        print('copy xz file done')

def secure_boot_sign(sign_key, in_file, out_file):
    ret = subprocess.call('espsecure.py sign_data --version 2 --keyfile {} --output {} {}'.format(sign_key, out_file, in_file), shell = True)
    if ret:
        raise Exception('sign failed')

def get_app_name():
    with open('flasher_args.json') as f:
        try:
            flasher_args = json.load(f)
            return flasher_args['app']['file']
        except Exception as e:
            print(e)

    return ''

def get_script_version():
    return SCRIPT_VERSION

def main():
    parser = argparse.ArgumentParser()
    parser.add_argument('-hv', '--header_ver', nargs='?', choices = ['v1', 'v2', 'v3'],
            default='v3', help='the version of the packed file header [default:v3]')
    parser.add_argument('-c', '--compress_type', nargs= '?', choices = ['none', 'xz'],
            default='xz', help='compressed type [default:xz]')
    parser.add_argument('-i', '--in_file', nargs = '?',
            default='', help='the new app firmware')
    parser.add_argument('--sign_key', nargs = '?',
            default='', help='the sign key used for secure boot')
    parser.add_argument('-fv', '--fw_ver', nargs='?',
            default='', help='the version of the compressed data(this field is deprecated in v3)')
    parser.add_argument('--add_app_header', action='store_true', help='add app header to use native esp_ota_* & esp_https_ota_* APIs')
    parser.add_argument('-v', '--version', action='version', version=get_script_version(), help='the version of the script')

    args = parser.parse_args()

    compress_type = args.compress_type
    firmware_ver = args.fw_ver
    src_file = args.in_file
    sign_key = args.sign_key
    header_ver = args.header_ver
    add_app_header = args.add_app_header

    if src_file == '':
        origin_app_name = get_app_name()
        if(origin_app_name == ''):
            print('get origin app name fail')
            return

        if os.path.exists(origin_app_name):
            src_file = origin_app_name
        else:
            print('origin app.bin not found')
            return

    print('src file is: {}'.format(src_file))

    # rebuild the cpmpressed_app directory
    cpmoressed_app_directory = 'custom_ota_binaries'
    if os.path.exists(cpmoressed_app_directory):
        shutil.rmtree(cpmoressed_app_directory)

    os.mkdir(cpmoressed_app_directory)
    print('The compressed file will store in {}'.format(cpmoressed_app_directory))

    #step1: compress
    if compress_type == 'xz':
        xz_compress(cpmoressed_app_directory, os.path.abspath(src_file))

        origin_app_name = os.path.split(src_file)[1]

        compressed_file_name = ''.join([origin_app_name, '.xz'])
        compressed_file = os.path.join(cpmoressed_app_directory, compressed_file_name)
    else:
        compressed_file = ''.join(src_file)

    print('compressed file is: {}'.format(compressed_file))

    #step2: packet the compressed image header
    with open(src_file, 'rb') as s_f:
        src_image_header = bytearray(s_f.read(ORIGIN_APP_IMAGE_HEADER_LEN))
        src_image_header[1] = 0x00

    packed_file = ''.join([compressed_file,'.packed'])
    with open(compressed_file, 'rb') as src_f:
        data = src_f.read()
        f_len = src_f.tell()
        # magic number
        bin_data = struct.pack('4s', b'ESP')
        # header version
        bin_data += struct.pack('B', header_version[header_ver])
        # Compress type
        bin_data += struct.pack('B', binary_compress_type[compress_type])
        print('compressed type: {}'.format(binary_compress_type[compress_type]))
        # in header v1/v2, there is a field "Encryption type", this field has been deprecated in v3
        if (header_version[header_ver] < 3):
            bin_data += struct.pack('B', 0)
            # Reserved
            bin_data += struct.pack('?', 0)
            # Firmware version
            bin_data += struct.pack('32s', firmware_ver.encode())
        else:
            # Reserved
            bin_data += struct.pack('10s', b'0')
        # The length of the compressed data
        bin_data += struct.pack('<I', f_len)
        print('compressed data len: {}'.format(f_len))
        # The MD5 for the compressed data
        if (header_version[header_ver] < 3):
            bin_data += struct.pack('32s', hashlib.md5(data).digest())
            if (header_version[header_ver] == 2):
                # Todo, if it's diff OTA, write base app check data len
                bin_data += struct.pack('<I', 0)
                # Todo, if it's diff OTA, write base app crc32 checksum
                bin_data += struct.pack('<I', 0)
        else:
            bin_data += struct.pack('16s', hashlib.md5(data).digest())
        # The CRC32 for the header
        bin_data += struct.pack('<I', binascii.crc32(bin_data, 0x0))

        # write compressed data
        bin_data += data
        with open(packed_file, 'wb') as dst_f:
            # write compressed image header and compressed dada
            dst_f.write(bin_data)

    print('packed file is: {}'.format(packed_file))

    #step3: if need sign, then sign the packed image
    if sign_key != '':
        signed_file = ''.join([packed_file,'.signed'])
        secure_boot_sign(sign_key, packed_file, signed_file)
        print('signed_file is: {}'.format(signed_file))
    else:
        signed_file = ''.join(packed_file)

    if (header_version[header_ver] == 3) and add_app_header:
        with open(signed_file, 'rb+') as src_f:
            packed_data = src_f.read()
            src_f.seek(0)
            # write origin app image header
            src_f.write(src_image_header)
            # write compressed image header and compressed dada
            src_f.write(packed_data)
            print('app image header has been added')

if __name__ == '__main__':
    try:
        main()
    except Exception as e:
        print(e)
        sys.exit(2)
