# Copyright 2013 The Flutter Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

import("//build/compiled_action.gni")
import("//flutter/common/config.gni")
import("//flutter/impeller/tools/malioc.gni")
import("//flutter/testing/testing.gni")

declare_args() {
  impeller_debug =
      flutter_runtime_mode == "debug" || flutter_runtime_mode == "profile"

  # Whether the Metal backend is enabled.
  impeller_enable_metal = is_mac || is_ios

  # Whether the OpenGLES backend is enabled.
  impeller_enable_opengles = is_linux || is_win || is_android

  # Whether the Vulkan backend is enabled.
  impeller_enable_vulkan = is_linux || is_win || is_android

  # Whether to use a prebuilt impellerc.
  # If this is the empty string, impellerc will be built.
  # If it is non-empty, it should be the absolute path to impellerc.
  impeller_use_prebuilt_impellerc = ""

  # Whether to use a prebuilt scenec.
  # If this is the empty string, scenec will be built.
  # If it is non-empty, it should be the absolute path to scenec.
  impeller_use_prebuilt_scenec = ""

  # If enabled, all OpenGL calls will be traced. Because additional trace
  # overhead may be substantial, this is not enabled by default.
  impeller_trace_all_gl_calls = false

  # Call glGetError after each OpenGL call and log failures.
  impeller_error_check_all_gl_calls = is_debug

  # Enable experimental 3D scene rendering.
  impeller_enable_3d = false
}

declare_args() {
  # Whether Impeller supports rendering on the platform.
  impeller_supports_rendering =
      impeller_enable_metal || impeller_enable_opengles ||
      impeller_enable_vulkan

  impeller_enable_compute = impeller_enable_vulkan || impeller_enable_metal
}

# ------------------------------------------------------------------------------
# @brief           Define an Impeller component. Components are different
#                  Impeller subsystems part of the umbrella framework.
#
# @param[optional] target_type  The type of the component. This can be any of
#                               the target types supported by GN. Defaults to
#                               source_set. If Impeller is not supported on the
#                               target platform, this target is a no-op.
#
template("impeller_component") {
  if (defined(invoker.testonly) && invoker.testonly && !enable_unittests) {
    group(target_name) {
      not_needed(invoker, "*")
    }
  } else {
    target_type = "source_set"
    if (defined(invoker.target_type)) {
      target_type = invoker.target_type
    }
    target(target_type, target_name) {
      forward_variables_from(invoker, "*")

      if (!defined(invoker.public_configs)) {
        public_configs = []
      }

      public_configs += [ "//flutter/impeller:impeller_public_config" ]

      if (!defined(invoker.cflags)) {
        cflags = []
      }
      cflags += [ "-Wthread-safety-analysis" ]

      if (!defined(invoker.cflags_objc)) {
        cflags_objc = []
      }

      if (is_ios || is_mac) {
        cflags_objc += flutter_cflags_objc_arc
      }

      if (!defined(invoker.cflags_objcc)) {
        cflags_objcc = []
      }

      if (is_ios || is_mac) {
        cflags_objcc += flutter_cflags_objcc_arc
      }
    }
  }
}

# ------------------------------------------------------------------------------
# @brief           Build a Metal Library. The output is a single file. Use
#                  get_target_outputs to get its location on build.
#
# @param[required] name     The name of the Metal library.
#
# @param[required] sources  The GLSL (4.60) sources to compiled into the Metal
#                           library.
#
# @param[required] metal_version  The Metal language version to compile for.
#
template("metal_library") {
  assert(is_ios || is_mac)
  assert(defined(invoker.name), "Metal library name must be specified.")
  assert(defined(invoker.sources), "Metal source files must be specified.")
  assert(defined(invoker.metal_version),
         "Metal language version must be specified.")

  metal_library_name = invoker.name

  action("$target_name") {
    forward_variables_from(invoker,
                           "*",
                           [
                             "inputs",
                             "outputs",
                             "script",
                             "depfile",
                             "args",
                           ])

    inputs = invoker.sources

    metal_library_path = "$root_out_dir/shaders/$metal_library_name.metallib"
    metal_library_symbols_path =
        "$root_out_dir/shaders/$metal_library_name.metallibsym"

    outputs = [
      metal_library_path,
      metal_library_symbols_path,
    ]

    script = "//flutter/impeller/tools/build_metal_library.py"

    depfile = "$target_gen_dir/mtl/$metal_library_name.depfile"

    args = [
      "--output",
      rebase_path(metal_library_path, root_out_dir),
      "--depfile",
      rebase_path(depfile),
      "--metal-version=$metal_version",
    ]

    if (is_ios) {
      if (use_ios_simulator) {
        args += [ "--platform=ios-simulator" ]
      } else {
        args += [ "--platform=ios" ]
      }
    } else if (is_mac) {
      args += [ "--platform=mac" ]
    } else {
      assert(false, "Unsupported platform for generating Metal shaders.")
    }

    foreach(source, invoker.sources) {
      args += [
        "--source",
        rebase_path(source, root_out_dir),
      ]
    }
  }
}

