///|
priv struct PropertyAnalysis {
  program : Program
  allowed : @hashset.HashSet[String]
  pinned : @hashset.HashSet[String]
  keys : Map[Int, String]
  members : Map[Int, String]
  literals : @hashset.HashSet[Int]
  class_methods : @hashset.HashSet[Int]
  quoted_names : Map[StringView, String?]
}

///|
fn PropertyAnalysis::new(
  program : Program,
  requested : ArrayView[String],
) -> PropertyAnalysis {
  let allowed = @hashset.HashSet([])
  for name in requested {
    guard name =~ ASCII_PROPERTY_NAME else { continue }
    allowed.add(name)
  }
  let pinned = keywords.copy()
  pinned.add("__proto__")
  pinned.add("constructor")
  pinned.add("prototype")
  {
    program,
    allowed,
    pinned,
    keys: {},
    members: {},
    literals: @hashset.HashSet([]),
    class_methods: program.class_method_ids(),
    quoted_names: {},
  }
}

///|
fn Program::class_method_ids(self : Program) -> @hashset.HashSet[Int] {
  let methods = @hashset.HashSet([])
  for class in self.classes {
    for index in 0.. String? {
  let spelling = self.program.source[start:end]
  guard self.quoted_names.get(spelling) is Some(name) else {
    let decoded = quoted_property_name(self.program.source, start, end)
    self.quoted_names[spelling] = decoded
    return decoded
  }
  name
}

///|
fn PropertyAnalysis::key_name(
  self : PropertyAnalysis,
  id : Int,
  computed : Bool,
) -> String? raise ParseError {
  let program = self.program
  let node = program.nodes[program.property_leaf(id)]
  guard node.kind == StringLiteral || node.kind == TemplateNoSubstitution else {
    return None
  }
  let is_quoted = program.source.view(start_offset=node.a) =~ (
    PROPERTY_QUOTE_START,
    after=_,
  )
  guard !is_quoted else { return self.quoted_name(node.a, node.b) }
  guard !computed else { return None }
  let name = program.names[program.identifier(node.a, node.b)]
  guard name =~ ASCII_PROPERTY_NAME else { return None }
  Some(name)
}

///|
fn PropertyAnalysis::collect_key(
  self : PropertyAnalysis,
  id : Int,
  computed : Bool,
) -> Unit raise ParseError {
  guard self.key_name(id, computed) is Some(name) else { return }
  self.keys[id] = name
  self.literals.add(self.program.property_leaf(id))
}

///|
fn PropertyAnalysis::collect(self : PropertyAnalysis) -> Unit raise ParseError {
  let program = self.program
  for id in 0..= 0 => {
        let name = program.names[program.identifier(node.b, node.c)]
        guard name =~ ASCII_PROPERTY_NAME else { continue }
        self.members[id] = name
      }
      Property | PatternProperty =>
        self.collect_key(node.a, node.d == PROPERTY_COMPUTED)
      Method => {
        let computed = if self.class_methods.contains(id) {
          (node.d & CLASS_MEMBER_COMPUTED) != 0
        } else {
          node.d == PROPERTY_COMPUTED
        }
        self.collect_key(node.a, computed)
      }
      ClassField =>
        self.collect_key(node.a, (node.d & CLASS_MEMBER_COMPUTED) != 0)
      Index => self.collect_key(node.b, true)
      Binary if program.operators[node.a] == Operator::In =>
        self.collect_key(node.b, true)
      _ => ()
    }
  }
}

///|
fn PropertyAnalysis::preserve_strings(self : PropertyAnalysis) -> Unit {
  let program = self.program
  for id in 0.. Unit {
  let program = self.program
  for statement in program.modules {
    let attributes = match statement {
      Import(_, _, attributes)
      | ExportNames(_, _, attributes)
      | ExportAll(_, _, attributes) => attributes
      _ => -1
    }
    guard attributes >= 0 else { continue }
    let object = program.nodes[attributes]
    for index in 0.. Unit {
  for id, name in uses {
    self.program.property_reserved.add(name)
    guard self.allowed.contains(name) && !self.pinned.contains(name) else {
      continue
    }
    selected[id] = name
  }
}

///|
fn Program::prepare_properties(
  program : Program,
  requested : ArrayView[String],
) -> Unit raise ParseError {
  guard requested.length() > 0 && !program.has_dynamic_scope else { return }
  let analysis = PropertyAnalysis::new(program, requested)
  analysis.collect()
  analysis.preserve_strings()
  analysis.preserve_attributes()
  for name in analysis.pinned {
    program.property_reserved.add(name)
  }
  analysis.select(analysis.keys, program.property_names)
  analysis.select(analysis.members, program.member_names)
}

///|
#valtype
priv struct PropertyFrequency {
  name : String
  count : Int
}

///|
fn Program::mangle_properties(program : Program) -> Unit {
  guard !program.property_names.is_empty() || !program.member_names.is_empty() else {
    return
  }
  let groups = [program.property_names, program.member_names]
  let frequencies = property_frequencies(groups)
  let generator = NameGenerator::new(
    program.alphabet,
    program.property_reserved,
  )
  let renamed : Map[String, String] = Map([])
  for property in frequencies {
    let candidate = generator.next_name()
    renamed[property.name] = candidate
    program.property_reserved.add(candidate)
  }
  for uses in groups {
    for id, name in uses {
      uses[id] = renamed.get(name).unwrap()
    }
  }
}

///|
fn property_frequencies(
  groups : Array[Map[Int, String]],
) -> Array[PropertyFrequency] {
  let counts : Map[String, Int] = Map([])
  for uses in groups {
    for _, name in uses {
      counts[name] = counts.get(name).unwrap_or(0) + 1
    }
  }
  let frequencies : Array[PropertyFrequency] = []
  for name, count in counts {
    frequencies.push({ name, count, })
  }
  frequencies.sort_by((left, right) => {
    let difference = right.count - left.count
    guard difference == 0 else { return difference }
    left.name.compare(right.name)
  })
  frequencies
}

///|
fn Printer::property_expression(self : Printer, id : Int) -> Unit {
  guard self.program.property_names.get(id) is Some(name) else {
    self.expression(id)
    return
  }
  self.text("\"", false)
  self.text(name, false)
  self.text("\"", false)
}