#!/usr/bin/env python
## @ FspDscBsf2Yaml.py
# This script convert DSC or BSF format file into YAML format
#
# Copyright(c) 2021, Intel Corporation. All rights reserved.<BR>
# SPDX-License-Identifier: BSD-2-Clause-Patent
#
##

import os
import re
import sys
from datetime import date
from collections import OrderedDict
from functools import reduce

from GenCfgOpt import CGenCfgOpt

__copyright_tmp__ = """## @file
#
#  YAML CFGDATA %s File.
#
#  Copyright(c) %4d, Intel Corporation. All rights reserved.<BR>
#  SPDX-License-Identifier: BSD-2-Clause-Patent
#
##
"""

__copyright_dsc__ = """## @file
#
#  Copyright (c) %04d, Intel Corporation. All rights reserved.<BR>
#  SPDX-License-Identifier: BSD-2-Clause-Patent
#
##

[PcdsDynamicVpd.Upd]
  #
  # Global definitions in BSF
  # !BSF BLOCK:{NAME:"FSP UPD Configuration", VER:"0.1"}
  #

"""


def Bytes2Val(Bytes):
    return reduce(lambda x, y: (x << 8) | y, Bytes[::-1])


def Str2Bytes(Value, Blen):
    Result = bytearray(Value[1:-1], 'utf-8')  # Excluding quotes
    if len(Result) < Blen:
        Result.extend(b'\x00' * (Blen - len(Result)))
    return Result


