# coding: utf-8

# Codebase
# This file contains definitions of the source files
# including generated ones. It defines convenient file
# lists to use as pre-requirements in the next stages.
# It installs the source code dependencies too.

############
### Date ###
############

require 'date'

now = Time.now
CURRENT_DATE = now.strftime("%Y-%m-%d %H:%M")

if ENV['STORK_BUILD_TIMESTAMP']
    TIMESTAMP = ENV['STORK_BUILD_TIMESTAMP']
else
    TIMESTAMP = now.strftime("%y%m%d%H%M%S")
end

#############
### Clean ###
#############

# Ruby has a built-in solution for handling CLEAN and CLOBBER arrays and
# deleting unnecessary files. But loading the 'rake' module significantly reduces
# the performance. For these reason we implement the clean and clobber tasks
# ourselves.
#
# Clean up the project by deleting scratch files and backup files. Add files to
# the CLEAN FileList to have the clean target handle them.
# Unlike the standard Rake Clean task, this implementation recursively removes
# the directories.
CLEAN = FileList[]
# Clobber all generated and non-source files in a project. The task depends on
# clean, so all the CLEAN files will be deleted as well as files in the CLOBBER
# FileList. The intent of this task is to return a project to its pristine,
# just unpacked state.
CLOBBER = FileList[]

# Ruby bundler local file
CLOBBER.append "rakelib/init_debs/.bundle/config"

################
### Code-gen ###
################

go_code_gen_codebase = FileList[
    "backend/codegen",
    "backend/codegen/*",
    "backend/cmd/stork-code-gen",
    "backend/cmd/stork-code-gen/*",
]

go_code_gen_codebase_without_binary = go_code_gen_codebase
    .exclude("backend/cmd/stork-code-gen/stork-code-gen")

CODE_GEN_BINARY_FILE = "backend/cmd/stork-code-gen/stork-code-gen"
file CODE_GEN_BINARY_FILE => go_code_gen_codebase_without_binary + [GO] do
    Dir.chdir("backend/cmd/stork-code-gen") do
        sh GO, "build", "-ldflags=-X 'isc.org/stork.BuildDate=#{CURRENT_DATE}'"
    end
    puts "Stork Code Gen build date: #{CURRENT_DATE} (timestamp: #{TIMESTAMP})"
end
CLEAN.append CODE_GEN_BINARY_FILE

###############
### Swagger ###
###############

SWAGGER_FILE = 'api/swagger.yaml'
swagger_api_files = FileList['api/*.yaml'].exclude(SWAGGER_FILE)
file SWAGGER_FILE => swagger_api_files + [YAMLINC] do
    sh YAMLINC, "-o", SWAGGER_FILE, "api/swagger.in.yaml"

    # Check if the readOnly attribute is not used.
    File.readlines(SWAGGER_FILE).each do |line|
        if line.match /^\s*readOnly: true\s*$/
            FileUtils.rm_rf SWAGGER_FILE
            # GoSwagger supports the "readOnly" property from the 0.24 version. The fields
            # marked as read-only cannot be included in the requests. The GoSwagger doesn't
            # have the switch to disable validation of the read-only fields.
            # The Stork UI often uses the schema: receive the data from the server, modify
            # it, and send it back. Unfortunately, the client generated by the OpenAPI
            # generator doesn't handle the read-only properties in any way. There also is
            # no utility to strip the read-only properties.
            fail "The 'readOnly: true' directive was used in the Swagger YAML file. " +
                "It is denied to use it in Stork due to not handling this " +
                "parameter by UI and system test OpenAPI clients."
        end
    end
end
CLEAN.append SWAGGER_FILE

#####################################
### JSON definitions for code-gen ###
#####################################

std_dhcpv4_option_definitions_json = "codegen/std_dhcpv4_option_def.json"
std_dhcpv6_option_definitions_json = "codegen/std_dhcpv6_option_def.json"

###############
### Backend ###
###############

