# Usage: python gen.py

import json
import os
import re
from collections import defaultdict

script_dir = os.path.dirname(os.path.abspath(__file__))
usage_file = os.path.join(script_dir, 'usage.json')

with open(usage_file, 'r') as file:
  data = json.load(file)
  
  with open(os.path.join(script_dir, '../USAGE.md'), 'w') as md_file:
    md_file.write("""<!--- This file is generated by gen.py, do not edit it directly. -->
Usage
----

There are many environment variables to control Box64's behaviour, which will be listed below by category.

There are 2 types of Box64 builds: the Wine WOW64 build (WowBox64) and the regular Linux build. Beware only some of the environment variables are available in WowBox64.

### Configuration files

In addition to environment variables, if you're using the regular Linux build, Box64 also looks for 2 places for rcfile by default: the system-wide `/etc/box64.box64rc` and user-specific `~/.box64rc`.
While in WowBox64, the configuration file is checked at `%USERPROFILE%/.box64rc` only.
Settings priority follows this order (from highest to lowest): `~/.box64rc` > `/etc/box64.box64rc` > environment variables.

Example configuration:

```
[factorio]
BOX64_DYNAREC_SAFEFLAGS=0
BOX64_DYNAREC_BIGBLOCK=2
BOX64_DYNAREC_FORWARD=1024
BOX64_DYNAREC_CALLRET=1
```

This configuration will apply the specified settings application-wide to any executable named `factorio`.

### Advanced usage for Linux build

1. **Wildcard Matching**

   Asterisks (`*`) can be used for basic pattern matching in application names. For instance, `[*setup*]` will match any program containing "setup" in its name. Note this implements simple wildcard matching rather than full regex support.
2. **Custom Configuration File**

   The `BOX64_RCFILE` environment variable can specify an alternative configuration file instead of the default `/etc/box64.box64rc`.
3. **Per-File Settings**

   Sections starting with `/` apply to specific files. For example:
   ```
   [/d3d9.dll]
   BOX64_DYNAREC_SAFEFLAGS=0
   ```
   These settings will only affect the `d3d9.dll` file. This syntax also works for **emulated** Linux libraries, e.g., `[/libstdc++.so.6]`.

----

""")

    categories = defaultdict(list)
    for entry in data:
      categories[entry["category"]].append(entry)

    # Put "Performance" at the top
    sorted_categories = sorted(categories.items(), key=lambda x: x[0] != "Performance")
    for category, entries in sorted_categories:
      md_file.write(f"## {category}\n\n")
      for entry in entries:
        md_file.write(f"### {entry['name']}\n\n{entry['description']}{' Availble in WowBox64.' if entry['wine'] else ''}\n\n")
        for option in entry['options']:
          md_file.write(f" * {option['key']}: {option['description']} {'[Default]' if option['default'] else ''}\n")
        md_file.write("\n")


  with open(os.path.join(script_dir, '../box64.pod'), 'w') as pod_file:
    pod_file.write("""=head1 NAME

box64 - Linux Userspace x86_64 Emulator with a twist

=head1 SYNOPSIS

B<box64> [B<--help>] [B<--version>] I<executable>

=head1 DESCRIPTION

B<Box64> lets you run x86_64 Linux programs (such as games) on non-x86_64 Linux
systems, like ARM (host system needs to be 64-bit little-endian). Since B<Box64>
uses the native versions of some "system" libraries, like libc, libm, SDL, and
OpenGL, it's easy to integrate and use with most applications, and performance
can be surprisingly high in many cases. B<Box64> integrates with DynaRec (dynamic
recompiler) for the ARM64 platform, providing a speed boost between 5 to 10
times faster than using only the interpreter.

=head1 OPTIONS

=over 8

=item B<-h,--help>

Print box64 help and quit.

=item B<-v,--version>

Print box64 version and quit.

=back

=head1 BRIEF USAGE

There are many environment variables to control B<Box64>'s behaviour. In
addition to environment variables, B<Box64> also looks for 2 places for rcfile:
F</etc/box64.box64rc> and F<~/.box64rc>, in the format of .ini files.
Settings priority: F<~/.box64rc> > F</etc/box64.box64rc> > environment variables.
Example:

    [factorio]
    BOX64_DYNAREC_SAFEFLAGS=0
    BOX64_DYNAREC_BIGBLOCK=2
    BOX64_DYNAREC_FORWARD=1024
    BOX64_DYNAREC_CALLRET=1

=head1 ENVIRONMENT VARIABLES

=over 8

""")

    for entry in data:
      pod_file.write(f"\n=item B<{entry['name']}> =I<{ '|'.join(option['key'] for option in entry['options']) }>\n\n{entry['description']}{' Availble in WowBox64.' if entry['wine'] else ''}\n\n")
      for option in entry['options']:
        pod_file.write(f" * {option['key']} : {option['description']} {'[Default]' if option['default'] else ''}\n")
      pod_file.write("\n")

    pod_file.write("""
=back

=cut
""")

