#!/usr/bin/python3

# Copyright (C) 2022 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.
#
"""A script to generate Java files and CPP header files based on annotations in VehicleProperty.aidl

   Need ANDROID_BUILD_TOP environmental variable to be set. This script will update
   ChangeModeForVehicleProperty.h and AccessForVehicleProperty.h under generated_lib/version/cpp and
   ChangeModeForVehicleProperty.java, AccessForVehicleProperty.java, EnumForVehicleProperty.java
   UnitsForVehicleProperty.java under generated_lib/version/java.

   Usage:
   $ python generate_annotation_enums.py
"""
import argparse
import filecmp
import os
import re
import sys
import tempfile

# Keep this updated with the latest in-development property version.
PROPERTY_VERSION = '4'

PROP_AIDL_FILE_PATH = ('hardware/interfaces/automotive/vehicle/aidl_property/android/hardware/' +
    'automotive/vehicle/VehicleProperty.aidl')
GENERATED_LIB = ('hardware/interfaces/automotive/vehicle/aidl/generated_lib/' + PROPERTY_VERSION +
        '/')
CHANGE_MODE_CPP_FILE_PATH = GENERATED_LIB + '/cpp/ChangeModeForVehicleProperty.h'
ACCESS_CPP_FILE_PATH = GENERATED_LIB + '/cpp/AccessForVehicleProperty.h'
CHANGE_MODE_JAVA_FILE_PATH = GENERATED_LIB + '/java/ChangeModeForVehicleProperty.java'
ACCESS_JAVA_FILE_PATH = GENERATED_LIB + '/java/AccessForVehicleProperty.java'
ENUM_CPP_FILE_PATH = GENERATED_LIB + '/cpp/EnumForVehicleProperty.h'
ENUM_JAVA_FILE_PATH = GENERATED_LIB + '/java/EnumForVehicleProperty.java'
UNITS_JAVA_FILE_PATH = GENERATED_LIB + '/java/UnitsForVehicleProperty.java'
VERSION_CPP_FILE_PATH = GENERATED_LIB + '/cpp/VersionForVehicleProperty.h'
ANNOTATIONS_CPP_FILE_PATH = GENERATED_LIB + '/cpp/AnnotationsForVehicleProperty.h'
ANNOTATIONS_JAVA_FILE_PATH = GENERATED_LIB + '/java/AnnotationsForVehicleProperty.java'
SCRIPT_PATH = 'hardware/interfaces/automotive/vehicle/tools/generate_annotation_enums.py'

TAB = '    '
RE_ENUM_START = re.compile(r'\s*enum VehicleProperty \{')
RE_ENUM_END = re.compile(r'\s*\}\;')
RE_COMMENT_BEGIN = re.compile(r'\s*\/\*\*?')
RE_COMMENT_END = re.compile(r'\s*\*\/')
RE_CHANGE_MODE = re.compile(r'\s*\* @change_mode (\S+)\s*')
RE_VERSION = re.compile(r'\s*\* @version (\S+)\s*')
RE_ACCESS = re.compile(r'\s*\* @access (\S+)\s*')
RE_DATA_ENUM = re.compile(r'\s*\* @data_enum (\S+)\s*')
RE_UNIT = re.compile(r'\s*\* @unit (\S+)\s+')
RE_VALUE = re.compile(r'\s*(\w+)\s*=(.*)')
RE_ANNOTATION = re.compile(r'\s*\* @(\S+)\s*')

SUPPORTED_ANNOTATIONS = ['change_mode', 'access', 'unit', 'data_enum', 'data_enum_bit_flags',
    'version', 'require_min_max_supported_value', 'require_supported_values_list',
    'legacy_supported_values_in_config']