template("embed_blob") {
  assert(defined(invoker.symbol_name), "The symbol name must be specified.")
  assert(defined(invoker.blob), "The blob file to embed must be specified")
  assert(defined(invoker.hdr),
         "The header file containing the symbol name must be specified.")
  assert(defined(invoker.cc),
         "The CC file containing the symbol data must be specified.")
  assert(defined(invoker.deps), "The target dependencies must be specified")

  gen_blob_target_name = "gen_blob_$target_name"
  action(gen_blob_target_name) {
    inputs = [ invoker.blob ]
    outputs = [
      invoker.hdr,
      invoker.cc,
    ]
    args = [
      "--symbol-name",
      invoker.symbol_name,
      "--output-header",
      rebase_path(invoker.hdr),
      "--output-source",
      rebase_path(invoker.cc),
      "--source",
      rebase_path(invoker.blob),
    ]
    script = "//flutter/impeller/tools/xxd.py"
    deps = invoker.deps
  }

  embed_config = "embed_$target_name"
  config(embed_config) {
    include_dirs = [ get_path_info(
            get_label_info("//flutter/impeller:impeller", "target_gen_dir"),
            "dir") ]
  }

  source_set(target_name) {
    public_configs = [ ":$embed_config" ]
    sources = get_target_outputs(":$gen_blob_target_name")
    deps = [ ":$gen_blob_target_name" ]
  }
}

# Dispatches to the build or prebuilt impellerc depending on the value of
# the impeller_use_prebuilt_impellerc argument. Forwards all variables to
# compiled_action_foreach or action_foreach as appropriate.
template("_impellerc") {
  if (impeller_use_prebuilt_impellerc == "") {
    compiled_action_foreach(target_name) {
      forward_variables_from(invoker, "*")
      tool = "//flutter/impeller/compiler:impellerc"
    }
  } else {
    action_foreach(target_name) {
      forward_variables_from(invoker, "*", [ "args" ])
      script = "//build/gn_run_binary.py"
      impellerc_path =
          rebase_path(impeller_use_prebuilt_impellerc, root_build_dir)
      args = [ impellerc_path ] + invoker.args
    }
  }
}

