#!/usr/bin/env python3
#
# Build Actions SoC firmware (RAW/USB/OTA)
#
# Copyright (c) 2017 Actions Semiconductor Co., Ltd
#
# SPDX-License-Identifier: Apache-2.0
#

import os
import sys
import time
import struct
import argparse
import platform
import subprocess
import array
import hashlib
import shutil
import zipfile
import xml.etree.ElementTree as ET
import zlib
import json
import traceback
import csv
import re

from ctypes import *

# private module
from nvram_prop import *;

CFG_TEST_GENERATE_OTA_FIRMWARES_COUNT = 3

CFG_FIRMWARE_NEED_ENCYPT   = False

PRODUCTION_GLOBAL_BUFFER_DEFAULT_SIZE = 0x1000000

PARTITION_ALIGNMENT   = 0x1000

class PARTITION_ENTRY(Structure): # import from ctypes
    _pack_ = 1
    _fields_ = [
        ("name",            c_uint8 * 8),
        ("type",            c_uint8),
        ("file_id",         c_uint8),
        ("mirror_id",       c_uint8, 4),
        ("storage_id",      c_uint8, 4),
        ("flag",            c_uint8),
        ("offset",          c_uint32),
        ("size",            c_uint32),
        ("entry_offs",      c_uint32),
        ] # 64 bytes
SIZEOF_PARTITION_ENTRY   = 0x18

class PARTITION_TABLE(Structure): # import from ctypes
    _pack_ = 1
    _fields_ = [
        ("magic",           c_uint32),
        ("version",         c_uint16),
        ("table_size",      c_uint16),
        ("part_cnt",        c_uint16),
        ("part_entry_size", c_uint16),
        ("reserved1",       c_uint8 * 4),
        ("parts",           PARTITION_ENTRY * 30),
        ("reserved2",       c_uint8 * 4),
        ("table_crc",       c_uint32),
        ] # 64 bytes
SIZEOF_PARTITION_TABLE   = 0x2e8
PARTITION_TABLE_MAGIC = 0x54504341   #'ACPT'

partition_type_table = {'RESERVED':0, 'BOOT':1, 'SYSTEM':2, 'RECOVERY':3, 'DATA':4, 'TEMP':5, 'SYS_PARAM':6}

class FIRMWARE_VERSION(Structure): # import from ctypes
    _pack_ = 1
    _fields_ = [
        ("magic",           c_uint32),
        ("version_code",    c_uint32),
        ("version_res",     c_uint32),
        ("sys_version_code",c_uint32),
        ("version_name",    c_uint8 * 64),
        ("board_name",      c_uint8 * 32),
        ("reserved",        c_uint8 * 12),
        ("checksum",        c_uint32),
        ] # 128 bytes
SIZEOF_FIRMWARE_VERSION   = 0x80
FIRMWARE_VERSION_MAGIC = 0x52455646   #'FVER'


class IMAGE_HEADER(Structure): # import from ctypes
    _pack_ = 1
    _fields_ = [
        ("magic",           c_uint32),
        ("version",         c_uint16),
        ("header_size",     c_uint16),
        ("data_type",       c_uint16),
        ("data_flag",       c_uint16),
        ("data_offset",     c_uint32),
        ("data_size",       c_uint32),
        ("data_base_vaddr", c_uint32),
        ("data_entry_vaddr", c_uint32),
        ("data_checksum",   c_uint32),
        ("reserved",        c_uint8 * 28),
        ("header_crc",      c_uint32),
        ] # 64 bytes
SIZEOF_IMAGE_HEADER   = 64
IMAGE_HEADER_MAGIC = 0x4d494341     #'ACIM'

MP_CARD_CFG_NAME="mp_card.cfg"
FW_MAKER_EXT_CFG_NAME="fw_maker_ext.cfg"
FW_BUILD_TIME_FILE_NAME="fw_build_time.bin";

script_path = os.path.split(os.path.realpath(__file__))[0]

soc_name = ''
board_name = ''
encrypt_fw = ''
efuse_bin = ''
ext_name = ''

# table for calculating CRC
CRC16_TABLE = [
        0x0000, 0x1189, 0x2312, 0x329b, 0x4624, 0x57ad, 0x6536, 0x74bf,
        0x8c48, 0x9dc1, 0xaf5a, 0xbed3, 0xca6c, 0xdbe5, 0xe97e, 0xf8f7,
        0x1081, 0x0108, 0x3393, 0x221a, 0x56a5, 0x472c, 0x75b7, 0x643e,
        0x9cc9, 0x8d40, 0xbfdb, 0xae52, 0xdaed, 0xcb64, 0xf9ff, 0xe876,
        0x2102, 0x308b, 0x0210, 0x1399, 0x6726, 0x76af, 0x4434, 0x55bd,
        0xad4a, 0xbcc3, 0x8e58, 0x9fd1, 0xeb6e, 0xfae7, 0xc87c, 0xd9f5,
        0x3183, 0x200a, 0x1291, 0x0318, 0x77a7, 0x662e, 0x54b5, 0x453c,
        0xbdcb, 0xac42, 0x9ed9, 0x8f50, 0xfbef, 0xea66, 0xd8fd, 0xc974,
        0x4204, 0x538d, 0x6116, 0x709f, 0x0420, 0x15a9, 0x2732, 0x36bb,
        0xce4c, 0xdfc5, 0xed5e, 0xfcd7, 0x8868, 0x99e1, 0xab7a, 0xbaf3,
        0x5285, 0x430c, 0x7197, 0x601e, 0x14a1, 0x0528, 0x37b3, 0x263a,
        0xdecd, 0xcf44, 0xfddf, 0xec56, 0x98e9, 0x8960, 0xbbfb, 0xaa72,
        0x6306, 0x728f, 0x4014, 0x519d, 0x2522, 0x34ab, 0x0630, 0x17b9,
        0xef4e, 0xfec7, 0xcc5c, 0xddd5, 0xa96a, 0xb8e3, 0x8a78, 0x9bf1,
        0x7387, 0x620e, 0x5095, 0x411c, 0x35a3, 0x242a, 0x16b1, 0x0738,
        0xffcf, 0xee46, 0xdcdd, 0xcd54, 0xb9eb, 0xa862, 0x9af9, 0x8b70,
        0x8408, 0x9581, 0xa71a, 0xb693, 0xc22c, 0xd3a5, 0xe13e, 0xf0b7,
        0x0840, 0x19c9, 0x2b52, 0x3adb, 0x4e64, 0x5fed, 0x6d76, 0x7cff,
        0x9489, 0x8500, 0xb79b, 0xa612, 0xd2ad, 0xc324, 0xf1bf, 0xe036,
        0x18c1, 0x0948, 0x3bd3, 0x2a5a, 0x5ee5, 0x4f6c, 0x7df7, 0x6c7e,
        0xa50a, 0xb483, 0x8618, 0x9791, 0xe32e, 0xf2a7, 0xc03c, 0xd1b5,
        0x2942, 0x38cb, 0x0a50, 0x1bd9, 0x6f66, 0x7eef, 0x4c74, 0x5dfd,
        0xb58b, 0xa402, 0x9699, 0x8710, 0xf3af, 0xe226, 0xd0bd, 0xc134,
        0x39c3, 0x284a, 0x1ad1, 0x0b58, 0x7fe7, 0x6e6e, 0x5cf5, 0x4d7c,
        0xc60c, 0xd785, 0xe51e, 0xf497, 0x8028, 0x91a1, 0xa33a, 0xb2b3,
        0x4a44, 0x5bcd, 0x6956, 0x78df, 0x0c60, 0x1de9, 0x2f72, 0x3efb,
        0xd68d, 0xc704, 0xf59f, 0xe416, 0x90a9, 0x8120, 0xb3bb, 0xa232,
        0x5ac5, 0x4b4c, 0x79d7, 0x685e, 0x1ce1, 0x0d68, 0x3ff3, 0x2e7a,
        0xe70e, 0xf687, 0xc41c, 0xd595, 0xa12a, 0xb0a3, 0x8238, 0x93b1,
        0x6b46, 0x7acf, 0x4854, 0x59dd, 0x2d62, 0x3ceb, 0x0e70, 0x1ff9,
        0xf78f, 0xe606, 0xd49d, 0xc514, 0xb1ab, 0xa022, 0x92b9, 0x8330,
        0x7bc7, 0x6a4e, 0x58d5, 0x495c, 0x3de3, 0x2c6a, 0x1ef1, 0x0f78,
        ]