# Non static data_enum properties that do not require supported values list.
# These properties are either deprecated or for internal use only.
ENUM_PROPERTIES_WITHOUT_SUPPORTED_VALUES = [
    # deprecated
    'TURN_SIGNAL_STATE',
    # The supported values are exposed through HVAC_FAN_DIRECTION_AVAILABLE
    'HVAC_FAN_DIRECTION',
    # Internal use only
    'HW_ROTARY_INPUT',
    # Internal use only
    'HW_CUSTOM_INPUT',
    # Internal use only
    'SHUTDOWN_REQUEST',
    # Internal use only
    'CAMERA_SERVICE_CURRENT_STATE'
]

LICENSE = """/*
 * Copyright (C) 2025 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.
 */

/**
 * DO NOT EDIT MANUALLY!!!
 *
 * Generated by tools/generate_annotation_enums.py.
 */

// clang-format off

"""

CHANGE_MODE_CPP_FORMATTER = """#pragma once

#include <aidl/android/hardware/automotive/vehicle/VehicleProperty.h>
#include <aidl/android/hardware/automotive/vehicle/VehiclePropertyChangeMode.h>

#include <unordered_map>

namespace aidl::android::hardware::automotive::vehicle {{
std::unordered_map<VehicleProperty, VehiclePropertyChangeMode> ChangeModeForVehicleProperty = {{
{0}
}};
}}  // aidl::android::hardware::automotive::vehicle
"""

ACCESS_CPP_FORMATTER = """#pragma once

#include <aidl/android/hardware/automotive/vehicle/VehicleProperty.h>
#include <aidl/android/hardware/automotive/vehicle/VehiclePropertyAccess.h>

#include <unordered_map>

namespace aidl::android::hardware::automotive::vehicle {{
// This map represents the default access mode for each property.
std::unordered_map<VehicleProperty, VehiclePropertyAccess> DefaultAccessForVehicleProperty = {{
{0}
}};

// This map represents the allowed access modes for each property.
std::unordered_map<VehicleProperty, std::vector<VehiclePropertyAccess>>
        AllowedAccessForVehicleProperty = {{
{1}
}};
}}  // aidl::android::hardware::automotive::vehicle
"""

VERSION_CPP_FORMATTER = """#pragma once

#include <aidl/android/hardware/automotive/vehicle/VehicleProperty.h>

#include <unordered_map>

namespace aidl::android::hardware::automotive::vehicle {{
std::unordered_map<VehicleProperty, int32_t> VersionForVehicleProperty = {{
{0}
}};
}}  // aidl::android::hardware::automotive::vehicle
"""

ANNOTATIONS_CPP_FORMATTER = """#pragma once

#include <aidl/android/hardware/automotive/vehicle/VehicleProperty.h>

#include <string>
#include <unordered_map>
#include <unordered_set>

namespace aidl::android::hardware::automotive::vehicle {{
std::unordered_map<VehicleProperty, std::unordered_set<std::string>>
        AnnotationsForVehicleProperty = {{
{0}
}};
}}  // aidl::android::hardware::automotive::vehicle
"""

ENUM_CPP_FORMATTER = """#pragma once

#define addSupportedValues(EnumType) \\
{{ \\
constexpr auto values = ndk::internal::enum_values<EnumType>; \\
for (size_t i = 0; i < values.size(); i++) {{ \\
    supportedValues.insert(static_cast<int64_t>(values[i])); \\
}} \\
}}

#include <VehicleHalTypes.h>
#include <aidl/android/hardware/automotive/vehicle/VehicleProperty.h>

#include <unordered_set>

namespace aidl::android::hardware::automotive::vehicle {{
std::unordered_set<int64_t> getSupportedEnumValuesForProperty(VehicleProperty propertyId) {{
    std::unordered_set<int64_t> supportedValues;
    switch (propertyId) {{
{0}
        default:
            // Do nothing.
            break;
    }}
    return supportedValues;
}}
}}  // aidl::android::hardware::automotive::vehicle
"""

ENUM_CPP_SWITCH_CASE_FORMATTER = """        case {0}:
{1}
            break;
"""