template("impellerc") {
  assert(defined(invoker.shaders), "Impeller shaders must be specified.")
  assert(defined(invoker.shader_target_flag),
         "The flag to impellerc for target selection must be specified.")
  assert(defined(invoker.sl_file_extension),
         "The extension of the SL file must be specified (metal, glsl, etc..).")

  sksl = invoker.shader_target_flag == "--sksl"
  iplr = false
  if (defined(invoker.iplr) && invoker.iplr) {
    iplr = invoker.iplr
  }
  json = false
  if (defined(invoker.json) && invoker.json) {
    json = invoker.json
  }

  # Not needed on every path.
  not_needed([
               "iplr",
               "sksl",
             ])

  # Optional: invoker.iplr Causes --sl output to be in iplr format.
  # Optional: invoker.defines specifies a list of valueless macro definitions.
  # Optional: invoker.intermediates_subdir specifies the subdirectory in which
  #           to put intermediates.
  # Optional: invoker.json Causes output format to be JSON instead of flatbuffer.

  _impellerc(target_name) {
    sources = invoker.shaders
    if (defined(invoker.intermediates_subdir)) {
      subdir = invoker.intermediates_subdir
      generated_dir = "$target_gen_dir/$subdir"
    } else {
      generated_dir = "$target_gen_dir"
    }

    shader_target_flag = invoker.shader_target_flag

    spirv_intermediate = "$generated_dir/{{source_file_part}}.spirv"
    spirv_intermediate_path = rebase_path(spirv_intermediate, root_build_dir)

    depfile_path = "$generated_dir/{{source_file_part}}.d"
    depfile_intermediate_path = rebase_path(depfile_path, root_build_dir)
    depfile = depfile_path

    shader_lib_dir = rebase_path("//flutter/impeller/compiler/shader_lib")
    args = [
      "--input={{source}}",
      "--include={{source_dir}}",
      "--include=$shader_lib_dir",
      "--depfile=$depfile_intermediate_path",
      "$shader_target_flag",
    ]

    if (defined(invoker.gles_language_version)) {
      gles_language_version = invoker.gles_language_version
      args += [ "--gles-language-version=$gles_language_version" ]
    }

    if (defined(invoker.metal_version)) {
      assert(is_mac || is_ios)
      metal_version = invoker.metal_version
      args += [ "--metal-version=$metal_version" ]
    }

    if (defined(invoker.use_half_textures) && invoker.use_half_textures) {
      args += [ "--use-half-textures" ]
    }

    if (json) {
      args += [ "--json" ]
    }

    if (sksl) {
      sl_intermediate =
          "$generated_dir/{{source_file_part}}.${invoker.sl_file_extension}"
      sl_intermediate_path = rebase_path(sl_intermediate, root_build_dir)
      args += [
        "--sl=$sl_intermediate_path",
        "--spirv=$spirv_intermediate_path",
      ]
      if (iplr) {
        args += [ "--iplr" ]
      }

      outputs = [ sl_intermediate ]
    } else {
      sl_intermediate =
          "$generated_dir/{{source_file_part}}.${invoker.sl_file_extension}"
      reflection_json_intermediate = "$generated_dir/{{source_file_part}}.json"
      reflection_header_intermediate = "$generated_dir/{{source_file_part}}.h"
      reflection_cc_intermediate = "$generated_dir/{{source_file_part}}.cc"

      sl_intermediate_path = rebase_path(sl_intermediate, root_build_dir)
      reflection_json_path =
          rebase_path(reflection_json_intermediate, root_build_dir)
      reflection_header_path =
          rebase_path(reflection_header_intermediate, root_build_dir)
      reflection_cc_path =
          rebase_path(reflection_cc_intermediate, root_build_dir)

      args += [
        "--sl=$sl_intermediate_path",
        "--spirv=$spirv_intermediate_path",
        "--reflection-json=$reflection_json_path",
        "--reflection-header=$reflection_header_path",
        "--reflection-cc=$reflection_cc_path",
      ]
      if (iplr) {
        args += [ "--iplr" ]
      }

      outputs = [
        sl_intermediate,
        reflection_header_intermediate,
        reflection_cc_intermediate,
      ]
    }

    if (defined(invoker.defines)) {
      foreach(def, invoker.defines) {
        args += [ "--define=$def" ]
      }
    }
  }
}

template("impellerc_reflect") {
  assert(
      defined(invoker.impellerc_invocation),
      "The target that specifies the ImpellerC invocation to reflect must be defined.")

  reflect_config = "reflect_$target_name"
  config(reflect_config) {
    include_dirs = [ get_path_info(
            get_label_info("//flutter/impeller:impeller", "target_gen_dir"),
            "dir") ]
  }

  impellerc_invocation = invoker.impellerc_invocation

  source_set(target_name) {
    public_configs = [ ":$reflect_config" ]
    public = filter_include(get_target_outputs(impellerc_invocation), [ "*.h" ])
    sources = filter_include(get_target_outputs(impellerc_invocation),
                             [
                               "*.h",
                               "*.cc",
                               "*.mm",
                             ])

    deps = [
      "//flutter/impeller/core",
      impellerc_invocation,
    ]
  }
}