####################
# Validation
####################

PADDING = 30
INCLUDE_BLANK = False

def get_usage_entry(usage, define):
    entry = list(e for e in usage if e["name"] == define)
    if len(entry) == 0:
        print(f"{define:<{PADDING}}: missing usage.json entry")
    elif len(entry) > 1:
        print(f"{define:<{PADDING}}: multiple usage entries found", len(entry))
    else:
        return entry[0]
    return None

# check `default` uniqueness
for entry in data:
    i = list(d["default"] for d in entry["options"]).count(True)
    if i > 1:
        print(f"{entry['name']:<{PADDING}}: multiple default values usage.json")


# regex to match env.h C code
regex = {
    "INTEGER": re.compile(
        r"^\s*INTEGER\((?P<define>\w+), (?P<name>\w+), (?P<default>\w+), (?P<min>\w+), (?P<max>\w+), (?P<wine>\w+)\)"
    ),
    "INTEGER64": re.compile(
        r"^\s*INTEGER64\((?P<define>\w+), (?P<name>\w+), (?P<default>\w+), (?P<wine>\w+)\)"
    ),
    "BOOLEAN": re.compile(
        r"^\s*BOOLEAN\((?P<define>\w+), (?P<name>\w+), (?P<default>\w+), (?P<wine>\w+)\)"
    ),
    "ADDRESS": re.compile(r"^\s*ADDRESS\((?P<define>\w+), (?P<name>\w+), (?P<wine>\w+)\)"),
    "STRING": re.compile(r"^\s*STRING\((?P<define>\w+), (?P<name>\w+), (?P<wine>\w+)\)"),
}

env_file = os.path.join(script_dir, "../../src/include/env.h")
with open(env_file, "r") as file:
    matches = {}
    for line in file.readlines():
        for t, r in regex.items():
            m = r.search(line)
            if m:
                # filter out comments and other non useful code
                if m.group("define") == "NAME":
                    continue

                if t not in matches:
                    matches[t] = {}
                if (m.group("define") in matches[t]):
                    # multiple definitions, no default
                    matches[t][m.group("define")]['no_default'] = True
                    break
                matches[t][m.group("define")] = m.groupdict()
                matches[t][m.group("define")]['no_default'] = False

