#!/usr/bin/env bash

# Copyright 2025 The KCP Authors.
#
# 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.

# This presents several functions for packages which want to use kcp
# code-generation tools.

# These functions insist that your input IDL (commented go) files be located in
# go packages following the pattern $input_pkg_root/$something_sans_slash/$api_version.
# Those $something_sans_slash will be propagated into the output directory structure.

# This file has been adapted from k8s.io/code-generator and can be sourced in
# parallel to its kube_codegen.sh. It works the same way, just with cluster:: as the
# function prefix rather than kube::.

set -o errexit
set -o nounset
set -o pipefail

CLUSTER_CODEGEN_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"

# Callers which want a specific tag of the github.com/kcp-dev/code-generator repo
# should set the CLUSTER_CODEGEN_TAG to the tag name, e.g. CLUSTER_CODEGEN_TAG="kcp-1.32.2"
# before sourcing this file.
CLUSTER_CODEGEN_VERSION_SPEC="${CLUSTER_CODEGEN_TAG:+"@${CLUSTER_CODEGEN_TAG}"}"

function cluster::codegen::internal::grep() {
    # We use `grep` rather than `git grep` because sometimes external projects
    # use this across repos.
    grep "$@" \
        --exclude-dir .git \
        --exclude-dir _output \
        --exclude-dir vendor
}