template("_impeller_shaders_metal") {
  assert(defined(invoker.shaders), "Impeller shaders must be specified.")
  assert(defined(invoker.name), "Name of the shader library must be specified.")

  metal_version = "1.2"
  if (defined(invoker.metal_version)) {
    metal_version = invoker.metal_version
  }

  use_half_textures = false
  if (defined(invoker.use_half_textures) && invoker.use_half_textures) {
    use_half_textures = invoker.use_half_textures
  }

  shaders_base_name = string_join("",
                                  [
                                    invoker.name,
                                    "_shaders",
                                  ])
  impellerc_mtl = "impellerc_$target_name"
  impellerc(impellerc_mtl) {
    shaders = invoker.shaders
    metal_version = metal_version
    sl_file_extension = "metal"
    use_half_textures = use_half_textures
    shader_target_flag = ""
    defines = [ "IMPELLER_TARGET_METAL" ]
    if (is_ios) {
      shader_target_flag = "--metal-ios"
      defines += [ "IMPELLER_TARGET_METAL_IOS" ]
    } else if (is_mac) {
      shader_target_flag = "--metal-desktop"
      defines += [ "IMPELLER_TARGET_METAL_DESKTOP" ]
    } else {
      assert(false, "Metal not supported on this platform.")
    }
  }

  mtl_lib = "genlib_$target_name"
  metal_library(mtl_lib) {
    name = invoker.name
    metal_version = metal_version
    sources =
        filter_include(get_target_outputs(":$impellerc_mtl"), [ "*.metal" ])
    deps = [ ":$impellerc_mtl" ]
  }

  reflect_mtl = "reflect_$target_name"
  impellerc_reflect(reflect_mtl) {
    impellerc_invocation = ":$impellerc_mtl"
  }

  embed_mtl_lib = "embed_$target_name"
  embed_blob(embed_mtl_lib) {
    metal_library_files = get_target_outputs(":$mtl_lib")
    symbol_name = shaders_base_name
    blob = metal_library_files[0]
    hdr = "$target_gen_dir/mtl/$shaders_base_name.h"
    cc = "$target_gen_dir/mtl/$shaders_base_name.cc"
    deps = [ ":$mtl_lib" ]
  }

  group(target_name) {
    public_deps = [
      ":$embed_mtl_lib",
      ":$reflect_mtl",
    ]
  }
}

template("blobcat_library") {
  assert(defined(invoker.shaders),
         "The shaders to build the library from must be specified.")
  assert(defined(invoker.deps), "Target dependencies must be specified.")

  output_file = "$target_gen_dir/$target_name.shaderblob"
  compiled_action(target_name) {
    tool = "//flutter/impeller/blobcat"
    inputs = invoker.shaders
    outputs = [ output_file ]
    output_path_rebased = rebase_path(output_file, root_build_dir)
    args = [ "--output=$output_path_rebased" ]
    foreach(shader, invoker.shaders) {
      shader_path = rebase_path(shader, root_build_dir)
      args += [ "--input=$shader_path" ]
    }
    deps = invoker.deps
  }
}

template("_impeller_shaders_gles") {
  assert(defined(invoker.shaders), "Impeller shaders must be specified.")
  assert(defined(invoker.name), "Name of the shader library must be specified.")
  assert(defined(invoker.analyze), "Whether to analyze must be specified.")

  shaders_base_name = string_join("",
                                  [
                                    invoker.name,
                                    "_shaders_gles",
                                  ])
  impellerc_gles = "impellerc_$target_name"
  impellerc(impellerc_gles) {
    shaders = invoker.shaders
    sl_file_extension = "gles"
    if (defined(invoker.gles_language_version)) {
      gles_language_version = invoker.gles_language_version
    }

    # Metal reflectors generate a superset of information.
    if (impeller_enable_metal || impeller_enable_vulkan) {
      intermediates_subdir = "gles"
    }
    if (is_mac) {
      shader_target_flag = "--opengl-desktop"
    } else {
      shader_target_flag = "--opengl-es"
    }

    defines = [ "IMPELLER_TARGET_OPENGLES" ]
  }

  gles_shaders =
      filter_include(get_target_outputs(":$impellerc_gles"), [ "*.gles" ])

  if (invoker.analyze) {
    analyze_lib = "analyze_$target_name"
    malioc_analyze_shaders(analyze_lib) {
      shaders = gles_shaders
      if (defined(invoker.gles_language_version)) {
        gles_language_version = invoker.gles_language_version
      }
      deps = [ ":$impellerc_gles" ]
    }
  }

  gles_lib = "genlib_$target_name"
  blobcat_library(gles_lib) {
    shaders = gles_shaders
    deps = [ ":$impellerc_gles" ]
  }

  reflect_gles = "reflect_$target_name"
  impellerc_reflect(reflect_gles) {
    impellerc_invocation = ":$impellerc_gles"
  }

  embed_gles_lib = "embed_$target_name"
  embed_blob(embed_gles_lib) {
    gles_library_files = get_target_outputs(":$gles_lib")
    symbol_name = shaders_base_name
    blob = gles_library_files[0]
    hdr = "$target_gen_dir/gles/$shaders_base_name.h"
    cc = "$target_gen_dir/gles/$shaders_base_name.cc"
    deps = [ ":$gles_lib" ]
  }

  group(target_name) {
    public_deps = [ ":$embed_gles_lib" ]

    if (invoker.analyze) {
      public_deps += [ ":$analyze_lib" ]
    }

    if (!impeller_enable_metal && !impeller_enable_vulkan) {
      public_deps += [ ":$reflect_gles" ]
    }
  }
}

