#!/bin/bash

# The single source of truth for reserved words, categorized by PHP documentation
# Taken from https://www.php.net/manual/en/reserved.keywords.php
PHP_RESERVED_KEYWORDS=(
    "abstract" "and" "array" "as" "break" "callable" "case" "catch"
    "class" "clone" "const" "continue" "declare" "default" "die" "do"
    "echo" "else" "elseif" "empty" "enddeclare" "endfor" "endforeach"
    "endif" "endswitch" "endwhile" "eval" "exit" "extends" "final"
    "finally" "fn" "for" "foreach" "function" "global" "goto" "if"
    "implements" "include" "include_once" "instanceof" "insteadof"
    "interface" "isset" "list" "match" "namespace" "new" "or" "parent"
    "print" "private" "protected" "public" "readonly" "require"
    "require_once" "return" "self" "static" "switch" "throw" "trait"
    "try" "unset" "use" "var" "while" "xor" "yield"
)

# Taken from https://www.php.net/manual/en/reserved.other-reserved-words.php
PHP_OTHER_RESERVED_WORDS=(
    "parent" "self" "int" "float" "bool" "string" "true" "false" "null"
    "void" "iterable" "object" "mixed" "never" "array" "callable"
)

# Derived lists:
# RESERVED_WORDS is the union of keywords and soft reserved words.
RESERVED_WORDS=($(echo "${PHP_RESERVED_KEYWORDS[@]}" "${PHP_OTHER_RESERVED_WORDS[@]}" | tr ' ' '\n' | sort -u))

# This is for backwards compatibility. New PHP_RESERVED_KEYWORDS should be added
# here to prevent unnecessary breaking changes, as constants do not need the PB prefix.
# @TODO: allow ALL names for constants except for "class"
VALID_CONSTANT_NAMES=(
    "bool" "false" "float" "int" "iterable" "mixed" "never" "null"
    "object" "parent" "readonly" "self" "string" "true" "void"
)
VALID_CONSTANT_NAMES=($(echo "${VALID_CONSTANT_NAMES[@]}" | tr ' ' '\n' | sort -u))

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(dirname "$SCRIPT_DIR")"