class CFspBsf2Dsc:

    def __init__(self, bsf_file):
        self.cfg_list = CFspBsf2Dsc.parse_bsf(bsf_file)

    def get_dsc_lines(self):
        return CFspBsf2Dsc.generate_dsc(self.cfg_list)

    def save_dsc(self, dsc_file):
        return CFspBsf2Dsc.generate_dsc(self.cfg_list, dsc_file)

    @staticmethod
    def parse_bsf(bsf_file):

        fd = open(bsf_file, 'r')
        bsf_txt = fd.read()
        fd.close()

        find_list = []
        regex = re.compile(r'\s+Find\s+"(.*?)"(.*?)^\s+\$(.*?)\s+', re.S | re.MULTILINE)
        for match in regex.finditer(bsf_txt):
            find = match.group(1)
            name = match.group(3)
            if not name.endswith('_Revision'):
                raise Exception("Unexpected CFG item following 'Find' !")
            find_list.append((name, find))

        idx = 0
        count = 0
        prefix = ''
        chk_dict = {}
        cfg_list = []
        cfg_temp = {'find': '', 'cname': '', 'length': 0, 'value': '0', 'type': 'Reserved',
                    'embed': '', 'page': '', 'option': '', 'instance': 0}
        regex = re.compile(r'^\s+(\$(.*?)|Skip)\s+(\d+)\s+bytes(\s+\$_DEFAULT_\s+=\s+(.+?))?$',
                           re.S | re.MULTILINE)

        for match in regex.finditer(bsf_txt):
            dlen = int(match.group(3))
            if match.group(1) == 'Skip':
                key = 'gPlatformFspPkgTokenSpaceGuid_BsfSkip%d' % idx
                val = ', '.join(['%02X' % ord(i) for i in '\x00' * dlen])
                idx += 1
                option = '$SKIP'
            else:
                key = match.group(2)
                val = match.group(5)
                option = ''

            cfg_item = dict(cfg_temp)
            finds = [i for i in find_list if i[0] == key]
            if len(finds) > 0:
                if count >= 1:
                    # Append a dummy one
                    cfg_item['cname'] = 'Dummy'
                    cfg_list.append(dict(cfg_item))
                    cfg_list[-1]['embed'] = '%s:TAG_%03X:END' % (prefix, ord(prefix[-1]))
                prefix = finds[0][1]
                cfg_item['embed'] = '%s:TAG_%03X:START' % (prefix, ord(prefix[-1]))
                cfg_item['find'] = prefix
                cfg_item['cname'] = 'Signature'
                cfg_item['length'] = len(finds[0][1])
                str2byte = Str2Bytes("'" + finds[0][1] + "'", len(finds[0][1]))
                cfg_item['value'] = '0x%X' % Bytes2Val(str2byte)
                cfg_list.append(dict(cfg_item))
                cfg_item = dict(cfg_temp)
                find_list.pop(0)
                count = 0

            cfg_item['cname'] = key
            cfg_item['length'] = dlen
            cfg_item['value'] = val
            cfg_item['option'] = option

            if key not in chk_dict.keys():
                chk_dict[key] = 0
            else:
                chk_dict[key] += 1
            cfg_item['instance'] = chk_dict[key]

            cfg_list.append(cfg_item)
            count += 1

        if prefix:
            cfg_item = dict(cfg_temp)
            cfg_item['cname'] = 'Dummy'
            cfg_item['embed'] = '%s:%03X:END' % (prefix, ord(prefix[-1]))
            cfg_list.append(cfg_item)

        option_dict = {}
        selreg = re.compile(r'\s+Selection\s*(.+?)\s*,\s*"(.*?)"$', re.S | re.MULTILINE)
        regex = re.compile(r'^List\s&(.+?)$(.+?)^EndList$', re.S | re.MULTILINE)
        for match in regex.finditer(bsf_txt):
            key = match.group(1)
            option_dict[key] = []
            for select in selreg.finditer(match.group(2)):
                option_dict[key].append((int(select.group(1), 0), select.group(2)))

        chk_dict = {}
        pagereg = re.compile(r'^Page\s"(.*?)"$(.+?)^EndPage$', re.S | re.MULTILINE)
        for match in pagereg.finditer(bsf_txt):
            page = match.group(1)
            for line in match.group(2).splitlines():
                match = re.match(r'\s+(Combo|EditNum)\s\$(.+?),\s"(.*?)",\s(.+?),$', line)
                if match:
                    cname = match.group(2)
                    if cname not in chk_dict.keys():
                        chk_dict[cname] = 0
                    else:
                        chk_dict[cname] += 1
                    instance = chk_dict[cname]
                    cfg_idxs = [i for i, j in enumerate(cfg_list) if j['cname'] == cname and j['instance'] == instance]
                    if len(cfg_idxs) != 1:
                        raise Exception("Multiple CFG item '%s' found !" % cname)
                    cfg_item = cfg_list[cfg_idxs[0]]
                    cfg_item['page'] = page
                    cfg_item['type'] = match.group(1)
                    cfg_item['prompt'] = match.group(3)
                    cfg_item['range'] = None
                    if cfg_item['type'] == 'Combo':
                        cfg_item['option'] = option_dict[match.group(4)[1:]]
                    elif cfg_item['type'] == 'EditNum':
                        cfg_item['option'] = match.group(4)
                match = re.match(r'\s+ Help\s"(.*?)"$', line)
                if match:
                    cfg_item['help'] = match.group(1)

                match = re.match(r'\s+"Valid\srange:\s(.*)"$', line)
                if match:
                    parts = match.group(1).split()
                    cfg_item['option'] = (
                        (int(parts[0], 0), int(parts[2], 0), cfg_item['option']))

        return cfg_list

    @staticmethod
    def generate_dsc(option_list, dsc_file=None):
        dsc_lines = []
        header = '%s' % (__copyright_dsc__ % date.today().year)
        dsc_lines.extend(header.splitlines())

        pages = []
        for cfg_item in option_list:
            if cfg_item['page'] and (cfg_item['page'] not in pages):
                pages.append(cfg_item['page'])

        page_id = 0
        for page in pages:
            dsc_lines.append('  # !BSF PAGES:{PG%02X::"%s"}' % (page_id, page))
            page_id += 1
        dsc_lines.append('')

        last_page = ''
        for option in option_list:
            dsc_lines.append('')
            default = option['value']
            pos = option['cname'].find('_')
            name = option['cname'][pos + 1:]

            if option['find']:
                dsc_lines.append('  # !BSF FIND:{%s}' % option['find'])
                dsc_lines.append('')

            if option['instance'] > 0:
                name = name + '_%s' % option['instance']

            if option['embed']:
                dsc_lines.append('  # !HDR EMBED:{%s}' % option['embed'])

            if option['type'] == 'Reserved':
                dsc_lines.append('  # !BSF NAME:{Reserved} TYPE:{Reserved}')
                if option['option'] == '$SKIP':
                    dsc_lines.append('  # !BSF OPTION:{$SKIP}')
            else:
                prompt = option['prompt']

                if last_page != option['page']:
                    last_page = option['page']
                    dsc_lines.append('  # !BSF PAGE:{PG%02X}' % (pages.index(option['page'])))

                if option['type'] == 'Combo':
                    dsc_lines.append('  # !BSF NAME:{%s} TYPE:{%s}' %
                                     (prompt, option['type']))
                    ops = []
                    for val, text in option['option']:
                        ops.append('0x%x:%s' % (val, text))
                    dsc_lines.append('  # !BSF OPTION:{%s}' % (', '.join(ops)))
                elif option['type'] == 'EditNum':
                    cfg_len = option['length']
                    if ',' in default and cfg_len > 8:
                        dsc_lines.append('  # !BSF NAME:{%s} TYPE:{Table}' % (prompt))
                        if cfg_len > 16:
                            cfg_len = 16
                        ops = []
                        for i in range(cfg_len):
                            ops.append('%X:1:HEX' % i)
                        dsc_lines.append('  # !BSF OPTION:{%s}' % (', '.join(ops)))
                    else:
                        dsc_lines.append(
                            '  # !BSF NAME:{%s} TYPE:{%s, %s,(0x%X, 0x%X)}' %
                            (prompt, option['type'], option['option'][2],
                             option['option'][0], option['option'][1]))
                dsc_lines.append('  # !BSF HELP:{%s}' % option['help'])

            if ',' in default:
                default = '{%s}' % default
            dsc_lines.append('  gCfgData.%-30s | * | 0x%04X | %s' %
                             (name, option['length'], default))

        if dsc_file:
            fd = open(dsc_file, 'w')
            fd.write('\n'.join(dsc_lines))
            fd.close()

        return dsc_lines