template("_impeller_shaders_vk") {
  assert(defined(invoker.shaders), "Impeller shaders must be specified.")
  assert(defined(invoker.name), "Name of the shader library must be specified.")
  assert(defined(invoker.analyze), "Whether to analyze must be specified.")

  shaders_base_name = string_join("",
                                  [
                                    invoker.name,
                                    "_shaders_vk",
                                  ])
  impellerc_vk = "impellerc_$target_name"
  impellerc(impellerc_vk) {
    shaders = invoker.shaders
    sl_file_extension = "vkspv"

    # Metal reflectors generate a superset of information.
    if (impeller_enable_metal) {
      if (defined(invoker.metal_version)) {
        metal_version = invoker.metal_version
      }

      intermediates_subdir = "vk"
    }
    shader_target_flag = "--vulkan"

    defines = [ "IMPELLER_TARGET_VULKAN" ]
  }

  vk_shaders =
      filter_include(get_target_outputs(":$impellerc_vk"), [ "*.vkspv" ])

  if (invoker.analyze) {
    analyze_lib = "analyze_$target_name"
    malioc_analyze_shaders(analyze_lib) {
      shaders = vk_shaders
      if (defined(invoker.vulkan_language_version)) {
        vulkan_language_version = invoker.vulkan_language_version
      }
      deps = [ ":$impellerc_vk" ]
    }
  }

  vk_lib = "genlib_$target_name"
  blobcat_library(vk_lib) {
    shaders = vk_shaders
    deps = [ ":$impellerc_vk" ]
  }

  reflect_vk = "reflect_$target_name"
  impellerc_reflect(reflect_vk) {
    impellerc_invocation = ":$impellerc_vk"
  }

  embed_vk_lib = "embed_$target_name"
  embed_blob(embed_vk_lib) {
    vk_library_files = get_target_outputs(":$vk_lib")
    symbol_name = shaders_base_name
    blob = vk_library_files[0]
    hdr = "$target_gen_dir/vk/$shaders_base_name.h"
    cc = "$target_gen_dir/vk/$shaders_base_name.cc"
    deps = [ ":$vk_lib" ]
  }

  group(target_name) {
    public_deps = [ ":$embed_vk_lib" ]

    if (invoker.analyze) {
      public_deps += [ ":$analyze_lib" ]
    }

    if (!impeller_enable_metal) {
      public_deps += [ ":$reflect_vk" ]
    }
  }
}