def reflect(crc):
    """
    :type n: int
    :rtype: int
    """
    m = ['0' for i in range(16)]
    b = bin(crc)[2:]
    m[:len(b)] = b[::-1]
    return int(''.join(m) ,2)

def _crc16(data, crc, table):
    """Calculate CRC16 using the given table.
    `data`      - data for calculating CRC, must be bytes
    `crc`       - initial value
    `table`     - table for caclulating CRC (list of 256 integers)
    Return calculated value of CRC

     polynom             :  0x1021
     order               :  16
     crcinit             :  0xffff
     crcxor              :  0x0
     refin               :  1
     refout              :  1
    """
    crc = reflect(crc)

    for byte in data:
        crc = ((crc >> 8) & 0xff) ^ table[(crc & 0xff) ^ byte]

    crc = reflect(crc)

    # swap byte
    crc = ((crc >> 8) & 0xff) | ((crc & 0xff) << 8)

    return crc

def crc16(data, crc=0xffff):
    """Calculate CRC16.
    `data`      - data for calculating CRC, must be bytes
    `crc`       - initial value
    Return calculated value of CRC
    """
    return _crc16(data, crc, CRC16_TABLE)

def md5_file(filename):
    if os.path.isfile(filename):
        with open(filename, 'rb') as f:
            md5 = hashlib.md5()
            md5.update(f.read())
            hash = md5.hexdigest()
            return str(hash)
    return None

def crc32_file(filename):
    if os.path.isfile(filename):
        with open(filename, 'rb') as f:
            crc = zlib.crc32(f.read(), 0) & 0xffffffff
            return crc
    return 0

def pad_file(filename, align = 4, fillbyte = 0xff):
        with open(filename, 'ab') as f:
            filesize = f.tell()
            if (filesize % align):
                padsize = align - filesize & (align - 1)
                f.write(bytearray([fillbyte]*padsize))

def new_file(filename, filesize, fillbyte = 0xff):
        with open(filename, 'wb') as f:
            f.write(bytearray([fillbyte]*filesize))

def dd_file(input_file, output_file, count=None, seek=None, skip=None):
    inf = open(input_file, mode='rb')
    outf = open(output_file, mode='rb+')
    if skip is not None:
        inf.seek(skip)
    if seek is not None:
        outf.seek(seek)
    if count is None:
        count = os.path.getsize(input_file)
    outf.write(inf.read(count))
    inf.close
    outf.close

def file_link(filename, file_add):
    if not os.path.exists(filename):
        return
    if not os.path.exists(file_add):
        return
    #print("file %s, link %s \n" %(filename, file_add))
    with open(file_add, 'rb') as fa:
        file_data = fa.read()
        fa.close()
    fsize = os.path.getsize(filename)
    with open(filename, 'rb+') as f:
        f.seek(fsize, 0)
        f.write(file_data)
        f.close()

def zip_dir(source_dir, output_filename):
    zf = zipfile.ZipFile(output_filename, 'w')
    pre_len = len(os.path.dirname(source_dir))
    for parent, dirnames, filenames in os.walk(source_dir):
        for filename in filenames:
            pathfile = os.path.join(parent, filename)
            arcname = pathfile[pre_len:].strip(os.path.sep)
            zf.write(pathfile, arcname)
    zf.close()

def memcpy_n(cbuffer, bufsize, pylist):
    size = min(bufsize, len(pylist))
    for i in range(size):
        cbuffer[i]= ord(pylist[i])

def c_struct_crc(c_struct, length):
    crc_buf = (c_byte * length)()
    memmove(addressof(crc_buf), addressof(c_struct), length)
    return zlib.crc32(crc_buf, 0) & 0xffffffff

def align_down(data, alignment):
    return data // alignment * alignment

def align_up(data, alignment):
    return align_down(data + alignment - 1, alignment)

def run_cmd(cmd):
    """Echo and run the given command.

    Args:
    cmd: the command represented as a list of strings.
    Returns:
    A tuple of the output and the exit code.
    """
#    print("Running: ", " ".join(cmd))
    p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
    output, _ = p.communicate()
#    print("%s" % (output.rstrip()))
    return (output, p.returncode)