format_columns() {
    local per_line=$1
    local indent=$2
    shift 2
    local words=("$@")
    local num_words=${#words[@]}

    # Default to original behavior of quoting the word.
    local item_fmt="${ITEM_FMT:-\"%s\"}"
    if [[ -z "$ITEM_FMT" ]]; then
        item_fmt="\"%s\""
    fi

    # Compute max width per column to match Google C++/C formatting
    local col_widths=()
    local c=0
    while (( c < per_line )); do
        local max_w=0
        local k=$c
        while (( k < num_words )); do
            local w="${words[k]}"
            local s
            printf -v s "$item_fmt" "$w"
            if (( k < num_words - 1 )); then
                s+=","
            fi
            if (( ${#s} > max_w )); then
                max_w=${#s}
            fi
            k=$((k + per_line))
        done
        col_widths+=($((max_w + 1)))
        c=$((c + 1))
    done

    local i=0
    while (( i < num_words )); do
        local line="$indent"
        local j=0
        while (( j < per_line && i < num_words )); do
            local w="${words[i]}"
            local s
            printf -v s "$item_fmt" "$w"
            if (( i < num_words - 1 )); then
                s+=","
            fi

            if (( j < per_line - 1 && i < num_words - 1 )); then
                local cw="${col_widths[j]}"
                printf -v padded "%-${cw}s" "$s"
                line+="$padded"
            else
                line+="$s"
            fi

            i=$((i + 1))
            j=$((j + 1))
        done
        echo "${line%" "}"
    done
}

update_file() {
    local path="$1"
    local label="$2"
    local new_content="$3"

    local begin="// BEGIN $label"
    local end="// END $label"
    local full_path="$REPO_ROOT/$path"
    if [[ ! -f "$full_path" ]]; then
        echo "Skipping $path (not found)"
        return
    fi

    # Extract indentation of the first line of content
    local indent=$(echo "$new_content" | head -n 1 | grep -o '^[[:space:]]*')

    local temp_file=$(mktemp)

    # Use perl to replace the block, writing to a temporary file
    BEGIN_MARKER="$begin" END_MARKER="$end" NEW_CONTENT="$new_content" INDENT="$indent" \
    perl -0777 -pe 's{^[ \t]*\Q$ENV{BEGIN_MARKER}\E.*?^[ \t]*\Q$ENV{END_MARKER}\E}{$ENV{INDENT}$ENV{BEGIN_MARKER}\n$ENV{NEW_CONTENT}\n$ENV{INDENT}$ENV{END_MARKER}}sm' "$full_path" > "$temp_file"

    # Move the temporary file to the original file's path
    mv "$temp_file" "$full_path"

    echo "Updated $path"
}

generate_names_cc() {
    local lines=()
    lines+=("// @see php/update_reserved_words.sh - DO NOT MODIFY THIS LIST MANUALLY")
    lines+=("const char* const kReservedNames[] = {")
    local col_lines=()
    while IFS= read -r line; do
        col_lines+=("$line")
    done < <(format_columns 5 "    " "${RESERVED_WORDS[@]}")
    for l in "${col_lines[@]}"; do lines+=("$l"); done
    lines[${#lines[@]}-1]+="};"
    lines+=("const int kReservedNamesSize = ${#RESERVED_WORDS[@]};")
    printf "%s\n" "${lines[@]}"
}

generate_names_c() {
    local lines=()
    lines+=("// @see php/update_reserved_words.sh - DO NOT MODIFY THIS LIST MANUALLY")
    lines+=("const char* const kReservedNames[] = {")
    local decorated=()
    for w in "${RESERVED_WORDS[@]}"; do
        decorated+=("\"$w\"")
    done
    decorated+=("NULL")
    local col_lines=()
    while IFS= read -r line; do
        col_lines+=("$line")
    done < <(ITEM_FMT="%s" format_columns 4 "    " "${decorated[@]}")
    for l in "${col_lines[@]}"; do lines+=("$l"); done
    lines[${#lines[@]}-1]+="};"
    printf "%s\n" "${lines[@]}"
}

generate_php_generator_cc() {
    local lines=()
    lines+=("// @see php/update_reserved_words.sh - DO NOT MODIFY THIS LIST MANUALLY")
    lines+=("constexpr absl::string_view kValidConstantNames[] = {")
    local col_lines=()
    while IFS= read -r line; do
        col_lines+=("$line")
    done < <(format_columns 5 "    " "${VALID_CONSTANT_NAMES[@]}")
    for l in "${col_lines[@]}"; do lines+=("$l"); done
    lines[${#lines[@]}-1]+="};"
    lines+=("const int kValidConstantNamesSize = ${#VALID_CONSTANT_NAMES[@]};")
    printf "%s\n" "${lines[@]}"
}

generate_proto_messages() {
    local upper=$1
    echo "// @see php/update_reserved_words.sh - DO NOT MODIFY THIS LIST MANUALLY"
    for w in "${RESERVED_WORDS[@]}"; do
        local name="$w"
        if [[ "$upper" == "true" ]]; then name=$(echo "$w" | tr '[:lower:]' '[:upper:]'); fi
        echo "message $name {}"
    done
}

generate_proto_enums() {
    local upper=$1
    echo "// @see php/update_reserved_words.sh - DO NOT MODIFY THIS LIST MANUALLY"
    local i=1
    for w in "${RESERVED_WORDS[@]}"; do
        local name="$w"
        if [[ "$upper" == "true" ]]; then name=$(echo "$w" | tr '[:lower:]' '[:upper:]'); fi
        echo "enum $name {"
        echo "  ZERO$i = 0;"
        echo "}"
        i=$((i + 1))
    done
}

generate_proto_enum_values() {
    local upper=$1
    echo "  // @see php/update_reserved_words.sh - DO NOT MODIFY THIS LIST MANUALLY"
    local i=0
    for w in "${RESERVED_WORDS[@]}"; do
        local name="$w"
        if [[ "$upper" == "true" ]]; then name=$(echo "$w" | tr '[:lower:]' '[:upper:]'); fi
        echo "  $name = $i;"
        i=$((i + 1))
    done
}

generate_test_cases() {
    echo "        // @see php/update_reserved_words.sh - DO NOT MODIFY THIS LIST MANUALLY"
    # Lower messages
    for w in "${RESERVED_WORDS[@]}"; do
        echo "        \$m = new \\Lower\\PB$w();"
    done
    echo ""
    # Upper messages
    for w in "${RESERVED_WORDS[@]}"; do
        local upper=$(echo "$w" | tr '[:lower:]' '[:upper:]')
        echo "        \$m = new \\Upper\\PB$upper();"
    done
    echo ""
    # Lower enums
    for w in "${RESERVED_WORDS[@]}"; do
        echo "        \$m = new \\Lower_enum\\PB$w();"
    done
    echo ""
    # Upper enums
    for w in "${RESERVED_WORDS[@]}"; do
        local upper=$(echo "$w" | tr '[:lower:]' '[:upper:]')
        echo "        \$m = new \\Upper_enum\\PB$upper();"
    done
    echo ""
    # Lower enum values
    for w in "${RESERVED_WORDS[@]}"; do
        local prefix="PB"
        for v in "${VALID_CONSTANT_NAMES[@]}"; do
            if [[ "$w" == "$v" ]]; then prefix=""; break; fi
        done
        echo "        \$m = \\Lower_enum_value\\NotAllowed::${prefix}${w};"
    done
    echo ""
    # Upper enum values
    for w in "${RESERVED_WORDS[@]}"; do
        local prefix="PB"
        for v in "${VALID_CONSTANT_NAMES[@]}"; do
            if [[ "$w" == "$v" ]]; then prefix=""; break; fi
        done
        local upper=$(echo "$w" | tr '[:lower:]' '[:upper:]')
        echo "        \$m = \\Upper_enum_value\\NotAllowed::${prefix}${upper};"
    done
}

generate_gpbutil_reserved_words() {
    echo "        // @see php/update_reserved_words.sh - DO NOT MODIFY THIS LIST MANUALLY"
    echo "        \$reserved_words = array("
    ITEM_FMT="\"%s\"=>0" format_columns 3 "            " "${RESERVED_WORDS[@]}"
    echo "        );"
}

update_file "src/google/protobuf/compiler/php/names.cc" "RESERVED NAMES" "$(generate_names_cc)"
update_file "php/ext/google/protobuf/names.c" "RESERVED NAMES" "$(generate_names_c)"
update_file "src/google/protobuf/compiler/php/php_generator.cc" "VALID CONSTANT NAMES" "$(generate_php_generator_cc)"
update_file "php/tests/proto/test_reserved_message_lower.proto" "RESERVED NAMES" "$(generate_proto_messages false)"
update_file "php/tests/proto/test_reserved_message_upper.proto" "RESERVED NAMES" "$(generate_proto_messages true)"
update_file "php/tests/proto/test_reserved_enum_lower.proto" "RESERVED NAMES" "$(generate_proto_enums false)"
update_file "php/tests/proto/test_reserved_enum_upper.proto" "RESERVED NAMES" "$(generate_proto_enums true)"
update_file "php/tests/proto/test_reserved_enum_value_lower.proto" "RESERVED NAMES" "$(generate_proto_enum_values false)"
update_file "php/tests/proto/test_reserved_enum_value_upper.proto" "RESERVED NAMES" "$(generate_proto_enum_values true)"
update_file "php/tests/GeneratedClassTest.php" "RESERVED NAMES" "$(generate_test_cases)"
update_file "php/src/Google/Protobuf/Internal/GPBUtil.php" "RESERVED WORDS" "$(generate_gpbutil_reserved_words)"