CHANGE_MODE_JAVA_FORMATTER = """package android.hardware.automotive.vehicle;

import java.util.Map;

public final class ChangeModeForVehicleProperty {{

    public static final Map<Integer, Integer> values = Map.ofEntries(
{0}
    );

}}
"""

ACCESS_JAVA_FORMATTER = """package android.hardware.automotive.vehicle;

import java.util.Map;

public final class AccessForVehicleProperty {{

    public static final Map<Integer, Integer> values = Map.ofEntries(
{0}
    );

}}
"""

ENUM_JAVA_FORMATTER = """package android.hardware.automotive.vehicle;

import java.util.List;
import java.util.Map;

public final class EnumForVehicleProperty {{

    public static final Map<Integer, List<Class<?>>> values = Map.ofEntries(
{0}
    );

}}
"""

UNITS_JAVA_FORMATTER = """package android.hardware.automotive.vehicle;

import java.util.Map;

public final class UnitsForVehicleProperty {{

    public static final Map<Integer, Integer> values = Map.ofEntries(
{0}
    );

}}
"""

ANNOTATIONS_JAVA_FORMATTER = """package android.hardware.automotive.vehicle;

import java.util.Set;
import java.util.Map;

public final class AnnotationsForVehicleProperty {{

    public static final Map<Integer, Set<String>> values = Map.ofEntries(
{0}
    );

}}
"""


class PropertyConfig:
    """Represents one VHAL property definition in VehicleProperty.aidl."""

    def __init__(self):
        self.name = None
        self.description = None
        self.comment = None
        self.change_mode = None
        self.access_modes = []
        self.enum_types = []
        self.unit_type = None
        self.version = None
        # Use a set to avoid duplicate annotation.
        self.annotations = set()

    def __repr__(self):
        return self.__str__()

    def __str__(self):
        return ('PropertyConfig{{' +
            'name: {}, description: {}, change_mode: {}, access_modes: {}, enum_types: {}' +
            ', unit_type: {}, version: {}, comment: {}}}').format(self.name, self.description,
                self.change_mode, self.access_modes, self.enum_types, self.unit_type,
                self.version, self.comment)


