///|
fn try_get_preprocessor_metadata(
  source : String,
  allow_defines : Bool,
  collect_import_uses : Bool,
) -> PreprocessorMetaData raise MetadataError {
  let (cleaned_for_directives, wgsl_directives) = extract_wgsl_directives(
    source,
  )

  let import_state = WgslImportSubstitutionState::default()
  let used_imports : Array[ImportDefWithOffset] = []
  let defines : Map[String, ShaderDefValue] = Map([])
  let effective_defs : @set.Set[String] = Set([])
  let mut name : String? = None

  for
    item in parse_wgsl_directive_scan_items(
      cleaned_for_directives,
      include_glsl_version=false,
    ) {
    match item.kind() {
      Directive(directive) => {
        let (is_scope, def) = metadata_scope_def_from_directive(directive)
        if is_scope {
          if def is Some(def_name) {
            effective_defs.add(def_name)
          }
        } else if directive.has_required_tail_separator() &&
          directive.kind() == Import {
          import_state.declare_imports(item.source().cleaned_text()) catch {
            Parse(message, item_offset) =>
              raise ImportParseError(message, item.start() + item_offset)
          }
        } else if directive.has_required_tail_separator() &&
          directive.kind() == DefineImportPath {
          let import_path = wgsl_metadata_first_space_separated_token(
            directive.tail(),
          )
          if import_path != "" {
            name = Some(import_path)
          }
        } else if directive.has_required_tail_separator() &&
          directive.kind() == Define {
          if allow_defines {
            let (define_name, define_value) = split_define_name_and_value(
              directive.tail(),
            )
            if define_name != "" {
              defines.set(define_name, parse_define_value(define_value))
            }
          } else {
            raise DefineInModule(item.start())
          }
        } else {
          metadata_collect_source_item(item, effective_defs)
        }
      }
      Source => metadata_collect_source_item(item, effective_defs)
      GlslVersion(_) => ()
    }
  }
  if collect_import_uses {
    metadata_collect_import_uses(
      cleaned_for_directives, import_state, used_imports,
    )
  }

  PreprocessorMetaData(
    name, used_imports, defines, effective_defs, wgsl_directives, cleaned_for_directives,
  )
}

///|
pub fn get_preprocessor_metadata(
  source : String,
  allow_defines? : Bool = true,
  collect_import_uses? : Bool = true,
) -> PreprocessorMetaData raise MetadataError {
  try_get_preprocessor_metadata(source, allow_defines, collect_import_uses)
}

///|
fn wgsl_metadata_skip_whitespace(line : String, start : Int) -> Int {
  let mut index = start
  while index < line.length() {
    let width = wgsl_regex_whitespace_width(line, index)
    guard width > 0 else { break }
    index = index + width
  }
  index
}

///|
fn wgsl_metadata_has_word_at(line : String, start : Int, word : String) -> Bool {
  guard start + word.length() <= line.length() else { return false }
  for i in 0..= line.length() || wgsl_regex_word_width(line, end_) == 0
}

///|
fn wgsl_metadata_line_may_define_shader_def(line : String) -> Bool {
  let hash_start = wgsl_metadata_skip_whitespace(line, 0)
  guard hash_start < line.length() &&
    line.code_unit_at(hash_start).to_int() == 35 else {
    return false
  }
  let name_start = wgsl_metadata_skip_whitespace(line, hash_start + 1)
  wgsl_metadata_has_word_at(line, name_start, "define")
}

///|
fn wgsl_metadata_source_may_define_shader_def(source : String) -> Bool {
  for line in source.split("\n") {
    if wgsl_metadata_line_may_define_shader_def(line.to_owned()) {
      return true
    }
  }
  false
}

///|
pub fn collect_wgsl_source_local_shader_defines(
  source : String,
) -> Map[String, ShaderDefValue] {
  let defines : Map[String, ShaderDefValue] = Map([])
  if !wgsl_metadata_source_may_define_shader_def(source) {
    return defines
  }
  for
    item in parse_wgsl_directive_scan_items(source, include_glsl_version=false) {
    match item.kind() {
      Directive(directive) =>
        if directive.has_required_tail_separator() && directive.kind() == Define {
          let (define_name, define_value) = split_define_name_and_value(
            directive.tail(),
          )
          if define_name != "" {
            defines.set(define_name, parse_define_value(define_value))
          }
        }
      _ => ()
    }
  }
  defines
}