#!/usr/bin/python3

#
# Copyright 2018, The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

import argparse
import sys
import tempfile
import os
import logging
import subprocess
import xml.etree.ElementTree as ET

import EddParser
from PFWScriptGenerator import PfwScriptTranslator
import hostConfig

#
# In order to build the XML Settings file at build time, an instance of the parameter-framework
# shall be started and fed with all the criterion types/criteria that will be used by
# the engineconfigurable.
# This scripts allows generates the settings from the same audio_criterion_types.xml /
# audio_criteria.xml files used at run time by the engineconfigurable
#

def parseArgs():
    argparser = argparse.ArgumentParser(description="Parameter-Framework XML \
                                        Settings file generator.\n\
                                        Exit with the number of (recoverable or not) \
                                        error that occured.")
    argparser.add_argument('--domain-generator-tool',
                           help="ParameterFramework domain generator tool. Mandatory.",
                           metavar="PFW_DOMAIN_GENERATOR_TOOL",
                           required=True)
    argparser.add_argument('--toplevel-config',
                           help="Top-level parameter-framework configuration file. Mandatory.",
                           metavar="TOPLEVEL_CONFIG_FILE",
                           required=True)
    argparser.add_argument('--criteria',
                           help="Criteria file, in XML format: \
                                 in '<criteria> \
                                         <criterion name="" type=""/> \
                                     </criteria>' \
                           format. Mandatory.",
                           metavar="CRITERIA_FILE",
                           type=argparse.FileType('r'),
                           required=True)
    argparser.add_argument('--criteriontypes',
                           help="Criterion types XML file, in \
                           '<criterion_types> \
                               <criterion_type name="" type=<inclusive|exclusive> \
                                               values=<value1,value2,...>/> \
                            </criterion_types>' \
                           format. Mandatory.",
                           metavar="CRITERION_TYPE_FILE",
                           type=argparse.FileType('r'),
                           required=False)
    argparser.add_argument('--initial-settings',
                           help="Initial XML settings file (containing a \
                           <ConfigurableDomains>  tag",
                           nargs='?',
                           default=None,
                           metavar="XML_SETTINGS_FILE")
    argparser.add_argument('--add-domains',
                           help="List of single domain files (each containing a single \
                           <ConfigurableDomain> tag",
                           metavar="XML_DOMAIN_FILE",
                           nargs='*',
                           dest='xml_domain_files',
                           default=[])
    argparser.add_argument('--add-edds',
                           help="List of files in EDD syntax (aka \".pfw\" files)",
                           metavar="EDD_FILE",
                           type=argparse.FileType('r'),
                           nargs='*',
                           default=[],
                           dest='edd_files')
    argparser.add_argument('--schemas-dir',
                           help="Directory of parameter-framework XML Schemas for generation \
                           validation",
                           default=None)
    argparser.add_argument('--target-schemas-dir',
                           help="Ignored. Kept for retro-compatibility")
    argparser.add_argument('--validate',
                           help="Validate the settings against XML schemas",
                           action='store_true')
    argparser.add_argument('--verbose',
                           action='store_true')

    return argparser.parse_args()

#
# Parses audio_criterion_types.xml / audio_criteria.xml files used at run time by the
# engineconfigurable and outputs a dictionnary of criteria.
# For each criteria, the name, type (aka inclusive (bitfield) or exclusive (enum), the values
# are provided.
#
def parseCriteriaAndCriterionTypes(criteriaFile, criterionTypesFile):
    # Parse criteria and criterion types XML files
    #
    criteria_tree = ET.parse(criteriaFile)
    logging.info("Importing criteriaFile {}".format(criteriaFile))
    criterion_types_tree = ET.parse(criterionTypesFile)
    logging.info("Importing criterionTypesFile {}".format(criterionTypesFile))

    criteria_root = criteria_tree.getroot()

    all_criteria = []
    for criterion in criteria_root.findall('criterion'):
        criterion_name = criterion.get('name')
        type_name = criterion.get('type')
        logging.info("Importing criterion_name {}".format(criterion_name))
        logging.info("Importing type_name {}".format(type_name))

        for criterion_types in criterion_types_tree.findall('criterion_type'):
            criterion_type_name = criterion_types.get('name')
            if criterion_type_name == type_name:
                criterion_inclusiveness = criterion_types.get('type')

                criterion_values = []

                values_node = criterion_types.find('values')
                if values_node is not None:
                    for value in values_node.findall('value'):
                        criterion_values.append(value.get('literal'))

                if len(criterion_values) == 0:
                    criterion_values.append('')

                logging.info("Importing criterion_type_name {}".format(criterion_type_name))
                logging.info("Importing criterion_inclusiveness {}".format(criterion_inclusiveness))
                logging.info("Importing criterion_values {}".format(criterion_values))

                all_criteria.append({
                    "name" : criterion_name,
                    "inclusive" : criterion_inclusiveness,
                    "values" : criterion_values})
                break

    return all_criteria