for define, m in matches["INTEGER"].items():
    name = m["name"]
    default = None if m["no_default"] or not m["default"].isdigit() else int(m["default"])
    min = int(m["min"])
    max = int(m["max"])

    # Check default in valid range
    if default and (default < min or default > max):
        print(f"{define:<{PADDING}}: default lays outside of min/max range")

    # Check consistency with usage.json
    if e := get_usage_entry(data, define):
        # guess min/max values if possible
        min2 = max2 = None
        # blank means that the entry has an 'XXXX' entry which usually indicated that arbitrary values are valid
        blank = False
        default2 = None

        if int(m["wine"]) != int(e["wine"]):
            print(f"{define:<{PADDING}}: wine mismatch: env.h={int(m['wine'])}, usage.json={int(e['wine'])}")

        for o in e["options"]:
            if o["key"] == "XXXX":
                blank = True
                continue

            val = int(o["key"])  # not supposed to fail

            min2 = min2 if min2 is not None and min2 < val else val
            max2 = max2 if max2 is not None and max2 > val else val

            if o["default"]:
                default2 = val

        if min2 and min2 != min:
            if not blank or (blank and INCLUDE_BLANK):
                print(
                    f"{define:<{PADDING}}: min value mismatch: env.h={min}, usage.json={min2}{(' (possible false positive)' if blank else '')}"
                )
        if max2 and max2 != max:
            if not blank or (blank and INCLUDE_BLANK):
                print(
                    f"{define:<{PADDING}}: max value mismatch: env.h={max}, usage.json={max2}{(' (possible false positive)' if blank else '')}"
                )
        if default2 != default:
            print(
                f"{define:<{PADDING}}: default value mismatch: env.h={default}, usage.json={default2}"
            )

for define, m in matches['INTEGER64'].items():
    # similar to INTEGER but without min/max
    name = m["name"]
    default = None if m["no_default"] or not m["default"].isdigit() else int(m["default"])

    # Check consistency with usage.json
    if e := get_usage_entry(data, define):
        default2 = None

        if int(m["wine"]) != int(e["wine"]):
            print(f"{define:<{PADDING}}: wine mismatch: env.h={int(m['wine'])}, usage.json={int(e['wine'])}")

        for o in e["options"]:
            if o["key"] == "XXXX":
                continue

            val = int(o["key"])

            if o["default"]:
                default2 = val

        if default2 != default:
            print(
                f"{define:<{PADDING}}: default value mismatch: env.h={default}, usage.json={default2}"
            )

for define, m in matches["BOOLEAN"].items():
    name = m["name"]
    default = None if m["no_default"] or m["default"] not in ["0", "1"] else bool(m["default"])

    # Check consistency with usage.json
    if e := get_usage_entry(data, define):
        default2 = None

        if int(m["wine"]) != int(e["wine"]):
            print(f"{define:<{PADDING}}: wine mismatch: env.h={int(m['wine'])}, usage.json={int(e['wine'])}")

        for o in e["options"]:
            try:
                val = bool(o["key"])
            except ValueError:
                print(f"{define:<{PADDING}}: failed to parse boolean {o['key']}")

            if o["default"]:
                default2 = val

        if default2 != default:
            print(
                f"{define:<{PADDING}}: default value mismatch: env.h={default}, usage.json={default2}"
            )

# ADDRESS and STRING are not that interesting
for define, m in matches["ADDRESS"].items():
    if e := get_usage_entry(data, define):
        if int(m["wine"]) != int(e["wine"]):
            print(f"{define:<{PADDING}}: wine mismatch: env.h={int(m['wine'])}, usage.json={int(e['wine'])}")

for define, m in matches["STRING"].items():
    # skip BOX64_ENV[1-5] entries, they mismatch but this is fine
    if define.startswith("BOX64_ENV"):
        continue
    if e := get_usage_entry(data, define):
        if int(m["wine"]) != int(e["wine"]):
            print(f"{define:<{PADDING}}: wine mismatch: env.h={int(m['wine'])}, usage.json={int(e['wine'])}")

# check that everything from usage.json is in env.h
for e in data:
    define = e["name"]

    # skip BOX64_ENV[1-5] entries, they mismatch but this is fine
    if define.startswith("BOX64_ENV"):
        continue

    found = False
    for t in matches:
        for d in matches[t]:
            if d == define:
                found = True

    if not found:
        print(f"{define:<{PADDING}}: missing env.h entry")