swagger_server_dir = "backend/server/gen"
file swagger_server_dir => [GOSWAGGER, SWAGGER_FILE] do
    swagger_abs = File.expand_path(SWAGGER_FILE)
    Dir.chdir("backend") do
        sh GOSWAGGER, "generate", "server",
        "-m", "server/gen/models",
        "-s", "server/gen/restapi",
        "--exclude-main",
        "--name", "Stork",
        "--regenerate-configureapi",
        "--spec", swagger_abs,
        "--template", "stratoscale"
    end
    sh "touch", "-c", swagger_server_dir

    # The Stratoscale template generates the go generate directives for mockery.
    # These directives are obsolete because the mockery recommends now using
    # the packages config.
    # However, we don't use these mocks in the project, so we strip the
    # directives to avoid the warnings.
    #
    # The following warnings are printed by the mockery tool multiple times:
    #
    # WRN DEPRECATION: use of the packages config will be the only way to
    # generate mocks in v3. Please migrate your config to use the packages
    # feature. deprecation-name=packages
    # migration=https://vektra.github.io/mockery/v2.51/migrating_to_packages/
    # url=https://vektra.github.io/mockery/v2.51/features/#packages-configuration version=v2.51.0
    #
    # WRN DEPRECATION: issue-845-fix must be set to True to remove this
    # warning. Visit the link for more details. deprecation-name=issue-845-fix
    # url=https://vektra.github.io/mockery/v2.51/deprecations/#issue-845-fix
    # version=v2.51.0

    swagger_server_configure_file = File.join(swagger_server_dir, "restapi/configure_stork.go")
    text = File.read(swagger_server_configure_file)
    new_contents = text.gsub(
        /\/\/go:generate mockery .*/,
        '')
    File.open(swagger_server_configure_file, "w") {|file| file.puts new_contents }
end
CLEAN.append swagger_server_dir

agent_proto_file = "backend/api/agent.proto"
agent_pb_go_file = "backend/api/agent.pb.go"
agent_grpc_pb_go_file = "backend/api/agent_grpc.pb.go"
file agent_pb_go_file => [agent_proto_file, PROTOC, PROTOC_GEN_GO, PROTOC_GEN_GO_GRPC] do
    Dir.chdir("backend/api") do
        sh PROTOC, "--proto_path=.", "--go_out=.", "--go-grpc_out=.", "agent.proto"
    end
end
file agent_grpc_pb_go_file => [agent_pb_go_file]
CLEAN.append agent_pb_go_file, agent_grpc_pb_go_file

std_option_defs6_go_file = "backend/appcfg/kea/stdoptiondef6.go"
file std_option_defs6_go_file => [GO, CODE_GEN_BINARY_FILE, std_dhcpv6_option_definitions_json] do
    sh CODE_GEN_BINARY_FILE, "std-option-defs",
    "--input", std_dhcpv6_option_definitions_json,
    "--output", std_option_defs6_go_file,
    "--template", "backend/appcfg/kea/stdoptiondef6.go.template"
    Dir.chdir('backend') do
        sh GO, "fmt", "./appcfg/kea/..."
    end
end

std_option_defs4_go_file = "backend/appcfg/kea/stdoptiondef4.go"
file std_option_defs4_go_file => [GO, CODE_GEN_BINARY_FILE, std_dhcpv4_option_definitions_json] do
    sh CODE_GEN_BINARY_FILE, "std-option-defs",
    "--input", std_dhcpv4_option_definitions_json,
    "--output", std_option_defs4_go_file,
    "--template", "backend/appcfg/kea/stdoptiondef4.go.template"
    Dir.chdir('backend') do
        sh GO, "fmt", "./appcfg/kea/..."
    end
end

# Go dependencies are installed automatically during build
# or can be triggered manually.
CLOBBER.append File.join(ENV["GOPATH"], "pkg")

go_server_codebase = FileList[
    "backend/server",
    "backend/server/**/*",
    "backend/cmd/stork-server",
    "backend/cmd/stork-server/*",
    swagger_server_dir
]
.exclude(swagger_server_dir + "/**/*")