# Generate client code
#
# USAGE: cluster::codegen::gen_client [FLAGS] <input-dir>
#
# <input-dir>
#   The root package under which to search for Go files which request clients
#   to be generated. This must be a local path, not a Go package.
#
#   See note at the top about package structure below that.
#
# FLAGS:
#   --one-input-api <string>
#     A specific API (a directory) under the input-dir for which to generate a
#     client.  If this is not set, clients for all APIs under the input-dir
#     will be generated (under the --output-pkg).
#
#   --boilerplate <string = path_to_kube_codegen_boilerplate>
#     An optional override for the header file to insert into generated files.
#
#   --output-dir <string>
#     The root directory under which to emit code.  Each aspect of client
#     generation will make one or more subdirectories unless an explicit
#     directory for that aspect is configured also.
#
#   --output-pkg <string>
#     The Go package path (import path) of the --output-dir.  Each aspect of
#     client generation will make one or more sub-packages unless an explicit
#     package for that aspect is configured also.
#
#   --versioned-clientset-dir <string>
#     The root directory under which to emit the generated cluster-aware clientset.
#     Overwrites the automatic detection based on --output-dir.
#
#   --versioned-clientset-pkg <string>
#     The Go package path (import path) of the --versioned-clientset-dir.
#     Overwrites the automatic detection based on --output-dir.
#
#   --informers-dir <string>
#     The root directory under which to emit the generated cluster-aware informers.
#     Overwrites the automatic detection based on --output-dir.
#
#   --informers-pkg <string>
#     The Go package path (import path) of the --informers-dir.
#     Overwrites the automatic detection based on --output-dir.
#
#   --listers-dir <string>
#     The root directory under which to emit the generated cluster-aware listers.
#     Overwrites the automatic detection based on --output-dir.
#
#   --listers-pkg <string>
#     The Go package path (import path) of the --listers-dir.
#     Overwrites the automatic detection based on --output-dir.
#
#   --single-cluster-versioned-clientset-pkg
#     The package name of the generated versioned Kubernetes clientset, e.g.
#     "acme.corp/sdk/generated/clientset/versioned". This has to have already been
#     generated by k8s.io/code-generator.
#
#   --single-cluster-applyconfigurations-pkg
#     The name of the generated Kubernetes applyconfiguration package, e.g.
#     "acme.corp/sdk/generated/applyconfigurations". This has to have already been
#     generated by k8s.io/code-generator.
#
#   --single-cluster-informers-pkg
#     The optional name of the generated Kubernetes informers package, e.g.
#     "acme.corp/sdk/generated/informers". This has to have already been
#     generated by k8s.io/code-generator. If not specified, the generated cluster
#     informers will include the necessary interfaces themselves.
#
#   --single-cluster-listers-pkg
#     The optional name of the generated Kubernetes listers package, e.g.
#     "acme.corp/sdk/generated/listers". This has to have already been
#     generated by k8s.io/code-generator. If not specified, the generated cluster
#     listers will include the necessary interfaces themselves.
#
#   --with-watch
#     Enables generation of listers and informers for APIs which support WATCH.
#
#   --plural-exceptions <string = "">
#     An optional list of comma separated plural exception definitions in Type:PluralizedType form.
#
#   --exclude-group-versions <string = "">
#     An optional list of comma separate group versions to exclude from
#     client generation.
#
function cluster::codegen::gen_client() {
    local in_dir=""
    local one_input_api=""
    local boilerplate="${CLUSTER_CODEGEN_ROOT}/hack/boilerplate.go.txt"
    local out_dir=""
    local out_pkg=""
    local versioned_clientset_dir=""
    local versioned_clientset_pkg=""
    local informers_dir=""
    local informers_pkg=""
    local listers_dir=""
    local listers_pkg=""
    local single_cluster_versioned_clientset_pkg=""
    local single_cluster_applyconfigurations_pkg=""
    local single_cluster_informers_pkg=""
    local single_cluster_listers_pkg=""
    local watchable="false"
    local plural_exceptions=""
    local exclude_group_versions=""
    local v="${KUBE_VERBOSE:-0}"

    while [ "$#" -gt 0 ]; do
        case "$1" in
            "--one-input-api")
                one_input_api="/$2"
                shift 2
                ;;
            "--boilerplate")
                boilerplate="$2"
                shift 2
                ;;
            "--output-dir")
                out_dir="$2"
                shift 2
                ;;
            "--output-pkg")
                out_pkg="$2"
                shift 2
                ;;
            "--versioned-clientset-dir")
                versioned_clientset_dir="$2"
                shift 2
                ;;
            "--versioned-clientset-pkg")
                versioned_clientset_pkg="$2"
                shift 2
                ;;
            "--informers-dir")
                informers_dir="$2"
                shift 2
                ;;
            "--informers-pkg")
                informers_pkg="$2"
                shift 2
                ;;
            "--listers-dir")
                listers_dir="$2"
                shift 2
                ;;
            "--listers-pkg")
                listers_pkg="$2"
                shift 2
                ;;
            "--single-cluster-versioned-clientset-pkg")
                single_cluster_versioned_clientset_pkg="$2"
                shift 2
                ;;
            "--single-cluster-applyconfigurations-pkg")
                single_cluster_applyconfigurations_pkg="$2"
                shift 2
                ;;
            "--single-cluster-informers-pkg")
                single_cluster_informers_pkg="$2"
                shift 2
                ;;
            "--single-cluster-listers-pkg")
                single_cluster_listers_pkg="$2"
                shift 2
                ;;
            "--with-watch")
                watchable="true"
                shift
                ;;
            "--plural-exceptions")
                plural_exceptions="$2"
                shift 2
                ;;
            "--exclude-group-versions")
                exclude_group_versions="$2"
                shift 2
                ;;
            *)
                if [[ "$1" =~ ^-- ]]; then
                    echo "unknown argument: $1" >&2
                    return 1
                fi
                if [ -n "$in_dir" ]; then
                    echo "too many arguments: $1 (already have $in_dir)" >&2
                    return 1
                fi
                in_dir="$1"
                shift
                ;;
        esac
    done

    if [ -z "${in_dir}" ]; then
        echo "input-dir argument is required" >&2
        return 1
    fi
    if [ -z "${single_cluster_versioned_clientset_pkg}" ]; then
        echo "--single-cluster-versioned-clientset-pkg is required" >&2
        return 1
    fi
    if [ -z "${single_cluster_applyconfigurations_pkg}" ]; then
        echo "--single-cluster-applyconfigurations-pkg is required" >&2
        return 1
    fi

    (
        # To support running this from anywhere, first cd into this directory,
        # and then install with forced module mode on and fully qualified name.
        cd "${CLUSTER_CODEGEN_ROOT}"
        BINS=(
            cluster-client-gen"${CLUSTER_CODEGEN_VERSION_SPEC}"
            cluster-informer-gen"${CLUSTER_CODEGEN_VERSION_SPEC}"
            cluster-lister-gen"${CLUSTER_CODEGEN_VERSION_SPEC}"
        )
        # shellcheck disable=2046 # printf word-splitting is intentional
        GO111MODULE=on go install $(printf "github.com/kcp-dev/code-generator/v3/cmd/%s " "${BINS[@]}")
    )
    # Go installs in $GOBIN if defined, and $GOPATH/bin otherwise
    gobin="${GOBIN:-$(go env GOPATH)/bin}"

    local group_versions=()
    local input_pkgs=()
    while read -r dir; do
        pkg="$(cd "${dir}" && GO111MODULE=on go list -find .)"
        leaf="$(basename "${dir}")"
        if grep -E -q '^v[0-9]+((alpha|beta)[0-9]+)?$' <<< "${leaf}"; then
            input_pkgs+=("${pkg}")

            dir2="$(dirname "${dir}")"
            leaf2="$(basename "${dir2}")"
            gv="${leaf2}/${leaf}"

            if [[ " ${exclude_group_versions} " == *" ${gv} "* ]]; then
              continue
            fi

            group_versions+=("${gv}")
        fi
    done < <(
        ( cluster::codegen::internal::grep -l --null \
            -e '^\s*//\s*+genclient' \
            -r "${in_dir}${one_input_api}" \
            --include '*.go' \
            || true \
        ) | while read -r -d $'\0' F; do dirname "${F}"; done \
          | LC_ALL=C sort -u
    )

    if [ "${#group_versions[@]}" == 0 ]; then
        return 0
    fi

    echo "Generating cluster client code for ${#group_versions[@]} targets"

    if [[ -z "${versioned_clientset_dir}" ]]; then
        if [ -z "${out_dir}" ]; then
            echo "--output-dir is required when no --versioned-clientset-dir is provided" >&2
            return 1
        fi
        versioned_clientset_dir="${out_dir}/clientset/versioned"
    fi

    if [[ -z "${versioned_clientset_pkg}" ]]; then
        if [ -z "${out_pkg}" ]; then
            echo "--output-pkg is required when no --versioned-clientset-pkg is provided" >&2
            return 1
        fi
        versioned_clientset_pkg="${out_pkg}/clientset/versioned"
    fi

    ( cluster::codegen::internal::grep -l --null \
        -e '^// Code generated by cluster-client-gen. DO NOT EDIT.$' \
        -r "${versioned_clientset_dir}" \
        --include '*.go' \
        || true \
    ) | xargs -0 rm -f

    local inputs=()
    for arg in "${group_versions[@]}"; do
        inputs+=("--input" "$arg")
    done
    "${gobin}/cluster-client-gen" \
        -v "${v}" \
        --go-header-file "${boilerplate}" \
        --output-dir "${versioned_clientset_dir}" \
        --output-pkg "${versioned_clientset_pkg}" \
        --input-base "$(cd "${in_dir}" && pwd -P)" `# must be absolute path or Go import path"` \
        --single-cluster-versioned-clientset-pkg "${single_cluster_versioned_clientset_pkg}" \
        --single-cluster-applyconfigurations-pkg "${single_cluster_applyconfigurations_pkg}" \
        --plural-exceptions "${plural_exceptions}" \
        "${inputs[@]}"

    if [ "${watchable}" == "true" ]; then
        echo "Generating cluster lister code for ${#input_pkgs[@]} targets"

        if [[ -z "${listers_dir}" ]]; then
            if [ -z "${out_dir}" ]; then
                echo "--output-dir is required when no --listers-dir is provided" >&2
                return 1
            fi
            listers_dir="${out_dir}/listers"
        fi

        if [[ -z "${listers_pkg}" ]]; then
            if [ -z "${out_pkg}" ]; then
                echo "--output-pkg is required when no --listers-pkg is provided" >&2
                return 1
            fi
            listers_pkg="${out_pkg}/listers"
        fi

        ( cluster::codegen::internal::grep -l --null \
            -e '^// Code generated by cluster-lister-gen. DO NOT EDIT.$' \
            -r "${listers_dir}" \
            --include '*.go' \
            || true \
        ) | xargs -0 rm -f

        "${gobin}/cluster-lister-gen" \
            -v "${v}" \
            --go-header-file "${boilerplate}" \
            --output-dir "${listers_dir}" \
            --output-pkg "${listers_pkg}" \
            --single-cluster-listers-pkg "${single_cluster_listers_pkg}" \
            --plural-exceptions "${plural_exceptions}" \
            "${input_pkgs[@]}"

        echo "Generating cluster informer code for ${#input_pkgs[@]} targets"

        if [[ -z "${informers_dir}" ]]; then
            if [ -z "${out_dir}" ]; then
                echo "--output-dir is required when no --informers-dir is provided" >&2
                return 1
            fi
            informers_dir="${out_dir}/informers"
        fi

        if [[ -z "${informers_pkg}" ]]; then
            if [ -z "${out_pkg}" ]; then
                echo "--output-pkg is required when no --informers-pkg is provided" >&2
                return 1
            fi
            informers_pkg="${out_pkg}/informers"
        fi

        ( cluster::codegen::internal::grep -l --null \
            -e '^// Code generated by cluster-informer-gen. DO NOT EDIT.$' \
            -r "${informers_dir}" \
            --include '*.go' \
            || true \
        ) | xargs -0 rm -f

        "${gobin}/cluster-informer-gen" \
            -v "${v}" \
            --go-header-file "${boilerplate}" \
            --output-dir "${informers_dir}" \
            --output-pkg "${informers_pkg}" \
            --versioned-clientset-pkg "${versioned_clientset_pkg}" \
            --listers-pkg "${listers_pkg}" \
            --single-cluster-listers-pkg "${single_cluster_listers_pkg}" \
            --single-cluster-informers-pkg "${single_cluster_informers_pkg}" \
            --single-cluster-versioned-clientset-pkg "${single_cluster_versioned_clientset_pkg}" \
            --plural-exceptions "${plural_exceptions}" \
            "${input_pkgs[@]}"
    fi
}