def panic(err_msg):
    #print('\033[1;31;40m')
    print('\nFW: Error: %s\n' %err_msg)
    #print('\033[0m')
    sys.exit(1)

def print_notice(msg):
    print('\033[1;32;40m%s\033[0m' %msg)

def cygpath(upath):
    cmd = ['cygpath', '-w', upath]
    (wpath, exit_code) = run_cmd(cmd)
    if (0 != exit_code):
        return upath
    return wpath.decode().strip()

def is_windows():
    sysstr = platform.system()
    if (sysstr.startswith('Windows') or \
       sysstr.startswith('MSYS') or     \
       sysstr.startswith('MINGW') or    \
       sysstr.startswith('CYGWIN')):
        return True
    else:
        return False

def is_msys():
    sysstr = platform.system()
    if (sysstr.startswith('MSYS') or    \
        sysstr.startswith('MINGW') or   \
        sysstr.startswith('CYGWIN')):
        return True
    else:
        return False

def soc_is_andes():
    if soc_name == 'andes':
        return True
    else:
        return False

class firmware(object):
    def __init__(self, cfg_file):
        self.crc_chunk_size = 32
        self.crc_full_chunk_size = self.crc_chunk_size + 2
        self.part_num = 0
        self.partitions = []
        self.disk_size = 0x400000   # 4MB by default
        self.fw_xml_file = cfg_file
        self.fw_dir = os.path.dirname(cfg_file)
        self.bin_dir = os.path.join(self.fw_dir, 'bin')
        self.ota_dir = os.path.join(self.fw_dir, 'ota')
        self.orig_bin_dir = self.bin_dir + '_orig'
        self.orig_ota_dir = self.ota_dir + '_orig'
        self.fw_version = {}
        self.boot_file_name = ''
        self.param_file_name = ''
        self.is_earphone_app = False
        self.upgrade_baudrate = 2000000
        self.atf_file_list = []

        if "bt_earphone" in os.path.abspath(self.fw_dir).split('\\'):
            self.is_earphone_app = True
            print("is earphone SDK")

        # self.parse_config(cfg_file)

    def _get_partition_info(self, keyword, keystr):
        if self.partitions:
            for info_dict in self.partitions:
                if keyword in info_dict and keystr == info_dict[keyword]:
                    return info_dict
        return {}

    def parse_config(self):
        #print('FW: Parse config file: %s' %self.fw_xml_file)
        tree = ET.ElementTree(file=self.fw_xml_file)
        root = tree.getroot()
        if (root.tag != 'firmware'):
            sys.stderr.write('error: invalid firmware config file')
            sys.exit(1)

        disk_size_prop = root.find('disk_size')
        if disk_size_prop is not None:
            self.disk_size = int(disk_size_prop.text.strip(), 0)

        firmware_version = root.find('firmware_version')
        for prop in firmware_version:
            self.fw_version[prop.tag] = prop.text.strip()

        if 'version_name' in self.fw_version.keys():
            cur_time = time.strftime('%y%m%d%H%M',time.localtime(time.time()))
            version_name = self.fw_version['version_name'].replace('$(build_time)', cur_time)
            self.fw_version['version_name'] = version_name
            # gen fw build time file
            fw_build_time_file = os.path.join(self.fw_dir, FW_BUILD_TIME_FILE_NAME)
            with open(fw_build_time_file, 'w') as f:
                f.write(cur_time)

        part_list = root.find('partitions').findall('partition')
        for part in part_list:
            part_prop = {}
            for prop in part:
                part_prop[prop.tag] = prop.text.strip()

            if 'file_name' in part_prop.keys():
                bin_file = os.path.join(self.bin_dir, part_prop['file_name'])
            else:
                bin_file = None

            if bin_file and ('SYS_PARAM' != part_prop['type']) and not os.path.exists(bin_file):
                print_notice('partition %s ignored: cannot found bin %s' %(part_prop['name'], bin_file))
            else:
                self.partitions.append(part_prop)
                self.part_num = self.part_num + 1

        self.part_num = len(self.partitions);