go_agent_codebase = FileList[
    "backend/agent",
    "backend/agent/**/*",
    "backend/cmd/stork-agent",
    "backend/cmd/stork-agent/*",
    "backend/server/certs/**/*",
    "backend/server/database/**/*"
]

go_tool_codebase = FileList[
    "backend/cmd/stork-tool",
    "backend/cmd/stork-tool/*",
    "backend/server/database/migrations/*",
    "backend/server/database/*.go",
    "backend/server/database/test/*"
]

go_common_codebase = FileList["backend/**/*"]
    .exclude("backend/coverage.*")
    .exclude(swagger_server_dir + "/**/*")
    .exclude(go_server_codebase)
    .exclude(go_agent_codebase)
    .exclude(go_tool_codebase)
    .exclude(go_code_gen_codebase)
    .include(agent_pb_go_file)
    .include(agent_grpc_pb_go_file)
    .include(std_option_defs4_go_file)
    .include(std_option_defs6_go_file)

GO_SERVER_CODEBASE = go_server_codebase
        .include(go_common_codebase)
        .exclude("backend/cmd/stork-server/stork-server*")
        .exclude("backend/**/*mock_test.go")

GO_AGENT_CODEBASE = go_agent_codebase
        .include(go_common_codebase)
        .exclude("backend/cmd/stork-agent/stork-agent*")
        .exclude("backend/**/*mock_test.go")

GO_TOOL_CODEBASE = go_tool_codebase
        .include(go_common_codebase)
        .exclude("backend/cmd/stork-tool/stork-tool*")
        .exclude("backend/**/*mock_test.go")

CLEAN.append *FileList["backend/**/*mock_test.go"]

##############
### Python ###
##############

agent_pb_python_file = "tests/system/agent_pb2.py"
file agent_pb_python_file => [agent_proto_file, PROTOC, PROTOC_GEN_PYTHON_GRPC] do
    Dir.chdir("backend/api") do
        sh PROTOC,
            "--proto_path=.",
            "--python_out=.",
            "--grpc_python_out=.",
            "--plugin=protoc-gen-grpc_python=#{PROTOC_GEN_PYTHON_GRPC}",
            "agent.proto"
    end
    sh "mv", *FileList["backend/api/*.py"], "tests/system"
    sh "touch", "-c", agent_pb_python_file
end
CLEAN.append agent_pb_python_file

agent_grpc_python_file = "tests/system/agent_grpc.py"
file agent_grpc_python_file => [agent_pb_python_file] do
    sh "touch", "-c", agent_grpc_python_file
end
CLEAN.append agent_grpc_python_file

GRPC_PYTHON_API_FILES = [agent_pb_python_file, agent_grpc_python_file]

OPEN_API_GENERATOR_PYTHON_DIR = "tests/system/openapi_client"
file OPEN_API_GENERATOR_PYTHON_DIR => [JAVA, SWAGGER_FILE, OPENAPI_GENERATOR] do
    sh "rm", "-rf", OPEN_API_GENERATOR_PYTHON_DIR
    sh JAVA, "-jar", OPENAPI_GENERATOR, "generate",
        "-i", SWAGGER_FILE,
        "-g", "python",
        "-o", "tests/system",
        "--global-property", "apiTests=false,modelTests=false",
        "--additional-properties", "generateSourceCodeOnly=true"
    sh "touch", OPEN_API_GENERATOR_PYTHON_DIR
end

CLEAN.append OPEN_API_GENERATOR_PYTHON_DIR, "tests/system/.openapi-generator",
    "tests/system/.openapi-generator", "tests/system/openapi_client_README.md",
    "tests/system/.openapi-generator-ignore",  *FileList["tests/system/**/__pycache__"],
    *FileList["tests/system/**/.pytest_cache"]

#####################
### Documentation ###
#####################

# File used in the below workarounds when there are no hooks. It describes the
# reason why there is no entries and provides a title for the placeholder
# entry.
dummy_hook_doc_file = "doc/user/no-hook.irst"

