#!/usr/bin/env python
# Copyright 2022 Google LLC
#
# This source code is licensed under the BSD-style license found in the
# LICENSE file in the root directory of this source tree.

import argparse
import codecs
import math
import os
import re
import sys
import yaml

sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import xngen
import xnncommon

parser = argparse.ArgumentParser(
    description='Generates enum header and convertion-to-string code.')
parser.add_argument(
    '-s',
    '--spec',
    metavar='FILE',
    required=True,
    help='Specification (YAML) file')
parser.add_argument(
    '--output_src',
    metavar='FILE',
    required=True,
    help='Output C source file')
parser.add_argument(
    '--output_hdr',
    metavar='FILE',
    required=True,
    help='Output C/C++ header file')
parser.add_argument(
    '-e',
    '--enum',
    metavar='NAME',
    required=True,
    help='Name of the enum variable')
parser.set_defaults(defines=list())


def generate_source(enum_name, spec_path, output_path, header_path):
  with codecs.open(spec_path, 'r', encoding='utf-8') as spec_file:
    spec_yaml = yaml.safe_load(spec_file)
    if not isinstance(spec_yaml, list):
      raise ValueError('expected a list of enumeration values in the spec')

    output = f"""\
// Copyright 2022 Google LLC
//
// This source code is licensed under the BSD-style license found in the
// LICENSE file in the root directory of this source tree.
//
// Auto-generated file. Do not edit!
//   Specification: {spec_path}
//   Generator: {sys.argv[0]}


#include <assert.h>
#include <stdint.h>

#include <{header_path}>\n\n\n"""

    max_offset = sum(len(entry['string']) + 1 for entry in spec_yaml[:-1])
    if max_offset < 256:
      offset_type = 'uint8_t'
    elif max_offset < 65536:
      offset_type = 'uint16_t'
    else:
      offset_type = 'uint32_t'

    offset_declaration = f'static const {offset_type} offset[{len(spec_yaml)}] = {{\n ';
    string_declaration = 'static const char data[] =\n'
    pos = 0
    for i, spec_entry in enumerate(spec_yaml):
      enum_item_name = spec_entry['name']
      assert enum_item_name.startswith(enum_name + "_")
      enum_item_string = spec_entry['string']

      if i + 1 != len(spec_yaml):
        string_declaration += '  "' + enum_item_string + '\\0"\n'
        offset_declaration += ' ' + str(pos) + ','
      else:
        string_declaration += '  "' + enum_item_string + '";\n'
        offset_declaration += ' ' + str(pos) + '\n};'

      # Wrap offset array on 120 columns
      last_offset_line = offset_declaration[offset_declaration.rfind('\n')+1:]
      if len(last_offset_line) > 120:
        last_offset_start = offset_declaration.rfind(',', 0, -1) + 1
        offset_declaration = offset_declaration[:last_offset_start] + '\n ' + offset_declaration[last_offset_start:]

      pos += len(enum_item_string) + 1

    output += offset_declaration
    output += '\n\n'
    output += string_declaration

    arg_name = enum_name[len("xnn_"):]
    output += f"""
const char* {enum_name}_to_string(enum {enum_name} {arg_name}) {{
  assert({arg_name} >= {spec_yaml[0]['name']});
  assert({arg_name} <= {spec_yaml[-1]['name']});
  return &data[offset[{arg_name}]];
}}\n"""

    txt_changed = True
    if os.path.exists(output_path):
      with codecs.open(output_path, 'r', encoding='utf-8') as output_file:
        txt_changed = output_file.read() != output

    if txt_changed:
      with codecs.open(output_path, 'w', encoding='utf-8') as output_file:
        output_file.write(output)

def generate_header(enum_name, spec_path, output_path):
  with codecs.open(spec_path, 'r', encoding='utf-8') as spec_file:
    spec_yaml = yaml.safe_load(spec_file)
    if not isinstance(spec_yaml, list):
      raise ValueError('expected a list of enumeration values in the spec')

    output = f"""\
// Copyright 2022 Google LLC
//
// This source code is licensed under the BSD-style license found in the
// LICENSE file in the root directory of this source tree.
//
// Auto-generated file. Do not edit!
//   Specification: {spec_path}
//   Generator: {sys.argv[0]}

#pragma once

#include <xnnpack/common.h>


#ifdef __cplusplus
extern "C" {{
#endif

enum {enum_name} {{\n"""

    enum_item_name = spec_yaml[0]['name']
    assert enum_item_name.startswith(enum_name + "_")
    output += '  ' + enum_item_name + ' = 0,\n'
    for spec_entry in spec_yaml[1:]:
      enum_item_name = spec_entry['name']
      assert enum_item_name.startswith(enum_name + "_")
      output += '  ' + enum_item_name + ',\n'

    arg_name = enum_name[len("xnn_"):]
    output += f"""}};

XNN_INTERNAL const char* {enum_name}_to_string(enum {enum_name} {arg_name});

#ifdef __cplusplus
}}  // extern "C"
#endif
"""


    txt_changed = True
    if os.path.exists(output_path):
      with codecs.open(output_path, 'r', encoding='utf-8') as output_file:
        txt_changed = output_file.read() != output

    if txt_changed:
      with codecs.open(output_path, 'w', encoding='utf-8') as output_file:
        output_file.write(output)

def main(args):
  options = parser.parse_args(args)
  generate_header(options.enum, options.spec, options.output_hdr)

  assert options.enum.startswith('xnn_')
  header_path = 'xnnpack/' + options.enum[len('xnn_'):].replace('_', '-') + '.h'
  generate_source(options.enum, options.spec, options.output_src, header_path)

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