#        print(self.part_num)
#        print(self.partitions)
        if (0 == self.part_num):
            panic('cannot found parition config')

        for part in self.partitions:
            if ('file_name' in part.keys()) and ('BOOT' == part['type']):
                self.boot_file_name = part['file_name']

        for part in self.partitions:
            if ('file_name' in part.keys()) and ('SYS_PARAM' == part['type']):
                self.param_file_name = part['file_name']

        param_file = os.path.join(self.bin_dir, self.param_file_name)
        self.generate_partition_table(param_file)
        self.generate_firmware_version(param_file)

        if os.path.isdir(self.orig_bin_dir):
            shutil.rmtree(self.orig_bin_dir)
            time.sleep(0.1)
        shutil.copytree(self.bin_dir, self.orig_bin_dir)
        self.rebuild_sdfs(self.bin_dir)

        self.check_part_file_size()

        self.generate_crc_file()

        if os.path.isfile(efuse_bin):
            self.encrypt_files()
        elif CFG_FIRMWARE_NEED_ENCYPT:
            panic('firmware encryt must be enabled')

    def __find_key_info(self, key, cfg_str):
        # key defintion pattern: key = "string0, string1";
        key_p = re.compile('^\s*%s\s*=\s*"(.+)"\s*;\s*$' %key, re.M)
        cfg_info_list = key_p.findall(cfg_str)
        if cfg_info_list:
            info_str = ', '.join(cfg_info_list)
            info_list = [i.strip() for i in info_str.split(',')]
            return info_list
        else:
            return []

    def rebuild_sdfs(self, bin_dir):
        """rebuild sdfs, in order to append configuration and tone files.
        """
        fw_cfg_file = os.path.join(bin_dir, "fw_configuration.cfg")
        if not os.path.isfile(fw_cfg_file):
            return False
        try:
            with open(fw_cfg_file, 'r') as f:
                cfg_str = f.read()
        except:
            with open(fw_cfg_file, 'r', encoding = 'utf-8') as f:
                cfg_str = f.read()

        count = 0
        dst_dir = None
        key_list = self.__find_key_info('CFG_FILE_KEY', cfg_str)
        if key_list:
            config_dir = os.path.join(bin_dir, 'config')
            if "TONE_SDFS" in key_list:
                key_list.remove("TONE_SDFS")
                dst_dir = os.path.join(bin_dir, 'sdfs')
                if not os.path.isdir(dst_dir):
                    dst_dir = os.path.join(bin_dir, 'tone')
            elif "SYS_SDFS" in key_list:
                key_list.remove("SYS_SDFS")
                dst_dir = os.path.join(bin_dir, 'ksdfs')
            if dst_dir and os.path.isdir(dst_dir) and os.path.isdir(config_dir):
                for key in key_list:
                    file_name_list = self.__find_key_info(key, cfg_str)
                    count += len(file_name_list)
                    for file_name in file_name_list:
                        #print(file_name)
                        shutil.copyfile(os.path.join(config_dir, file_name),
                            os.path.join(dst_dir, file_name))

        if count > 0:
            sdfs_name = os.path.basename(dst_dir)
            script_sdfs_path = os.path.join(script_path, 'build_sdfs.py')
            sdfs_bin_path = os.path.join(bin_dir, sdfs_name+".bin")
            cmd = ['python', '-B', script_sdfs_path,  '-o', sdfs_bin_path, '-d', dst_dir]
            (outmsg, exit_code) = run_cmd(cmd)
            if exit_code !=0:
                print('make build_sdfs_bin error')
                print(outmsg.decode('utf-8'))
                sys.exit(1)
            #print("\n build_sdfs %s finshed\n\n" %(sdfs_name))
            return True
        return False

    def check_part_file_size(self):
        csv_file = os.path.join(self.bin_dir, 'partition_size.csv')
        csv_f = open(csv_file, 'w', newline = '')
        csv_writer = csv.writer(csv_f, dialect = 'excel')
        csv_writer.writerow(['partition', 'size', 'real size', 'free'])

        for part in self.partitions:
            if not 'file_name' in part.keys():
                continue
            partfile = os.path.join(self.bin_dir, part['file_name']);
            if os.path.isfile(partfile):
                partfile_size = os.path.getsize(partfile)
                part_size = int(part['size'], 0);
                part_addr = int(part['address'], 0);
                file_addr = int(part['file_address'], 0);

                if part['type'] == 'SYSTEM':
                    ksdfs_bin_file = os.path.join(self.bin_dir, "ksdfs.bin")
                    if os.path.isfile(ksdfs_bin_file):
                        file_link(partfile, ksdfs_bin_file)
                        partfile_size += os.path.getsize(ksdfs_bin_file)

                if file_addr < part_addr or file_addr >= (part_addr + part_size) :
                    panic('partition %s: invalid file_address 0x%x' \
                         %(part['name'], file_addr))

                part_file_max_size = part_size - (file_addr - part_addr)
                csv_writer.writerow([part['file_name'],
                    "%.2f" %(part_file_max_size / 1024),
                    "%.2f" %(partfile_size / 1024),
                    "%.2f" %((part_file_max_size - partfile_size) / 1024)])

                print('partition %s: \'%s\' file size 0x%x(%d KB), max size 0x%x(%d KB)!' \
                    %(part['name'], part['file_name'], partfile_size, partfile_size/1024, \
                    part_file_max_size, part_file_max_size/1024))

                if partfile_size > part_file_max_size:
                    csv_f.close()
                    panic('partition %s: \'%s\' file size 0x%x is bigger than partition size 0x%x!' \
                        %(part['name'], part['file_name'], partfile_size, part_size))

        csv_f.close()

    def boot_calc_checksum(self, data):
            s = sum(array.array('H',data))
            s = s & 0xffff
            return s

    def get_nvram_prop(self, name):
        prop_file = os.path.join(self.bin_dir, 'nvram.prop');
        if os.path.isfile(prop_file):
            with open(prop_file) as f:
                properties = PropFile(f.readlines())
                return properties.get(name)
        return ''

    def generate_crc_file(self):
        crc_files = []
        for part in self.partitions:
            if ('file_name' in part.keys()) and ('true' == part['enable_crc']):
                crc_files.append(part['file_name'])

        crc_files = list(set(crc_files))

        for crc_file in crc_files:
            self.add_crc(os.path.join(self.bin_dir, crc_file), self.crc_chunk_size)

    def encrypt_file(self, file_path, blk_size):
        #print ('FW: encrypt file %s' %(file_path))

        if not os.path.isfile(encrypt_fw):
            panic('Cannot found encrypt fw')

        if (is_windows()):
            # windows
            fw2x_path = script_path + '/utils/windows/fw2x.exe'
        else:
            if ('32bit' == platform.architecture()[0]):
                # linux x86
                fw2x_path = script_path + '/utils/linux-x86/fw2x/fw2x'
            else:
                # linux x86_64
                fw2x_path = script_path + '/utils/linux-x86_64/fw2x/fw2x'

        chip_name = "DVB"
        fwimage_cfg_file = os.path.join(self.orig_bin_dir, "fwimage.cfg") 
        if os.path.isfile(fwimage_cfg_file):
            with open(fwimage_cfg_file, 'r') as f:
                cfg_str = f.read()
            # CHIP_NAME = "ATS3085C";
            name_p = re.compile('^\s*CHIP_NAME\s*=\s*\"\s*(\w+)\s*\";$', re.M)
            name_obj = name_p.search(cfg_str)
            if name_obj:
                chip_name = name_obj.groups()[0]

        # extrace lfi.bin from btsys.fw
        fw2x_cmd = [fw2x_path, '-fsssfffs', encrypt_fw, 'MakeEncryptBin',\
               'Encrypt', str(blk_size), file_path, file_path, efuse_bin, chip_name]
        (outmsg, exit_code) = run_cmd(fw2x_cmd)
        if exit_code != 0:
            print('FW2X: encrypt file %s error' %(file_path))
            print(outmsg.decode('utf-8'))
            sys.exit(1)

    def encrypt_files(self):
        for part in self.partitions:
            if CFG_FIRMWARE_NEED_ENCYPT:
                if ('BOOT' == part['type'] or 'SYSTEM' == part['type'] or 'RECOVERY' == part['type']) or 'SYS_PARAM' == part['type'] \
                    and ('true' != part['enable_encryption']):
                    panic('BOOT/SYSTEM partition must enable encryption')
                    sys.exit(3)

            if not 'file_name' in part.keys():
                continue

            if ('true' == part['enable_encryption']):
                print ('FW: encrypt file \'' + part['file_name'] + '\'')

                encrypt_unit = self.crc_chunk_size
                if 'BOOT' == part['type']:
                    encrypt_unit = 512

                if ('true' == part['enable_crc']):
                    self.encrypt_file(os.path.join(self.bin_dir, part['file_name']), encrypt_unit + 2)
                else:
                    self.encrypt_file(os.path.join(self.bin_dir, part['file_name']), encrypt_unit)

    def encrypt_param_file(self, param_file):
        for part in self.partitions:
            if ('file_name' in part.keys() and 'SYS_PARAM' == part['type']):
                if ('true' == part['enable_encryption']):
                    if ('true' == part['enable_crc']):
                        self.encrypt_file(param_file, self.crc_full_chunk_size)
                    else:
                        self.encrypt_file(param_file, self.crc_chunk_size)

    def generate_partition_table(self, param_file):
        part_tbl = PARTITION_TABLE()
        memset(addressof(part_tbl), 0, SIZEOF_PARTITION_TABLE)

        part_tbl.magic = PARTITION_TABLE_MAGIC
        part_tbl.version = 0x0101
        part_tbl.table_size = SIZEOF_PARTITION_TABLE
        part_tbl.table_crc = 0x0
        part_tbl.part_entry_size = SIZEOF_PARTITION_ENTRY
        idx = 0
        for part in self.partitions:
            memcpy_n(part_tbl.parts[idx].name, 8, part['name'])
            part_tbl.parts[idx].type = partition_type_table[part['type']]
            part_tbl.parts[idx].file_id = int(part['file_id'])
            if 'mirror_id' in part.keys():
                part_tbl.parts[idx].mirror_id = int(part['mirror_id'])
            else:
                part_tbl.parts[idx].mirror_id = 0xf

            if 'storage_id' in part.keys():
                part_tbl.parts[idx].storage_id = int(part['storage_id'])
            else:
                part_tbl.parts[idx].storage_id = 0x0

            #print('part [%d] mirror id %d' %(idx, part_tbl.parts[idx].mirror_id))
            part_tbl.parts[idx].flag = 0
            if 'enable_crc' in part.keys() and 'true' == part['enable_crc']:
                part_tbl.parts[idx].flag |= 0x1
            if 'enable_encryption' in part.keys() and 'true' == part['enable_encryption']:
                part_tbl.parts[idx].flag |= 0x2
            if 'enable_boot_check' in part.keys() and 'true' == part['enable_boot_check']:
                part_tbl.parts[idx].flag |= 0x4
            if 'enable_sector' in part.keys() and 'true' == part['enable_sector']:
                part_tbl.parts[idx].flag |= 0x8

            part_addr = int(part['address'], 0)
            part_size = int(part['size'], 0)
            file_addr = int(part['file_address'], 0)

            if part_addr & (PARTITION_ALIGNMENT - 1):
                panic('partition %s: unaligned part address 0x%x, need be aligned with 0x%x' \
                     %(part['name'], part_addr, PARTITION_ALIGNMENT))

            if part_size & (PARTITION_ALIGNMENT - 1):
                panic('partition %s: unaligned part size 0x%x, need be aligned with 0x%x' \
                     %(part['name'], part_size, PARTITION_ALIGNMENT))


            if file_addr < part_addr or file_addr >= (part_addr + part_size) :
                panic('partition %s: file_address 0x%x is not in part' \
                     %(part['name'], file_addr))

            if 'enable_sector' in part.keys() and 'true' == part['enable_sector'] :
                part_tbl.parts[idx].offset = int(part_addr / 512)
                part_tbl.parts[idx].size = int(part_size / 512)
                part_tbl.parts[idx].entry_offs = int(file_addr / 512)
            else:
                part_tbl.parts[idx].offset = part_addr
                part_tbl.parts[idx].size = part_size
                part_tbl.parts[idx].entry_offs = file_addr

            #print('%d: 0x%x, 0x%x' %(idx, part_tbl.parts[idx].offset, part_tbl.parts[idx].size))
            idx = idx + 1
        part_tbl.part_cnt = idx
        part_tbl.table_crc = c_struct_crc(part_tbl, SIZEOF_PARTITION_TABLE - 4)

        """
        with open('parttbl.bin', 'wb') as f:
            f.write(part_tbl)
        """

        with open(param_file, 'wb') as f:
            f.seek(0, 0)
            f.write(part_tbl)

    def generate_firmware_version(self, param_file):
        global board_name
        if 'version_code' not in self.fw_version:
            print('\033[1;31;40minfo: no version code\033[0m');
            self.fw_version['version_code'] = '0'
        if 'version_res' not in self.fw_version:
            print('\033[1;31;40minfo: no version res\033[0m');
            self.fw_version['version_res'] = '0'
        #print(self.fw_version)
        fw_ver = FIRMWARE_VERSION()
        memset(addressof(fw_ver), 0, SIZEOF_FIRMWARE_VERSION)

        fw_ver.magic = FIRMWARE_VERSION_MAGIC
        fw_ver.version_code = int(self.fw_version['version_code'], 0)
        fw_ver.version_res = int(self.fw_version['version_res'], 0)
        version_name = self.fw_version['version_name']
        if len(version_name) > len(fw_ver.version_name):
            panic('max version_name is t' %(len(fw_ver.version_name)))

        memcpy_n(fw_ver.version_name, len(self.fw_version['version_name']), \
                 self.fw_version['version_name'])

        memcpy_n(fw_ver.board_name, len(board_name), board_name)

        self.fw_version['board_name'] = board_name

        fw_ver.checksum = c_struct_crc(fw_ver, SIZEOF_FIRMWARE_VERSION - 4)
        #time.strftime('%y%m%d-%H%M%S',time.localtime(time.time()))

        """
        with open('fw.bin', 'wb') as f:
            f.write(fw_ver)
        """

        with open(param_file, 'rb+') as f:
            f.seek(SIZEOF_PARTITION_TABLE, 0)
            f.write(fw_ver)
            f.seek(1020, 0)
            f.write(b'part')

    def generate_maker_ext_config(self, m_ext_cfg_file):
        with open(m_ext_cfg_file, 'a') as f:
            f.write('\n//nvram partition infor\n')

            nvram_partition_find = 0
            for part in self.partitions:
                if (('nvram_factory' == part['name'])):
                    f.write('NVRAM_FACTORY_RO=' + str(part['address']) + ',' + str(part['size']) + ';\n')
                    nvram_partition_find = 1;
            if (nvram_partition_find == 0):
                f.write('NVRAM_FACTORY_RO=0,0;\n')

            nvram_partition_find = 0
            for part in self.partitions:
                if (('nvram_user' == part['name'])):
                    f.write('NVRAM_USER_RW=' + str(part['address']) + ',' + str(part['size']) + ';\n')
                    nvram_partition_find = 1;
            if (nvram_partition_find == 0):
                f.write('NVRAM_USER_RW=0,0;\n')

            nvram_partition_find = 0
            for part in self.partitions:
                if (('nvram_factory_rw' == part['name'])):
                    f.write('NVRAM_FACTORY_RW=' + str(part['address']) + ',' + str(part['size']) + ';\n')
                    nvram_partition_find = 1;
            if (nvram_partition_find == 0):
                f.write('NVRAM_FACTORY_RW=0,0;\n')

    def generate_maker_config(self, mcfg_file):
        with open(mcfg_file, 'w') as f:
            f.write('SETPATH=\".\\";\n')
            f.write('SPI_STG_CAP=' + str(self.disk_size // 0x200) + ';\n')
            f.write('BASEFILE=\"afi.bin\";\n')

            """
            storage_id definition:
                - 0: NOR
                - 1: EMMC
                - 2: NAND
            INF_STORAGE_SOLUTION definition:
                - 0: NAND
                - 1: NOR
                - 2: MMC
                - 3: NOR + NAND
                - 4: NOR + EMMC
            """
            boot_storage_id = 0
            data_storage_id = 0
            # check boot storage id
            for part in self.partitions:
                if (('file_name' in part.keys()) and ('true' == part['enable_dfu']) and ('BOOT' == part['type'])):
                    boot_storage_id = int(part['storage_id'], 0)

            # write normal partition first
            for part in self.partitions:
                if (('file_name' in part.keys()) and ('true' == part['enable_dfu']) and ('BOOT' != part['type'])):
                    if (boot_storage_id == int(part['storage_id'], 0)) :
                        f.write('WFILE=\"' + part['file_name'] + '\",' + part['file_address'])
                        f.write(';\n')

            # nor boot ,can have extern data storage
            if (boot_storage_id == 0):
                for part in self.partitions:
                    if (('file_name' in part.keys()) and ('true' == part['enable_dfu']) and ('BOOT' != part['type'])):
                        if (boot_storage_id != int(part['storage_id'], 0) ) :
                            f.write('EXTERN_DATA_FILE=\"' + part['file_name'] + '\",' + part['file_address'])
                            f.write(';\n')
                            data_storage_id = int(part['storage_id'], 0)

            # write boot partition
            for part in self.partitions:
                if (('file_name' in part.keys()) and ('true' == part['enable_dfu']) and ('BOOT' == part['type'])):
                    if (boot_storage_id == int(part['storage_id'], 0)):
                        f.write('WFILE=\"' + part['file_name'] + '\",' + part['file_address'])
                        f.write(';\n')

            # check the current solution such as NOR-only, NAND-only, NOR + NAND

            # check the current solution such as NOR-only, EMMC-only NAND-only, NOR + NAND, NOR + EMMC
            if (boot_storage_id == 0):
                if (data_storage_id == 0):
                    f.write('INF_TDK_STORAGE_TYPE=1' + ';\n') # NOR-only solution
                elif (data_storage_id == 1):
                    f.write('INF_TDK_STORAGE_TYPE=4' + ';\n') # NOR + EMMC solution
                else :
                    f.write('INF_TDK_STORAGE_TYPE=3' + ';\n') # NOR + NAND solution
                f.write('EXTERN_DATA_STG_CAP=0x80000' + ';\n')
            elif (boot_storage_id == 1):  # EMMC-only solution
                f.write('INF_TDK_STORAGE_TYPE=2' + ';\n') # EMMC-only solution
            else:  # NAND-only solution
                f.write('INF_TDK_STORAGE_TYPE=0' + ';\n') # NAND-only solution

            global_buffer_size = max(self.disk_size, 0)
            if global_buffer_size > PRODUCTION_GLOBAL_BUFFER_DEFAULT_SIZE:
                f.write('INF_GLOBAL_BUFFER_SIZE=' + str(global_buffer_size) + ';\n')

            # The length of 'VER' field is limited to 32 bytes
            if len(self.fw_version['version_name']) > 32:
                fw_ver_temp = self.fw_version['version_name'][0:31]
            else:
                fw_ver_temp = self.fw_version['version_name']

            f.write('VER=\"' + fw_ver_temp + '\"' + ';\n');

            #add mp card config
            if os.path.exists(os.path.join(self.bin_dir, MP_CARD_CFG_NAME)):
                f.write('#include \"' + MP_CARD_CFG_NAME + '\"\n')

            #add extern data config
            if os.path.exists(os.path.join(self.bin_dir, FW_MAKER_EXT_CFG_NAME)):
                f.write('#include \"' + FW_MAKER_EXT_CFG_NAME + '\"\n')

    def generate_raw_image(self, rawfw_name):
        print('FW: Build raw spinor image')

        rawfw_file = os.path.join(self.fw_dir, rawfw_name)
        if os.path.exists(rawfw_file):
            os.remove(rawfw_file)

        new_file(rawfw_file, self.disk_size, 0xff)

        for part in self.partitions:
            if ('file_name' in part.keys()) and ('true' == part['enable_raw']) and (int(part['storage_id'], 0) == 0):
                addr = int(part["address"], 16)
                srcfile = os.path.join(self.bin_dir, part['file_name'])
                dd_file(srcfile, rawfw_file, seek=addr)

        if not os.path.exists(rawfw_file):
            panic('Failed to generate SPINOR raw image')

    def generate_firmware(self, fw_name):
        print('FW: Build USB DFU image')
        maker_cfgfile = os.path.join(self.bin_dir, 'fw_maker.cfg')
        self.generate_maker_config(maker_cfgfile);

        maker_ext_cfgfile = os.path.join(self.bin_dir, FW_MAKER_EXT_CFG_NAME)
        self.generate_maker_ext_config(maker_ext_cfgfile);

        fw_file = os.path.join(self.fw_dir, fw_name);

        if (is_windows()):
            cmd = [script_path + '/utils/windows/maker.exe', '-c',  maker_cfgfile, \
                   '-o', fw_file, '-mf']
        else:
            if os.path.exists(fw_file):
                os.remove(fw_file)

            if ('32bit' == platform.architecture()[0]):
                # linux x86
                maker_path = script_path + '/utils/linux-x86/maker/PyMaker.pyo'
            else:
                # linux x86_64
                maker_path = script_path + '/utils/linux-x86_64/maker/PyMaker.pyo'
            cmd = ['python2', '-O', maker_path, '-c', maker_cfgfile, '-o', fw_file, '--mf', '1']

        (outmsg, exit_code) = run_cmd(cmd)

        if not os.path.exists(fw_file):
            panic('Maker error: %s' %(outmsg))

        print_notice('Build successfully!')
        if is_msys():
            print_notice('Firmware: ' + cygpath(fw_file))
        else:
            print_notice('Firmware: ' + fw_file)

    def build_ota_image(self, ota_fw_name):
        ota_fw_name = os.path.join(self.fw_dir, ota_fw_name)
        script_ota_path = os.path.join(script_path, 'build_ota_image.py')
        cmd = [sys.executable, script_ota_path, '-i', self.bin_dir, '-c', self.fw_xml_file,
            '-o', ota_fw_name, '-b', board_name]

        (outmsg, exit_code) = run_cmd(cmd)
        if exit_code !=0:
            print('make ota error')
            print(outmsg.decode('utf-8'))
            sys.exit(1)

    def generate_att_image(self, att_filename, fw_filename):
        att_dir = os.path.join(self.fw_dir, 'att')
        if not os.path.isdir(att_dir):
            return

        print('ATT: Build ATT image')

        for part in self.partitions:
            if ('file_name' in part.keys())and ('true' == part['enable_dfu']):
                shutil.copyfile(os.path.join(self.bin_dir, part['file_name']), \
                                os.path.join(att_dir, part['file_name']))

        maker_cfgfile = os.path.join(self.bin_dir, 'fw_maker.cfg')
        self.generate_maker_config(maker_cfgfile);

        temp_maker_cfgfile = os.path.join(self.bin_dir, 'fw_maker_temp.cfg')
        with open(temp_maker_cfgfile, 'wb') as f:
            with open(maker_cfgfile, 'rb') as f1:
                f1_data = f1.read()
                f.write(f1_data)

            mp_card_cfgfile = os.path.join(self.bin_dir, 'mp_card.cfg')
            with open(mp_card_cfgfile, 'rb') as f2:
                f2_data = f2.read()
                f.write(f2_data)

            maker_ext_cfgfile = os.path.join(self.bin_dir, 'fw_maker_ext.cfg')
            with open(maker_ext_cfgfile, 'rb') as f3:
                f3_data = f3.read()
                f.write(f3_data)

        shutil.copyfile(temp_maker_cfgfile, \
                        os.path.join(att_dir, 'fw_maker.cfg'))

        shutil.copyfile(os.path.join(self.fw_dir, fw_filename),
                        os.path.join(att_dir, 'attdfu.fw'))

        print('ATT: Generate ATT image %s' %(os.path.join(self.fw_dir, att_filename)))
        self.build_atf_image(os.path.join(self.fw_dir, att_filename), att_dir)

    def build_atf_image(self, image_path, atf_file_dir, files = []):
        script_atf_path = os.path.join(script_path, 'build_atf_image.py')
        cmd = [sys.executable, script_atf_path,  '-o', image_path]

        for parent, dirnames, filenames in os.walk(atf_file_dir):
            for filename in filenames:
                files.append(os.path.join(parent, filename))

        cmd = cmd + files

        (outmsg, exit_code) = run_cmd(cmd)
        if exit_code !=0:
            print('make att error')
            print(outmsg.decode('utf-8'))
            sys.exit(1)

    def add_crc(self, filename, chunk_size):
        with open(filename, 'rb') as f:
            filedata = f.read()

        length = len(filedata)
        i = 0
        with open(filename, 'wb') as f:
            while (length > 0):
                if (length < chunk_size):
                    chunk = filedata[i : i + length] + bytearray(chunk_size - length)
                else:
                    chunk = filedata[i : i + chunk_size]
                crc = crc16(bytearray(chunk), 0xffff)
                f.write(chunk + struct.pack('<H',crc))
                length -= chunk_size
                i += chunk_size

    def generate_earphone_config(self, mcfg_file, upg_ini_file):
        with open(mcfg_file, 'w') as f, open(upg_ini_file, 'w') as f_u:
            fw_ver_str = str(self.fw_version).strip()[1:-1]
            f.write('\nFW_VERSION="%s";\n\n' %fw_ver_str.replace("\'", ""))

            f.write('\nSETPATH="..\\";\n')
            f.write('FIRMWARE_XML="firmware.xml";\n')

            f.write('\nSETPATH=".\\";\n')
            f.write('SPI_STG_CAP=%d;\n' %(self.disk_size // 0x200))
            f.write('BASEFILE="afi.bin";\n')
            f.write('UPGRADE_BAUDRATE=%d;\n' %self.upgrade_baudrate)
            f.write('PRODUCT_CONFIG="%s";\n' %os.path.basename(upg_ini_file))
            f.write('SYS_SDFS="ksdfs.bin";\n')

            f_u.write("adfu_switch\n")
            f_u.write("baudrate = %d\n" %self.upgrade_baudrate)
            f_u.write("payload = 32768\n")
            # f_u.write("adfu_start\n")

            swrite_data_list = []
            # write dfu data
            for part in self.partitions:
                if ('file_name' in part.keys()) and ('true' == part['enable_dfu']):
                    w_file_name = part['file_name']
                    storage_id = int(part['storage_id'], 0)
                    w_file_addr = int(part['file_address'], 0)
                    if 'BOOT' == part['type']:
                        info_tuple = (w_file_name, storage_id, w_file_addr, True)
                    else:
                        info_tuple = (w_file_name, storage_id, w_file_addr)
                    swrite_data_list.append(info_tuple)

            swrite_data_list.sort(key = lambda x:(x[1], x[2]))

            prv_storage_id = -1
            b_clear_history_set = 0
            for info_tuple in swrite_data_list:
                w_file_name = info_tuple[0]
                storage_id = info_tuple[1]
                w_file_addr = info_tuple[2]

                if storage_id != prv_storage_id:
                    if prv_storage_id >= 0:
                        f_u.write("verify_fw\n")
                    if prv_storage_id != -1:
                        if b_clear_history_set == 0:
                            f_u.write("clear_history\n")
                            b_clear_history_set = 1
                    f.write("\n")
                    f_u.write("\n")
                    f_u.write("renew_flash_id %d\n" %storage_id)
                    f_u.write("sinit %d\n" %storage_id)
                    prv_storage_id = storage_id

                if len(info_tuple) > 3:
                    f_u.write("serase 0x%x %d 0\n" %(w_file_addr, 0x4000))
                else:
                    f_u.write("swrite 0x%08x 0 %s\n" %(w_file_addr, w_file_name))

                file_path = os.path.join(self.orig_bin_dir, w_file_name)
                if os.path.getsize(file_path) > 0x400000:
                    self.atf_file_list.append(file_path)
                else:
                    f.write('WFILE="%s",0x%x;\n' %(w_file_name, w_file_addr))

            if prv_storage_id >= 0:
                f_u.write("verify_fw\n")
            if b_clear_history_set == 0:
                f_u.write("clear_history\n")

            for info_tuple in swrite_data_list:
                if len(info_tuple) <= 3:
                    continue
                w_file_name = info_tuple[0]
                storage_id = info_tuple[1]
                w_file_addr = info_tuple[2]
                if storage_id != prv_storage_id:
                    f_u.write("\n")
                    f_u.write("renew_flash_id %d\n" %storage_id)
                    f_u.write("sinit %d\n" %storage_id)
                    prv_storage_id = storage_id

                f_u.write("swrite 0x%08x 0 %s\n" %(w_file_addr, w_file_name))

            f_u.write("\ndisconnect reboot\n")

            #add extern data config
            f.write('\n#include "fw_product.cfg"\n')
            if os.path.isfile(os.path.join(self.orig_bin_dir, "fwimage.cfg")):
                f.write('#include "fwimage.cfg"\n')
            f.write('\nSETPATH=".\\config";\n')
            f.write('#include "fw_configuration.cfg"\n')

    def generate_earphone_firmware(self, fw_name):
        print('FW: Build earphone firmware image')
        maker_cfgfile = os.path.join(self.orig_bin_dir, 'fw_maker.cfg')
        upg_cfgfile = os.path.join(self.orig_bin_dir, 'product.ini')
        self.generate_earphone_config(maker_cfgfile, upg_cfgfile);

        att_dir = os.path.join(self.orig_bin_dir, 'ATT')
        upg_file = os.path.join(att_dir, 'upgrade.fw')
        fw_file = os.path.join(self.fw_dir, fw_name)

        if (is_windows()):
            cmd = [script_path + '/utils/windows/maker.exe', '-c',  maker_cfgfile, \
                   '-o', upg_file, '-mf']
        else:
            if ('32bit' == platform.architecture()[0]):
                # linux x86
                maker_path = script_path + '/utils/linux-x86/maker/PyMaker.pyo'
            else:
                # linux x86_64
                maker_path = script_path + '/utils/linux-x86_64/maker/PyMaker.pyo'
            cmd = ['python2', '-O', maker_path, '-c', maker_cfgfile, '-o', upg_file, '--mf', '1']

        (outmsg, exit_code) = run_cmd(cmd)
        if not os.path.exists(upg_file):
            panic('Maker error: %s' %(outmsg))

        self.build_atf_image(fw_file, att_dir, self.atf_file_list)

        print_notice('Build successfully!')

def main(argv):
    global ext_name, soc_name, board_name, encrypt_fw, efuse_bin

    parser = argparse.ArgumentParser(
        description='Build firmware',
    )
    parser.add_argument('-c', dest = 'fw_cfgfile')
    parser.add_argument('-e', dest = 'encrypt_fw')
    parser.add_argument('-ef', dest = 'efuse_bin')
    parser.add_argument('-b', dest = 'board_name')
    parser.add_argument('-s', dest = 'soc_name')
    parser.add_argument('-v', dest = 'fw_ver_file')
    parser.add_argument('-x', dest = 'ext_name')
    args = parser.parse_args();

    fw_cfgfile = args.fw_cfgfile
    encrypt_fw = args.encrypt_fw
    efuse_bin = args.efuse_bin
    board_name = args.board_name
    date_stamp = time.strftime('%y%m%d',time.localtime(time.time()))
    fw_prefix = board_name  + '_' + date_stamp
    soc_name = args.soc_name
    ext_name = args.ext_name

    if (not os.path.isfile(fw_cfgfile)):
        print('firmware config file is not exist')
        sys.exit(1)

    try:
        fw = firmware(fw_cfgfile)
        fw.crc_chunk_size = 32
        fw.parse_config()

        if (ext_name == "debug"):
            fw.generate_raw_image(fw_prefix + '_raw_debug.bin')
            fw.generate_earphone_firmware(fw_prefix + '_' + ext_name + '.fw')
        else:
 	        fw.generate_raw_image(fw_prefix + '_raw.bin')
 	        fw.generate_earphone_firmware(fw_prefix + '.fw')

 	        # build ota for firmware
 	        fw.fw_xml_file = os.path.join(fw.fw_dir, 'ota_firmware.xml')
 	        if os.path.exists(fw.fw_xml_file):
 	            fw.build_ota_image('ota_firmware.bin')
 	        # build ota for fonts
 	        fw.fw_xml_file = os.path.join(fw.fw_dir, 'ota_fonts.xml')
 	        if os.path.exists(fw.fw_xml_file):
 	            fw.build_ota_image('ota_fonts.bin')
 	        fw.fw_xml_file = os.path.join(fw.fw_dir, 'ota_full.xml')
 	        if os.path.exists(fw.fw_xml_file):
 	            fw.build_ota_image('ota_full.bin')
 	        # build ota for res
 	        fw.fw_xml_file = os.path.join(fw.fw_dir, 'ota_res.xml')
 	        if os.path.exists(fw.fw_xml_file):
 	            fw.build_ota_image('ota_res.bin')
 	        # build ota for res_b
 	        fw.fw_xml_file = os.path.join(fw.fw_dir, 'ota_res_b.xml')
 	        if os.path.exists(fw.fw_xml_file):
 	            fw.build_ota_image('ota_res_b.bin')
        
    except Exception as e:
        print('unknown exception, %s' %(str(e)))
        traceback.print_exc()
        sys.exit(2)

if __name__ == '__main__':
    main(sys.argv[1:])