class FileParser:

    def __init__(self):
        self.configs = None

    def parseFile(self, input_file):
        """Parses the input VehicleProperty.aidl file into a list of property configs."""
        processing = False
        in_comment = False
        configs = []
        config = None
        with open(input_file, 'r') as f:
            for line in f.readlines():
                if RE_ENUM_START.match(line):
                    processing = True
                elif RE_ENUM_END.match(line):
                    processing = False
                if not processing:
                    continue
                if RE_COMMENT_BEGIN.match(line):
                    in_comment = True
                    config = PropertyConfig()
                    # Use an array so that we could modify the string in parseComment.
                    description = ['']
                    continue

                if RE_COMMENT_END.match(line):
                    in_comment = False
                if in_comment:
                    # We will update the string in description in this function.
                    self.parseComment(line, config, description)
                else:
                    match = RE_VALUE.match(line)
                    if match:
                        prop_name = match.group(1)
                        if prop_name == 'INVALID':
                            continue
                        if not config.change_mode:
                            raise Exception(
                                    'No change_mode annotation for property: ' + prop_name)
                        if not config.access_modes:
                            raise Exception(
                                    'No access_mode annotation for property: ' + prop_name)
                        if not config.version:
                            raise Exception(
                                    'No version annotation for property: ' + prop_name)
                        if ('data_enum' in config.annotations and
                            'require_supported_values_list' not in config.annotations and
                            config.change_mode != 'STATIC' and
                            prop_name not in ENUM_PROPERTIES_WITHOUT_SUPPORTED_VALUES):
                            raise Exception(
                                    'The property: ' + prop_name + ' has @data_enum '
                                    'annotation but does not have @require_supported_values_list'
                                    ', either add the annotation or add the property name to '
                                    'ENUM_PROPERTIES_WITHOUT_SUPPORTED_VALUES in '
                                    'generate_annotation_enums.py')

                        config.name = prop_name
                        configs.append(config)

        self.configs = configs

    def parseComment(self, line, config, description):
        match_annotation = RE_ANNOTATION.match(line)
        if match_annotation:
            annotation = match_annotation.group(1)
            if annotation not in SUPPORTED_ANNOTATIONS:
                raise Exception('Annotation: @' + annotation + " is not supported, typo?")
            config.annotations.add(annotation)
            match = RE_CHANGE_MODE.match(line)
            if match:
                config.change_mode = match.group(1).replace('VehiclePropertyChangeMode.', '')
                return
            match = RE_ACCESS.match(line)
            if match:
                config.access_modes.append(match.group(1).replace('VehiclePropertyAccess.', ''))
                return
            match = RE_UNIT.match(line)
            if match:
                config.unit_type = match.group(1)
                return
            match = RE_DATA_ENUM.match(line)
            if match:
                config.enum_types.append(match.group(1))
                return
            match = RE_VERSION.match(line)
            if match:
                if config.version != None:
                    raise Exception('Duplicate version annotation for property: ' + prop_name)
                config.version = match.group(1)
                return

        sline = line.strip()
        if sline.startswith('*'):
            # Remove the '*'.
            sline = sline[1:].strip()

        if not config.description:
            # We reach an empty line of comment, the description part is ending.
            if sline == '':
                config.description = description[0]
            else:
                if description[0] != '':
                    description[0] += ' '
                description[0] += sline
        else:
            if not config.comment:
                if sline != '':
                    # This is the first line for comment.
                    config.comment = sline
            else:
                if sline != '':
                    # Concat this line with the previous line's comment with a space.
                    config.comment += ' ' + sline
                else:
                    # Treat empty line comment as a new line.
                    config.comment += '\n'

    def convert(self, output, formatter, cpp, field):
        """Converts the property config file to C++/Java output file."""
        counter = 0
        content = ''
        for config in self.configs:
            if field == 'change_mode':
                if cpp:
                    value = "VehiclePropertyChangeMode::" + config.change_mode
                else:
                    value = "VehiclePropertyChangeMode." + config.change_mode
            elif field == 'access_mode':
                if cpp:
                    value = "VehiclePropertyAccess::" + config.access_modes[0]
                else:
                    value = "VehiclePropertyAccess." + config.access_modes[0]
            elif field == 'enum_types':
                if len(config.enum_types) < 1:
                    continue
                if cpp:
                    switch_case = ''
                    for index, enum_type in enumerate(config.enum_types):
                        if index != 0:
                            switch_case += '\n'
                        switch_case += TAB + TAB + TAB + 'addSupportedValues({0})'.format(enum_type)
                    content += ENUM_CPP_SWITCH_CASE_FORMATTER.format(
                        'VehicleProperty::' + config.name, switch_case)
                    continue
                else:
                    value = "List.of(" + ', '.join([class_name + ".class" for class_name in config.enum_types]) + ")"
            elif field == 'unit_type':
                if not config.unit_type:
                    continue
                if not cpp:
                    value = config.unit_type
            elif field == 'version':
                if cpp:
                    value = config.version
            elif field == 'annotations':
                if len(config.annotations) < 1:
                    continue
                joined_annotation_strings = ', '.join(['"' + annotation + '"' for annotation in sorted(config.annotations)])
                if cpp:
                    value = "{" + joined_annotation_strings + "}"
                else:
                    value = "Set.of(" + joined_annotation_strings + ")"
            else:
                raise Exception('Unknown field: ' + field)
            if counter != 0:
                content += '\n'
            if cpp:
                content += (TAB + TAB + '{VehicleProperty::' + config.name + ', ' +
                            value + '},')
            else:
                content += (TAB + TAB + 'Map.entry(VehicleProperty.' + config.name + ', ' +
                            value + '),')
            counter += 1

        # Remove the additional ',' at the end for the Java file.
        if not cpp:
            content = content[:-1]

        if field != 'access_mode' or not cpp:
            content = LICENSE + formatter.format(content)
        else:
            content2 = ''
            counter = 0
            for config in self.configs:
                if counter != 0:
                    content2 += '\n'
                value = ', '. join(['VehiclePropertyAccess::' + access_mode for access_mode in config.access_modes])
                content2 += TAB + TAB + '{{VehicleProperty::{0}, {{{1}}}}},'.format(config.name, value)
                counter += 1
            content = LICENSE + formatter.format(content, content2)


        with open(output, 'w') as f:
            f.write(content)

    def outputAsCsv(self, output):
        content = 'name,description,change mode,access mode,enum type,unit type,comment\n'
        for config in self.configs:
            enum_types = None
            if not config.enum_types:
                enum_types = '/'
            else:
                enum_types = '/'.join(config.enum_types)
            unit_type = config.unit_type
            if not unit_type:
                unit_type = '/'
            access_modes = ''
            comment = config.comment
            if not comment:
                comment = ''
            content += '"{}","{}","{}","{}","{}","{}", "{}"\n'.format(
                    config.name,
                    # Need to escape quote as double quote.
                    config.description.replace('"', '""'),
                    config.change_mode,
                    '/'.join(config.access_modes),
                    enum_types,
                    unit_type,
                    comment.replace('"', '""'))

        with open(output, 'w+') as f:
            f.write(content)


