///|
/// What the director gives to `create_context`: the original plugin context,
/// the model and the service after the integrations changed the model, the
/// symbol provider and the names of the integrations in run order.
pub struct CreateContext {
  model : @smithy.Model
  plugin : PluginContext
  service : @smithy.Shape
  provider : SymbolProvider
  integrations : Array[String]
}

///|
/// The steps of a generator. `C` is the context type that `create_context`
/// makes. The director calls one `generate_*` step for each kind of shape.
pub struct DirectedCodegen[C] {
  priv create_symbol_provider : (@smithy.Model, PluginContext) -> SymbolProvider
  priv create_context : (CreateContext) -> C
  priv delegator : (C) -> WriterDelegator
  priv generate_service : (C, @smithy.Shape) -> Unit raise CodegenError
  priv generate_structure : (C, @smithy.Shape) -> Unit raise CodegenError
  priv generate_error : (C, @smithy.Shape) -> Unit raise CodegenError
  priv generate_union : (C, @smithy.Shape) -> Unit raise CodegenError
  priv generate_enum : (C, @smithy.Shape) -> Unit raise CodegenError
  priv generate_int_enum : (C, @smithy.Shape) -> Unit raise CodegenError
  priv generate_operation : (C, @smithy.Shape) -> Unit raise CodegenError
  priv generate_resource : (C, @smithy.Shape) -> Unit raise CodegenError
  priv generate_list : (C, @smithy.Shape) -> Unit raise CodegenError
  priv generate_map : (C, @smithy.Shape) -> Unit raise CodegenError
  priv customize_before_integrations : (C) -> Unit raise CodegenError
  priv customize_after_integrations : (C) -> Unit raise CodegenError
}

///|
/// Make a generator from its steps. The steps for operations, resources, lists
/// and maps, and the two customize steps, do nothing by default.
pub fn[C] DirectedCodegen::new(
  create_symbol_provider~ : (@smithy.Model, PluginContext) -> SymbolProvider,
  create_context~ : (CreateContext) -> C,
  delegator~ : (C) -> WriterDelegator,
  generate_service~ : (C, @smithy.Shape) -> Unit raise CodegenError,
  generate_structure~ : (C, @smithy.Shape) -> Unit raise CodegenError,
  generate_error~ : (C, @smithy.Shape) -> Unit raise CodegenError,
  generate_union~ : (C, @smithy.Shape) -> Unit raise CodegenError,
  generate_enum~ : (C, @smithy.Shape) -> Unit raise CodegenError,
  generate_int_enum~ : (C, @smithy.Shape) -> Unit raise CodegenError,
  generate_operation~ : (C, @smithy.Shape) -> Unit raise CodegenError = (_, _) => {
    ()
  },
  generate_resource~ : (C, @smithy.Shape) -> Unit raise CodegenError = (_, _) => {
    ()
  },
  generate_list~ : (C, @smithy.Shape) -> Unit raise CodegenError = (_, _) => (),
  generate_map~ : (C, @smithy.Shape) -> Unit raise CodegenError = (_, _) => (),
  customize_before_integrations~ : (C) -> Unit raise CodegenError = _ => (),
  customize_after_integrations~ : (C) -> Unit raise CodegenError = _ => (),
) -> DirectedCodegen[C] {
  {
    create_symbol_provider,
    create_context,
    delegator,
    generate_service,
    generate_structure,
    generate_error,
    generate_union,
    generate_enum,
    generate_int_enum,
    generate_operation,
    generate_resource,
    generate_list,
    generate_map,
    customize_before_integrations,
    customize_after_integrations,
  }
}

///|
/// Run a generator on the service of the plugin context.
///
/// 1. Sort the integrations.
/// 2. Let each integration change the model, in order. Find the service in the
///    new model. It raises `MissingService` when the service is gone, and
///    `NotAService` when it is not a service any more.
/// 3. Make the symbol provider, let each integration decorate it, and cache it.
/// 4. Make the context.
/// 5. Add the interceptors of the integrations to the delegator.
/// 6. Generate each shape of the service closure in topological order. I skip
///    prelude shapes, member shapes and simple shapes.
/// 7. Run `customize_before_integrations`, the `customize` step of each
///    integration, and `customize_after_integrations`.
/// 8. Return the files of the delegator.
pub fn[C] run_codegen(
  plugin : PluginContext,
  director : DirectedCodegen[C],
  integrations~ : Array[Integration[C]] = [],
) -> Map[String, String] raise CodegenError {
  let sorted = sort_integrations(integrations)
  let mut model = plugin.model
  for integration in sorted {
    model = (integration.preprocess_model)(model, plugin)
  }
  let service = match model.shapes.get(plugin.service.id) {
    None => raise MissingService(plugin.service.id)
    Some({ body: Service(..), .. } as shape) => shape
    Some(_) => raise NotAService(plugin.service.id)
  }
  let mut provider = (director.create_symbol_provider)(model, plugin)
  for integration in sorted {
    provider = (integration.decorate_symbol_provider)(provider, plugin)
  }
  provider = provider.cached()
  let ctx = (director.create_context)({
    model,
    plugin,
    service,
    provider,
    integrations: sorted.map(integration => integration.name),
  })
  let delegator = (director.delegator)(ctx)
  let interceptors : Array[Interceptor] = []
  for integration in sorted {
    interceptors.append((integration.interceptors)(ctx))
  }
  delegator.add_interceptors(interceptors)
  let error_trait = @smithy.ShapeId::parse("smithy.api#error").unwrap()
  for id in topological_order(model, service_closure(model, service.id)) {
    if id.namespace() == "smithy.api" {
      continue
    }
    guard model.shapes.get(id) is Some(shape) else { continue }
    match shape.body {
      Member(..) | Simple(_) => ()
      Structure(_) =>
        if shape.traits.contains(error_trait) {
          (director.generate_error)(ctx, shape)
        } else {
          (director.generate_structure)(ctx, shape)
        }
      Union(_) => (director.generate_union)(ctx, shape)
      Enum(_) => (director.generate_enum)(ctx, shape)
      IntEnum(_) => (director.generate_int_enum)(ctx, shape)
      Operation(..) => (director.generate_operation)(ctx, shape)
      Resource(..) => (director.generate_resource)(ctx, shape)
      List(_) => (director.generate_list)(ctx, shape)
      Map(..) => (director.generate_map)(ctx, shape)
      Service(..) => (director.generate_service)(ctx, shape)
    }
  }
  (director.customize_before_integrations)(ctx)
  for integration in sorted {
    (integration.customize)(ctx)
  }
  (director.customize_after_integrations)(ctx)
  delegator.flush()
}