# Collects the documentation of the cloned hooks. It may be empty if no hooks
# are available in the hooks directory.
hook_doc_directory = "doc/user/hooks"
file hook_doc_directory => [dummy_hook_doc_file] + FileList["hooks/**/doc/**/*"] do
    sh "rm", "-rf", hook_doc_directory
    sh "mkdir", "-p", hook_doc_directory

    if Dir.exist?("hooks")
        Dir.each_child("hooks") do |hook|
            if !Dir.exist?(File.join("hooks", hook, "doc"))
                # The hook documentation is not available.
                next
            end

            hook_subdirectory = File.join(hook_doc_directory, hook)
            sh "mkdir", "-p", hook_subdirectory
            sh "cp", "-r",
                *FileList[File.join("hooks", hook, "doc/")],
                hook_subdirectory
        end
    end

    # Workaround for Sphinx warning that reports the non-matching glob pattern.
    # It interrupts build when there are no hooks. I reported the issue to the
    # Sphinx project: https://github.com/sphinx-doc/sphinx/pull/13230
    if Dir.empty?(hook_doc_directory)
        sh "mkdir", "-p", File.join(hook_doc_directory, "dummy")
        sh "cp", dummy_hook_doc_file, File.join(hook_doc_directory, "dummy", "index.rst")
    end
end

hook_man_directory = "doc/user/man/hooks"
file hook_man_directory => [dummy_hook_doc_file] + FileList["hooks/**/man/man.8.rst"] do
    sh "rm", "-rf", hook_man_directory
    sh "mkdir", "-p", hook_man_directory

    if Dir.exist?("hooks")
        Dir.each_child("hooks") do |hook|
            hook_man_path = File.join("hooks", hook, "man", "man.8.rst")
            if !File.exist?(File.join(hook_man_path))
                # The hook man is not available.
                next
            end

            sh "cp", hook_man_path, File.join(hook_man_directory, hook + ".8.rst")
        end
    end

    # Workaround for Sphinx warning that reports the non-matching glob pattern.
    # It interrupts build when there is no hooks. I reported the issue to the
    # Sphinx project: https://github.com/sphinx-doc/sphinx/pull/13230
    if Dir.empty?(hook_man_directory)
        sh "cp", dummy_hook_doc_file, File.join(hook_man_directory, "dummy.8.rst")
    end
end

DOC_USER_CODEBASE = FileList[
    "doc/static/**/*",
    "doc/user/**/*",
    "backend/version.go",
    hook_doc_directory,
    hook_man_directory,
]

DOC_DEV_CODEBASE = FileList["doc/static/**/*", "doc/dev/**/*", "backend/version.go"]

################
### Frontend ###
################

open_api_generator_webui_dir = "webui/src/app/backend"
file open_api_generator_webui_dir => [JAVA, SWAGGER_FILE, OPENAPI_GENERATOR] do
    sh "rm", "-rf", open_api_generator_webui_dir
    sh JAVA, "-jar", OPENAPI_GENERATOR, "generate",
    "-i", SWAGGER_FILE,
    "-g", "typescript-angular",
    "-o", open_api_generator_webui_dir,
    "--additional-properties", "snapshot=true,ngVersion=10.1.5,modelPropertyNaming=camelCase"
    sh "touch", "-c", open_api_generator_webui_dir
end
CLEAN.append open_api_generator_webui_dir

node_module_dir = "webui/node_modules"
file node_module_dir => [CLANGPLUSPLUS, NPM, "webui/package.json", "webui/package-lock.json"] do
    ci_opts = []
    if ENV["CI"] == "true"
        ci_opts += ["--no-audit", "--no-progress"]
    end

    Dir.chdir("webui") do
        ENV["NG_CLI_ANALYTICS"] = "false"

        if OS == "OpenBSD"
            # The clang++ is required but instead what is actually used is g++.
            # See: https://obsd.solutions/en/blog/2022/02/23/node-sass-build-fails-on-openbsd-how-to-fix/
            ENV["CXX"] = CLANGPLUSPLUS
        end

        sh NPM, "ci",
                "--prefer-offline",
                *ci_opts
    end
    sh "touch", "-c", node_module_dir