def createTempFile():
    f = tempfile.NamedTemporaryFile(delete=False);
    f.close();
    return f.name


class GeneratedFile:

    def __init__(self, type):
        self.type = type
        self.cpp_file_path = None
        self.java_file_path = None
        self.cpp_formatter = None
        self.java_formatter = None
        self.cpp_output_file = None
        self.java_output_file = None

    def setCppFilePath(self, cpp_file_path):
        self.cpp_file_path = cpp_file_path

    def setJavaFilePath(self, java_file_path):
        self.java_file_path = java_file_path

    def setCppFormatter(self, cpp_formatter):
        self.cpp_formatter = cpp_formatter

    def setJavaFormatter(self, java_formatter):
        self.java_formatter = java_formatter

    def convert(self, file_parser, check_only, temp_files):
        if self.cpp_file_path:
            output_file = GeneratedFile._getOutputFile(self.cpp_file_path, check_only, temp_files)
            file_parser.convert(output_file, self.cpp_formatter, True, self.type)
            self.cpp_output_file = output_file

        if self.java_file_path:
            output_file = GeneratedFile._getOutputFile(self.java_file_path, check_only, temp_files)
            file_parser.convert(output_file, self.java_formatter, False, self.type)
            self.java_output_file = output_file

    def cmp(self):
        if self.cpp_file_path:
            if not filecmp.cmp(self.cpp_output_file, self.cpp_file_path):
                return False

        if self.java_file_path:
            if not filecmp.cmp(self.java_output_file, self.java_file_path):
                return False

        return True

    @staticmethod
    def _getOutputFile(file_path, check_only, temp_files):
        if not check_only:
            return file_path

        temp_file = createTempFile()
        temp_files.append(temp_file)
        return temp_file