#
# Parses the Edd files (aka .pfw extension file), which is a simplified language to write the
# parameter framework settings.
#
def parseEdd(EDDFiles):
    parsed_edds = []

    for edd_file in EDDFiles:
        try:
            root = EddParser.Parser().parse(edd_file)
        except EddParser.MySyntaxError as ex:
            logging.critical(str(ex))
            logging.info("EXIT ON FAILURE")
            exit(2)

        try:
            root.propagate()
        except EddParser.MyPropagationError as ex:
            logging.critical(str(ex))
            logging.info("EXIT ON FAILURE")
            exit(1)

        parsed_edds.append((edd_file.name, root))
    return parsed_edds

#
# Generates all the required commands to be sent to the instance of parameter-framework launched
# at runtime to generate the XML Settings file.
# It takes as input the collection of criteria, the domains and the simplified settings read from
# pfw.
#
def generateDomainCommands(logger, all_criteria, initial_settings, xml_domain_files, parsed_edds):
    # create and inject all the criteria
    logger.info("Creating all criteria")
    for criterion in all_criteria:
        yield ["createSelectionCriterion", criterion['inclusive'],
               criterion['name']] + criterion['values']

    yield ["start"]

    # Import initial settings file
    if initial_settings:
        logger.info("Importing initial settings file {}".format(initial_settings))
        yield ["importDomainsWithSettingsXML", initial_settings]

    # Import each standalone domain files
    for domain_file in xml_domain_files:
        logger.info("Importing single domain file {}".format(domain_file))
        yield ["importDomainWithSettingsXML", domain_file]

    # Generate the script for each EDD file
    for filename, parsed_edd in parsed_edds:
        logger.info("Translating and injecting EDD file {}".format(filename))
        translator = PfwScriptTranslator()
        parsed_edd.translate(translator)
        for command in translator.getScript():
            yield command

#
# Entry point of the domain generator.
#       -Parses Criterion types and criteria files
#       -Parses settings written in simplified pfw language.
#       -Launches a parameter-framework
#       -Translates the settings into command that can be interpreted by parameter-framework.
#       -Use the exports command and output them in XML Settings file.
#
def main():
    logging.root.setLevel(logging.INFO)
    args = parseArgs()

    all_criteria = parseCriteriaAndCriterionTypes(args.criteria, args.criteriontypes)

    #
    # EDD files (aka ".pfw" files)
    #
    parsed_edds = parseEdd(args.edd_files)

    # We need to modify the toplevel configuration file to account for differences
    # between development setup and target (installation) setup, in particular, the
    # TuningMwith ode must be enforced, regardless of what will be allowed on the target
    fake_toplevel_config = tempfile.NamedTemporaryFile(mode='w', delete=False, suffix=".xml",
                                                       prefix="TMPdomainGeneratorPFConfig_")

    install_path = os.path.dirname(os.path.realpath(args.toplevel_config))
    hostConfig.configure(infile=args.toplevel_config,
                         outfile=fake_toplevel_config,
                         structPath=install_path)
    fake_toplevel_config.close()

    # Create the connector. Pipe its input to us in order to write commands;
    # connect its output to stdout in order to have it dump the domains
    # there; connect its error output to stderr.
    connector = subprocess.Popen([args.domain_generator_tool,
                                  fake_toplevel_config.name,
                                  'verbose' if args.verbose else 'no-verbose',
                                  'validate' if args.validate else 'no-validate',
                                  args.schemas_dir],
                                 stdout=sys.stdout, stdin=subprocess.PIPE, stderr=sys.stderr)

    initial_settings = None
    if args.initial_settings:
        initial_settings = os.path.realpath(args.initial_settings)

    for command in generateDomainCommands(logging, all_criteria, initial_settings,
                                          args.xml_domain_files, parsed_edds):
        connector.stdin.write('\0'.join(command).encode('utf-8'))
        connector.stdin.write("\n".encode('utf-8'))

    # Closing the connector's input triggers the domain generation
    connector.stdin.close()
    connector.wait()
    os.remove(fake_toplevel_config.name)
    return connector.returncode

# If this file is directly executed
if __name__ == "__main__":
    sys.exit(main())