class CFspDsc2Yaml():

    def __init__(self):
        self._Hdr_key_list = ['EMBED', 'STRUCT']
        self._Bsf_key_list = ['NAME', 'HELP', 'TYPE', 'PAGE', 'PAGES', 'OPTION',
                              'CONDITION', 'ORDER', 'MARKER', 'SUBT', 'FIELD', 'FIND']
        self.gen_cfg_data = None
        self.cfg_reg_exp = re.compile(r"^([_a-zA-Z0-9$\(\)]+)\s*\|\s*(0x[0-9A-F]+|\*)\s*\|"
                                      + r"\s*(\d+|0x[0-9a-fA-F]+)\s*\|\s*(.+)")
        self.bsf_reg_exp = re.compile(r"(%s):{(.+?)}(?:$|\s+)" % '|'.join(self._Bsf_key_list))
        self.hdr_reg_exp = re.compile(r"(%s):{(.+?)}" % '|'.join(self._Hdr_key_list))
        self.prefix = ''
        self.unused_idx = 0
        self.offset = 0
        self.base_offset = 0

    def load_config_data_from_dsc(self, file_name):
        """
        Load and parse a DSC CFGDATA file.
        """
        gen_cfg_data = CGenCfgOpt('FSP')
        if file_name.endswith('.dsc'):
            # if gen_cfg_data.ParseDscFileYaml(file_name, '') != 0:
            if gen_cfg_data.ParseDscFile(file_name, '') != 0:
                raise Exception('DSC file parsing error !')
            if gen_cfg_data.CreateVarDict() != 0:
                raise Exception('DSC variable creation error !')
        else:
            raise Exception('Unsupported file "%s" !' % file_name)
        self.gen_cfg_data = gen_cfg_data

    def print_dsc_line(self):
        """
        Debug function to print all DSC lines.
        """
        for line in self.gen_cfg_data._DscLines:
            print(line)

    def format_value(self, field, text, indent=''):
        """
        Format a CFGDATA item into YAML format.
        """
        if(not text.startswith('!expand')) and (': ' in text):
            tgt = ':' if field == 'option' else '- '
            text = text.replace(': ', tgt)
        lines = text.splitlines()
        if len(lines) == 1 and field != 'help':
            return text
        else:
            return '>\n   ' + '\n   '.join([indent + i.lstrip() for i in lines])

    def reformat_pages(self, val):
        # Convert XXX:YYY into XXX::YYY format for page definition
        parts = val.split(',')
        if len(parts) <= 1:
            return val

        new_val = []
        for each in parts:
            nodes = each.split(':')
            if len(nodes) == 2:
                each = '%s::%s' % (nodes[0], nodes[1])
            new_val.append(each)
        ret = ','.join(new_val)
        return ret

    def reformat_struct_value(self, utype, val):
        # Convert DSC UINT16/32/64 array into new format by
        # adding prefix 0:0[WDQ] to provide hint to the array format
        if utype in ['UINT16', 'UINT32', 'UINT64']:
            if val and val[0] == '{' and val[-1] == '}':
                if utype == 'UINT16':
                    unit = 'W'
                elif utype == 'UINT32':
                    unit = 'D'
                else:
                    unit = 'Q'
                val = '{ 0:0%s, %s }' % (unit, val[1:-1])
        return val

    def process_config(self, cfg):
        if 'page' in cfg:
            cfg['page'] = self.reformat_pages(cfg['page'])

        if 'struct' in cfg:
            cfg['value'] = self.reformat_struct_value(cfg['struct'], cfg['value'])

    def parse_dsc_line(self, dsc_line, config_dict, init_dict, include):
        """
        Parse a line in DSC and update the config dictionary accordingly.
        """
        init_dict.clear()
        match = re.match(r'g(CfgData|\w+FspPkgTokenSpaceGuid)\.(.+)', dsc_line)
        if match:
            match = self.cfg_reg_exp.match(match.group(2))
            if not match:
                return False
            config_dict['cname'] = self.prefix + match.group(1)
            value = match.group(4).strip()
            length = match.group(3).strip()
            config_dict['length'] = length
            config_dict['value'] = value
            if match.group(2) == '*':
                self.offset += int(length, 0)
            else:
                org_offset = int(match.group(2), 0)
                if org_offset == 0:
                    self.base_offset = self.offset
                offset = org_offset + self.base_offset
                if self.offset != offset:
                    if offset > self.offset:
                        init_dict['padding'] = offset - self.offset
                self.offset = offset + int(length, 0)
            return True

        match = re.match(r"^\s*#\s+!([<>])\s+include\s+(.+)", dsc_line)
        if match and len(config_dict) == 0:
            # !include should not be inside a config field
            # if so, do not convert include into YAML
            init_dict = dict(config_dict)
            config_dict.clear()
            config_dict['cname'] = '$ACTION'
            if match.group(1) == '<':
                config_dict['include'] = match.group(2)
            else:
                config_dict['include'] = ''
            return True

        match = re.match(r"^\s*#\s+(!BSF|!HDR)\s+(.+)", dsc_line)
        if not match:
            return False

        remaining = match.group(2)
        if match.group(1) == '!BSF':
            result = self.bsf_reg_exp.findall(remaining)
            if not result:
                return False

            for each in result:
                key = each[0].lower()
                val = each[1]
                if key == 'field':
                    name = each[1]
                    if ':' not in name:
                        raise Exception('Incorrect bit field format !')
                    parts = name.split(':')
                    config_dict['length'] = parts[1]
                    config_dict['cname'] = '@' + parts[0]
                    return True
                elif key in ['pages', 'page', 'find']:
                    init_dict = dict(config_dict)
                    config_dict.clear()
                    config_dict['cname'] = '$ACTION'
                    if key == 'find':
                        config_dict['find'] = val
                    else:
                        config_dict['page'] = val
                    return True
                elif key == 'subt':
                    config_dict.clear()
                    parts = each[1].split(':')
                    tmp_name = parts[0][:-5]
                    if tmp_name == 'CFGHDR':
                        cfg_tag = '_$FFF_'
                        sval = '!expand { %s_TMPL : [ ' % tmp_name + '%s, %s, ' % (parts[1], cfg_tag) \
                               + ', '.join(parts[2:]) + ' ] }'
                    else:
                        sval = '!expand { %s_TMPL : [ ' % tmp_name + ', '.join(parts[1:]) + ' ] }'
                    config_dict.clear()
                    config_dict['cname'] = tmp_name
                    config_dict['expand'] = sval
                    return True
                else:
                    if key in ['name', 'help', 'option'] and val.startswith('+'):
                        val = config_dict[key] + '\n' + val[1:]
                    if val.strip() == '':
                        val = "''"
                    config_dict[key] = val

        else:
            match = self.hdr_reg_exp.match(remaining)
            if not match:
                return False
            key = match.group(1)
            remaining = match.group(2)
            if key == 'EMBED':
                parts = remaining.split(':')
                names = parts[0].split(',')
                if parts[-1] == 'END':
                    prefix = '>'
                else:
                    prefix = '<'
                skip = False
                if parts[1].startswith('TAG_'):
                    tag_txt = '%s:%s' % (names[0], parts[1])
                else:
                    tag_txt = names[0]
                    if parts[2] in ['START', 'END']:
                        if names[0] == 'PCIE_RP_PIN_CTRL[]':
                            skip = True
                        else:
                            tag_txt = '%s:%s' % (names[0], parts[1])
                if not skip:
                    config_dict.clear()
                    config_dict['cname'] = prefix + tag_txt
                    return True

            if key == 'STRUCT':
                text = remaining.strip()
                config_dict[key.lower()] = text

        return False

    def process_template_lines(self, lines):
        """
        Process a line in DSC template section.
        """
        template_name = ''
        bsf_temp_dict = OrderedDict()
        temp_file_dict = OrderedDict()
        include_file = ['.']

        for line in lines:
            match = re.match(r"^\s*#\s+!([<>])\s+include\s+(.+)", line)
            if match:
                if match.group(1) == '<':
                    include_file.append(match.group(2))
                else:
                    include_file.pop()

            match = re.match(r"^\s*#\s+(!BSF)\s+DEFT:{(.+?):(START|END)}", line)
            if match:
                if match.group(3) == 'START' and not template_name:
                    template_name = match.group(2).strip()
                    temp_file_dict[template_name] = list(include_file)
                    bsf_temp_dict[template_name] = []
                if match.group(3) == 'END' and (template_name == match.group(2).strip()) \
                   and template_name:
                    template_name = ''
            else:
                if template_name:
                    bsf_temp_dict[template_name].append(line)
        return bsf_temp_dict, temp_file_dict

    def process_option_lines(self, lines):
        """
        Process a line in DSC config section.
        """
        cfgs = []
        struct_end = False
        config_dict = dict()
        init_dict = dict()
        include = ['']
        for line in lines:
            ret = self.parse_dsc_line(line, config_dict, init_dict, include)
            if ret:
                if 'padding' in init_dict:
                    num = init_dict['padding']
                    init_dict.clear()
                    padding_dict = {}
                    cfgs.append(padding_dict)
                    padding_dict['cname'] = 'UnusedUpdSpace%d' % self.unused_idx
                    padding_dict['length'] = '0x%x' % num
                    padding_dict['value'] = '{ 0 }'
                    self.unused_idx += 1

                if cfgs and cfgs[-1]['cname'][0] != '@' and config_dict['cname'][0] == '@':
                    # it is a bit field, mark the previous one as virtual
                    cname = cfgs[-1]['cname']
                    new_cfg = dict(cfgs[-1])
                    new_cfg['cname'] = '@$STRUCT'
                    cfgs[-1].clear()
                    cfgs[-1]['cname'] = cname
                    cfgs.append(new_cfg)

                if cfgs and cfgs[-1]['cname'] == 'CFGHDR' and config_dict['cname'][0] == '<':
                    # swap CfgHeader and the CFG_DATA order
                    if ':' in config_dict['cname']:
                        # replace the real TAG for CFG_DATA
                        cfgs[-1]['expand'] = cfgs[-1]['expand'].replace(
                            '_$FFF_', '0x%s' %
                            config_dict['cname'].split(':')[1][4:])
                    cfgs.insert(-1, config_dict)
                else:
                    self.process_config(config_dict)
                    if struct_end:
                        struct_end = False
                        cfgs.insert(-1, config_dict)
                    else:
                        cfgs.append(config_dict)
                        if config_dict['cname'][0] == '>':
                            struct_end = True

                config_dict = dict(init_dict)
        return cfgs

    def variable_fixup(self, each):
        """
        Fix up some variable definitions for SBL.
        """
        key = each
        val = self.gen_cfg_data._MacroDict[each]
        return key, val

    def template_fixup(self, tmp_name, tmp_list):
        """
        Fix up some special config templates for SBL
        """
        return

    def config_fixup(self, cfg_list):
        """
        Fix up some special config items for SBL.
        """

        # Insert FSPT_UPD/FSPM_UPD/FSPS_UPD tag so as to create C strcture
        idxs = []
        for idx, cfg in enumerate(cfg_list):
            if cfg['cname'].startswith('<FSP_UPD_HEADER'):
                idxs.append(idx)

        if len(idxs) != 3:
            return

        # Handle insert backwards so that the index does not change in the loop
        fsp_comp = 'SMT'
        idx_comp = 0
        for idx in idxs[::-1]:
            # Add current FSP?_UPD start tag
            cfgfig_dict = {}
            cfgfig_dict['cname'] = '<FSP%s_UPD' % fsp_comp[idx_comp]
            cfg_list.insert(idx, cfgfig_dict)
            if idx_comp < 2:
                # Add previous FSP?_UPD end tag
                cfgfig_dict = {}
                cfgfig_dict['cname'] = '>FSP%s_UPD' % fsp_comp[idx_comp + 1]
                cfg_list.insert(idx, cfgfig_dict)
            idx_comp += 1

        # Add final FSPS_UPD end tag
        cfgfig_dict = {}
        cfgfig_dict['cname'] = '>FSP%s_UPD' % fsp_comp[0]
        cfg_list.append(cfgfig_dict)

        return

    def get_section_range(self, section_name):
        """
        Extract line number range from config file for a given section name.
        """
        start = -1
        end = -1
        for idx, line in enumerate(self.gen_cfg_data._DscLines):
            if start < 0 and line.startswith('[%s]' % section_name):
                start = idx
            elif start >= 0 and line.startswith('['):
                end = idx
                break
        if start == -1:
            start = 0
        if end == -1:
            end = len(self.gen_cfg_data._DscLines)
        return start, end

    def normalize_file_name(self, file, is_temp=False):
        """
        Normalize file name convention so that it is consistent.
        """
        if file.endswith('.dsc'):
            file = file[:-4] + '.yaml'
        dir_name = os.path.dirname(file)
        base_name = os.path.basename(file)
        if is_temp:
            if 'Template_' not in file:
                base_name = base_name.replace('Template', 'Template_')
        else:
            if 'CfgData_' not in file:
                base_name = base_name.replace('CfgData', 'CfgData_')
        if dir_name:
            path = dir_name + '/' + base_name
        else:
            path = base_name
        return path

    def output_variable(self):
        """
        Output variable block into a line list.
        """
        lines = []
        for each in self.gen_cfg_data._MacroDict:
            key, value = self.variable_fixup(each)
            lines.append('%-30s : %s' % (key, value))
        return lines

    def output_template(self):
        """
        Output template block into a line list.
        """
        self.offset = 0
        self.base_offset = 0
        start, end = self.get_section_range('PcdsDynamicVpd.Tmp')
        bsf_temp_dict, temp_file_dict = self.process_template_lines(self.gen_cfg_data._DscLines[start:end])
        template_dict = dict()
        lines = []
        file_lines = {}
        last_file = '.'
        file_lines[last_file] = []

        for tmp_name in temp_file_dict:
            temp_file_dict[tmp_name][-1] = self.normalize_file_name(temp_file_dict[tmp_name][-1], True)
            if len(temp_file_dict[tmp_name]) > 1:
                temp_file_dict[tmp_name][-2] = self.normalize_file_name(temp_file_dict[tmp_name][-2], True)

        for tmp_name in bsf_temp_dict:
            file = temp_file_dict[tmp_name][-1]
            if last_file != file and len(temp_file_dict[tmp_name]) > 1:
                inc_file = temp_file_dict[tmp_name][-2]
                file_lines[inc_file].extend(['', '- !include %s' % temp_file_dict[tmp_name][-1], ''])
            last_file = file
            if file not in file_lines:
                file_lines[file] = []
            lines = file_lines[file]
            text = bsf_temp_dict[tmp_name]
            tmp_list = self.process_option_lines(text)
            self.template_fixup(tmp_name, tmp_list)
            template_dict[tmp_name] = tmp_list
            lines.append('%s: >' % tmp_name)
            lines.extend(self.output_dict(tmp_list, False)['.'])
            lines.append('\n')
        return file_lines

    def output_config(self):
        """
        Output config block into a line list.
        """
        self.offset = 0
        self.base_offset = 0
        start, end = self.get_section_range('PcdsDynamicVpd.Upd')
        cfgs = self.process_option_lines(self.gen_cfg_data._DscLines[start:end])
        self.config_fixup(cfgs)
        file_lines = self.output_dict(cfgs, True)
        return file_lines

    def output_dict(self, cfgs, is_configs):
        """
        Output one config item into a line list.
        """
        file_lines = {}
        level = 0
        file = '.'
        for each in cfgs:
            if 'length' in each and int(each['length'], 0) == 0:
                continue

            if 'include' in each:
                if each['include']:
                    each['include'] = self.normalize_file_name(each['include'])
                    file_lines[file].extend(['', '- !include %s' % each['include'], ''])
                    file = each['include']
                else:
                    file = '.'
                continue

            if file not in file_lines:
                file_lines[file] = []

            lines = file_lines[file]
            name = each['cname']

            prefix = name[0]
            if prefix == '<':
                level += 1

            padding = '  ' * level
            if prefix not in '<>@':
                padding += '  '
            else:
                name = name[1:]
                if prefix == '@':
                    padding += '    '

            if ':' in name:
                parts = name.split(':')
                name = parts[0]

            padding = padding[2:] if is_configs else padding

            if prefix != '>':
                if 'expand' in each:
                    lines.append('%s- %s' % (padding, each['expand']))
                else:
                    lines.append('%s- %-12s :' % (padding, name))

            for field in each:
                if field in ['cname', 'expand', 'include']:
                    continue
                value_str = self.format_value(field, each[field], padding + ' ' * 16)
                full_line = '  %s  %-12s : %s' % (padding, field, value_str)
                lines.extend(full_line.splitlines())

            if prefix == '>':
                level -= 1
                if level == 0:
                    lines.append('')

        return file_lines