def main():
    parser = argparse.ArgumentParser(
            description='Generate Java and C++ enums based on annotations in VehicleProperty.aidl')
    parser.add_argument('--android_build_top', required=False, help='Path to ANDROID_BUILD_TOP')
    parser.add_argument('--preupload_files', nargs='*', required=False, help='modified files')
    parser.add_argument('--check_only', required=False, action='store_true',
            help='only check whether the generated files need update')
    parser.add_argument('--output_csv', required=False,
            help='Path to the parsing result in CSV style, useful for doc generation')
    args = parser.parse_args();
    android_top = None
    output_folder = None
    if args.android_build_top:
        android_top = args.android_build_top
        vehiclePropertyUpdated = False
        for preuload_file in args.preupload_files:
            if preuload_file.endswith('VehicleProperty.aidl'):
                vehiclePropertyUpdated = True
                break
        if not vehiclePropertyUpdated:
            return
    else:
        android_top = os.environ['ANDROID_BUILD_TOP']
    if not android_top:
        print('ANDROID_BUILD_TOP is not in environmental variable, please run source and lunch ' +
            'at the android root')

    aidl_file = os.path.join(android_top, PROP_AIDL_FILE_PATH)
    f = FileParser();
    f.parseFile(aidl_file)

    if args.output_csv:
        f.outputAsCsv(args.output_csv)
        return

    generated_files = []

    change_mode = GeneratedFile('change_mode')
    change_mode.setCppFilePath(os.path.join(android_top, CHANGE_MODE_CPP_FILE_PATH))
    change_mode.setJavaFilePath(os.path.join(android_top, CHANGE_MODE_JAVA_FILE_PATH))
    change_mode.setCppFormatter(CHANGE_MODE_CPP_FORMATTER)
    change_mode.setJavaFormatter(CHANGE_MODE_JAVA_FORMATTER)
    generated_files.append(change_mode)

    access_mode = GeneratedFile('access_mode')
    access_mode.setCppFilePath(os.path.join(android_top, ACCESS_CPP_FILE_PATH))
    access_mode.setJavaFilePath(os.path.join(android_top, ACCESS_JAVA_FILE_PATH))
    access_mode.setCppFormatter(ACCESS_CPP_FORMATTER)
    access_mode.setJavaFormatter(ACCESS_JAVA_FORMATTER)
    generated_files.append(access_mode)

    enum_types = GeneratedFile('enum_types')
    enum_types.setCppFilePath(os.path.join(android_top, ENUM_CPP_FILE_PATH))
    enum_types.setJavaFilePath(os.path.join(android_top, ENUM_JAVA_FILE_PATH))
    enum_types.setJavaFormatter(ENUM_JAVA_FORMATTER)
    enum_types.setCppFormatter(ENUM_CPP_FORMATTER)
    generated_files.append(enum_types)

    unit_type = GeneratedFile('unit_type')
    unit_type.setJavaFilePath(os.path.join(android_top, UNITS_JAVA_FILE_PATH))
    unit_type.setJavaFormatter(UNITS_JAVA_FORMATTER)
    generated_files.append(unit_type)

    version = GeneratedFile('version')
    version.setCppFilePath(os.path.join(android_top, VERSION_CPP_FILE_PATH))
    version.setCppFormatter(VERSION_CPP_FORMATTER)
    generated_files.append(version)

    annotations = GeneratedFile('annotations')
    annotations.setCppFilePath(os.path.join(android_top, ANNOTATIONS_CPP_FILE_PATH))
    annotations.setJavaFilePath(os.path.join(android_top, ANNOTATIONS_JAVA_FILE_PATH))
    annotations.setCppFormatter(ANNOTATIONS_CPP_FORMATTER)
    annotations.setJavaFormatter(ANNOTATIONS_JAVA_FORMATTER)
    generated_files.append(annotations)

    temp_files = []

    try:
        for generated_file in generated_files:
            generated_file.convert(f, args.check_only, temp_files)

        if not args.check_only:
            return

        for generated_file in generated_files:
            if not generated_file.cmp():
                print('The generated enum files for VehicleProperty.aidl requires update, ')
                print('Run \npython ' + android_top + '/' + SCRIPT_PATH)
                sys.exit(1)
    except Exception as e:
        print('Error parsing VehicleProperty.aidl')
        print(e)
        sys.exit(1)
    finally:
        for file in temp_files:
            os.remove(file)


if __name__ == '__main__':
    main()