# ------------------------------------------------------------------------------
# @brief           A target for precompiled shaders and the associated
#                  generated reflection library.
#
# @param[required] name
#
#    The base name for the reflected C++ library.
#
# @param[required] shaders
#
#    A list of GLSL shader files.
#
# @param[optional] analyze
#
#    Whether to analyze shaders with malioc. Defaults to false. Shaders will
#    only be analyzed if the GN argument "impeller_malioc_path" is defined.
#
# @param[optional] enable_opengles
#
#    Whether to compile the shaders for the OpenGL ES backend. Defaults to the
#    value of the GN argument "impeller_enable_opengles".
#
# @param[optional] gles_language_version
#
#    The GLES version required by the shaders.
#
# @param[optional] metal_version
#
#    The version of the Metal API required by the shaders.
#
# @param[optional] vulkan_language_version
#
#    The SPIR-V version required by the shaders.
#
# @param[options] use_half_textures
#
#    Whether the metal shader is using half-precision textures and requires
#    openGL semantics when compilig SPIR-V.
#
template("impeller_shaders") {
  if (defined(invoker.metal_version)) {
    metal_version = invoker.metal_version
  }

  use_half_textures = false
  if (defined(invoker.use_half_textures) && invoker.use_half_textures) {
    use_half_textures = true
  }

  not_needed([
               "metal_version",
               "use_half_textures",
             ])

  enable_opengles = impeller_enable_opengles
  if (defined(invoker.enable_opengles)) {
    enable_opengles = invoker.enable_opengles
  }

  if (impeller_enable_metal) {
    if (!defined(metal_version)) {
      metal_version = "1.2"
    }
    not_needed(invoker, [ "analyze" ])
    mtl_shaders = "mtl_$target_name"
    _impeller_shaders_metal(mtl_shaders) {
      name = invoker.name
      shaders = invoker.shaders
      metal_version = metal_version
      use_half_textures = use_half_textures
    }
  }

  if (enable_opengles) {
    analyze = true
    if (defined(invoker.analyze) && !invoker.analyze) {
      analyze = false
    }
    gles_shaders = "gles_$target_name"
    _impeller_shaders_gles(gles_shaders) {
      name = invoker.name
      if (defined(invoker.gles_language_version)) {
        gles_language_version = invoker.gles_language_version
      }
      if (defined(invoker.gles_exclusions)) {
        shaders = invoker.shaders - invoker.gles_exclusions
      } else {
        shaders = invoker.shaders
      }
      analyze = analyze
    }
  }

  if (impeller_enable_vulkan) {
    analyze = true
    if (defined(invoker.analyze) && !invoker.analyze) {
      analyze = false
    }
    vk_shaders = "vk_$target_name"
    _impeller_shaders_vk(vk_shaders) {
      name = invoker.name
      if (defined(invoker.vulkan_language_version)) {
        vulkan_language_version = invoker.vulkan_language_version
      }
      shaders = invoker.shaders
      analyze = analyze
    }
  }

  if (!impeller_supports_rendering) {
    not_needed(invoker, "*")
  }

  group(target_name) {
    public_deps = []
    if (impeller_enable_metal) {
      public_deps += [ ":$mtl_shaders" ]
    }

    if (enable_opengles) {
      public_deps += [ ":$gles_shaders" ]
    }

    if (impeller_enable_vulkan) {
      public_deps += [ ":$vk_shaders" ]
    }
  }
}

# Dispatches to the build or prebuilt scenec depending on the value of
# the impeller_use_prebuilt_scenec argument. Forwards all variables to
# compiled_action_foreach or action_foreach as appropriate.
template("_scenec") {
  if (impeller_use_prebuilt_scenec == "") {
    compiled_action_foreach(target_name) {
      forward_variables_from(invoker, "*")
      tool = "//flutter/impeller/scene/importer:scenec"
    }
  } else {
    action_foreach(target_name) {
      forward_variables_from(invoker, "*", [ "args" ])
      script = "//build/gn_run_binary.py"
      scenec_path = rebase_path(impeller_use_prebuilt_scenec, root_build_dir)
      args = [ scenec_path ] + invoker.args
    }
  }
}

template("scenec") {
  assert(defined(invoker.geometry), "Geometry input files must be specified.")
  assert(defined(invoker.type),
         "The type of geometry to be parsed (gltf, etc..).")

  _scenec(target_name) {
    sources = invoker.geometry
    generated_dir = "$target_gen_dir"

    input_type = invoker.type
    args = [
      "--input={{source}}",
      "--input-type=$input_type",
    ]

    output = "$generated_dir/{{source_file_part}}.ipscene"
    output_path = rebase_path(output, root_build_dir)

    args += [ "--output=$output_path" ]

    outputs = [ output ]
  }
}