def bsf_to_dsc(bsf_file, dsc_file):
    fsp_dsc = CFspBsf2Dsc(bsf_file)
    dsc_lines = fsp_dsc.get_dsc_lines()
    fd = open(dsc_file, 'w')
    fd.write('\n'.join(dsc_lines))
    fd.close()
    return


def dsc_to_yaml(dsc_file, yaml_file):
    dsc2yaml = CFspDsc2Yaml()
    dsc2yaml.load_config_data_from_dsc(dsc_file)

    cfgs = {}
    for cfg in ['Template', 'Option']:
        if cfg == 'Template':
            file_lines = dsc2yaml.output_template()
        else:
            file_lines = dsc2yaml.output_config()
        for file in file_lines:
            lines = file_lines[file]
            if file == '.':
                cfgs[cfg] = lines
            else:
                if('/' in file or '\\' in file):
                    continue
                file = os.path.basename(file)
                fo = open(os.path.join(file), 'w')
                fo.write(__copyright_tmp__ % (cfg, date.today().year) + '\n\n')
                for line in lines:
                    fo.write(line + '\n')
                fo.close()

    variables = dsc2yaml.output_variable()
    fo = open(yaml_file, 'w')
    fo.write(__copyright_tmp__ % ('Default', date.today().year))
    if len(variables) > 0:
        fo.write('\n\nvariable:\n')
        for line in variables:
            fo.write('  ' + line + '\n')

    fo.write('\n\ntemplate:\n')
    for line in cfgs['Template']:
        if line != '':
            fo.write('  ' + line + '\n')

    fo.write('\n\nconfigs:\n')
    for line in cfgs['Option']:
        if line != '':
            fo.write('  ' + line + '\n')

    fo.close()