end
CLOBBER.append node_module_dir


std_option_defs6_ts_file = "webui/src/app/std-dhcpv6-option-defs.ts"
file std_option_defs6_ts_file => [NPX, node_module_dir, CODE_GEN_BINARY_FILE, std_dhcpv6_option_definitions_json] do
    sh CODE_GEN_BINARY_FILE, "std-option-defs",
        "--input", std_dhcpv6_option_definitions_json,
        "--output", std_option_defs6_ts_file,
        "--template", "webui/src/app/std-dhcpv6-option-defs.ts.template"
    Dir.chdir('webui') do
        sh NPX, "prettier", "--config", ".prettierrc", "--write", "src/app/std-dhcpv6-option-defs.ts"
    end
end

std_option_defs4_ts_file = "webui/src/app/std-dhcpv4-option-defs.ts"
file std_option_defs4_ts_file => [NPX, node_module_dir, CODE_GEN_BINARY_FILE, std_dhcpv4_option_definitions_json] do
    sh CODE_GEN_BINARY_FILE, "std-option-defs",
        "--input", std_dhcpv4_option_definitions_json,
        "--output", std_option_defs4_ts_file,
        "--template", "webui/src/app/std-dhcpv4-option-defs.ts.template"
    Dir.chdir('webui') do
        sh NPX, "prettier", "--config", ".prettierrc", "--write", "src/app/std-dhcpv4-option-defs.ts"
    end

end

WEBUI_CODEBASE = FileList["webui", "webui/**/*"]
    .exclude("webui/.angular")
    .exclude("webui/.angular/**/*")
    .exclude("webui/node_modules/**/*")
    .exclude(File.join(open_api_generator_webui_dir, "**/*"))
    .exclude("webui/dist")
    .exclude("webui/dist/**/*")
    .exclude("webui/src/assets/arm")
    .exclude("webui/src/assets/arm/**/*")
    .include(open_api_generator_webui_dir)
    .include(node_module_dir)
    .include(std_option_defs4_ts_file)
    .include(std_option_defs6_ts_file)

#############
### Tasks ###
#############

def remove_files(list)
    list.each do |item|
        FileUtils.rm_rf(item)
    end
end

namespace :clean do
    desc 'Clean up the project by deleting scratch files and backup files'
    task :soft do
        remove_files(CLEAN)
    end

    desc 'Clobber all generated and non-source files in a project.'
    task :hard => [:soft] do
        remove_files(CLOBBER)
    end
end

namespace :gen do
    namespace :backend do
        desc 'Generate Swagger API files'
        task :swagger => [swagger_server_dir]

        desc 'Generate standard DHCP option definitions for the backend'
        task :std_option_defs => [std_option_defs4_go_file, std_option_defs6_go_file]
    end

    namespace :ui do
        desc 'Generate standard DHCP option definitions for the UI'
        task :std_option_defs => [std_option_defs4_ts_file, std_option_defs6_ts_file]

        desc 'Generate Swagger API files'
        task :swagger => [open_api_generator_webui_dir]
    end

    desc 'Generate standard DHCP option definitions for the backend and the UI'
    task :std_option_defs => ["gen:ui:std_option_defs", "gen:backend:std_option_defs"]
end

namespace :prepare do
    desc 'Install the external dependencies related to the codebase'
    task :codebase do
        find_and_prepare_deps(__FILE__)
    end

    desc 'Trigger the backend (GO) dependencies installation.'
    task :backend_deps => [GO] do
        Dir.chdir("backend") do
            sh GO, "mod", "download"
        end
    end

    desc 'Trigger the frontend (UI) dependencies installation'
    task :ui_deps => [node_module_dir]

    desc 'Trigger the frontend (UI) and backend (GO) dependencies installation'
    task :deps => [:ui_deps, :backend_deps]

    desc 'Prepare files needed for documentation building'
    task :doc => [hook_doc_directory, hook_man_directory]
end

namespace :check do
    desc 'Check the external dependencies related to the codebase'
    task :codebase do
        check_deps(__FILE__)
    end
end
