///|
fn width_of_range(range : @syntax.Range?) -> Width {
  match range {
    Some(value) =>
      match value.width() {
        Some(width) => Known(width)
        None => Unknown
      }
    None => Known(1)
  }
}

///|
fn expression_name(expression : @syntax.Expression) -> String? {
  match expression {
    @syntax.Name(name, _) => Some(name)
    @syntax.Index(base, _, _) => expression_name(base)
    _ => None
  }
}

///|
/// Infer widths for the small expression subset understood by the analyzer.
fn expression_width(
  modules : Array[ModuleInfo],
  module_name : String,
  expression : @syntax.Expression,
) -> Width {
  match expression {
    @syntax.Name(name, _) =>
      match modules.search_by(fn(mod) { mod.name == module_name }) {
        Some(index) =>
          match modules[index].signal(name) {
            Some(signal) => signal.width
            None => Unknown
          }
        None => Unknown
      }
    @syntax.Number(value, _) =>
      if value == "0" || value == "1" {
        Known(1)
      } else {
        Unknown
      }
    @syntax.Index(base, _, _) => expression_width(modules, module_name, base)
    @syntax.Unary(_, operand, _) =>
      expression_width(modules, module_name, operand)
    @syntax.Binary(left, _, right, _) => {
      let left_width = expression_width(modules, module_name, left)
      let right_width = expression_width(modules, module_name, right)
      match (left_width, right_width) {
        (Known(left), Known(right)) =>
          Known(if left >= right { left } else { right })
        _ => Unknown
      }
    }
    @syntax.StringLiteral(_, _) | @syntax.Unknown(_) => Unknown
  }
}

///|
fn width_mismatch(
  modules : Array[ModuleInfo],
  module_name : String,
  target : @syntax.Expression,
  value : @syntax.Expression,
) -> Bool {
  match
    (
      expression_width(modules, module_name, target),
      expression_width(modules, module_name, value),
    ) {
    (Known(target_width), Known(value_width)) => target_width != value_width
    _ => false
  }
}

///|
fn signal_index(signals : Array[SignalInfo], name : String) -> Int? {
  for i, signal in signals {
    if signal.name == name {
      return Some(i)
    }
  }
  None
}

///|
fn add_driver(
  modules : Array[ModuleInfo],
  module_name : String,
  expression : @syntax.Expression,
  kind : DriveKind,
) -> Unit {
  match expression_name(expression) {
    Some(name) =>
      for mod in modules {
        if mod.name == module_name {
          match signal_index(mod.signals, name) {
            Some(index) => {
              let span = expression.span()
              mod.signals[index].drivers.push({ kind, span })
            }
            None => ()
          }
        }
      }
    None => ()
  }
}

///|
/// Add one procedural driver per signal for a single always block.
fn add_procedural_driver_once(
  modules : Array[ModuleInfo],
  module_name : String,
  expression : @syntax.Expression,
  seen : Array[String],
) -> Unit {
  match expression_name(expression) {
    Some(name) =>
      if !seen.contains(name) {
        seen.push(name)
        add_driver(modules, module_name, expression, Procedural)
      }
    None => ()
  }
}

///|
fn collect_statement_drivers(
  modules : Array[ModuleInfo],
  module_name : String,
  statement : @syntax.Statement,
  seen : Array[String],
) -> Unit {
  match statement {
    @syntax.Assignment(target, _, _, _) =>
      add_procedural_driver_once(modules, module_name, target, seen)
    @syntax.Block(statements, _) =>
      for statement in statements {
        collect_statement_drivers(modules, module_name, statement, seen)
      }
    @syntax.If(_, then_branch, else_branch, _) => {
      collect_statement_drivers(modules, module_name, then_branch, seen)
      match else_branch {
        Some(branch) =>
          collect_statement_drivers(modules, module_name, branch, seen)
        None => ()
      }
    }
    @syntax.Case(_, arms, _) =>
      for arm in arms {
        collect_statement_drivers(modules, module_name, arm.body, seen)
      }
    @syntax.Empty(_) | @syntax.Unknown(_) => ()
  }
}

///|
fn make_module_info(mod : @syntax.Module) -> ModuleInfo {
  let signals : Array[SignalInfo] = []
  for port in mod.ports {
    signals.push({
      name: port.name,
      net_type: @syntax.Unspecified,
      width: width_of_range(port.width),
      declaration_span: port.span,
      from_port: true,
      drivers: [],
    })
  }
  for signal in mod.signals {
    signals.push({
      name: signal.name,
      net_type: signal.net_type,
      width: width_of_range(signal.width),
      declaration_span: signal.span,
      from_port: false,
      drivers: [],
    })
  }
  let instances : Array[InstanceInfo] = []
  for item in mod.items {
    match item {
      @syntax.Instance(instance) =>
        instances.push({
          module_name: instance.module_name,
          instance_name: instance.instance_name,
          span: instance.span,
        })
      _ => ()
    }
  }
  { name: mod.name, ports: mod.ports, signals, instances, span: mod.span }
}

///|
pub fn index(design : @syntax.Design) -> SemanticIndex {
  let modules : Array[ModuleInfo] = []
  let issues : Array[SemanticIssue] = []
  for mod in design.modules {
    if modules.any(fn(existing) { existing.name == mod.name }) {
      issues.push({
        code: "RTL101",
        message: "duplicate module declaration",
        span: mod.span,
      })
    }
    modules.push(make_module_info(mod))
  }
  let edges : Array[(String, String)] = []
  for mod in design.modules {
    for item in mod.items {
      match item {
        @syntax.Assign(assign) => {
          add_driver(modules, mod.name, assign.target, Continuous)
          if width_mismatch(modules, mod.name, assign.target, assign.value) {
            issues.push({
              code: "RTL204",
              message: "assignment widths do not match",
              span: assign.span,
            })
          }
        }
        @syntax.Always(always) =>
          collect_statement_drivers(modules, mod.name, always.body, [])
        @syntax.Instance(instance) => {
          edges.push((mod.name, instance.module_name))
          if !modules.any(fn(candidate) {
              candidate.name == instance.module_name
            }) {
            issues.push({
              code: "RTL102",
              message: "unresolved module instance",
              span: instance.span,
            })
          }
          for connection in instance.connections {
            add_driver(modules, mod.name, connection.expression, InstanceOutput)
          }
        }
        @syntax.Statement(statement) =>
          collect_statement_drivers(modules, mod.name, statement, [])
        @syntax.Unknown(_) => ()
      }
    }
  }
  {
    modules,
    graph: { edge_list: edges },
    issues,
    syntax_modules: design.modules,
  }
}