def get_fsp_name_from_path(bsf_file):
    name = ''
    parts = bsf_file.split(os.sep)
    for part in parts:
        if part.endswith('FspBinPkg'):
            name = part[:-9]
            break
    if not name:
        raise Exception('Could not get FSP name from file path!')
    return name


def usage():
    print('\n'.join([
          "FspDscBsf2Yaml Version 0.10",
          "Usage:",
          "    FspDscBsf2Yaml  BsfFile|DscFile  YamlFile"
          ]))


def main():
    #
    # Parse the options and args
    #
    argc = len(sys.argv)
    if argc < 3:
        usage()
        return 1

    bsf_file = sys.argv[1]
    yaml_file = sys.argv[2]
    if os.path.isdir(yaml_file):
        yaml_file = os.path.join(yaml_file, get_fsp_name_from_path(bsf_file) + '.yaml')

    if bsf_file.endswith('.dsc'):
        dsc_file = bsf_file
        bsf_file = ''
    else:
        dsc_file = os.path.splitext(yaml_file)[0] + '.dsc'
        bsf_to_dsc(bsf_file, dsc_file)

    dsc_to_yaml(dsc_file, yaml_file)

    print("'%s' was created successfully!" % yaml_file)

    return 0


if __name__ == '__main__':
    sys.exit(main())
