#include "compiler.h"

#include <errno.h>
#include <math.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

typedef struct {
  bool known;
  double seconds;
} Duration;

typedef enum {
  NODE_SOURCE,
  NODE_SLICE,
  NODE_TAKE,
  NODE_CONCAT,
  NODE_DELAY,
  NODE_VOLUME,
  NODE_MIX,
  NODE_SILENCE,
  NODE_SPEED,
  NODE_FADE_IN,
  NODE_FADE_OUT,
} NodeKind;

typedef struct Node {
  NodeKind kind;
  ValueType type;
  struct Node* input[2];
  size_t id;
  size_t source_index;
  bool reachable;
  unsigned consumers;
  unsigned next_consumer;
  double first;
  double second;
  Duration duration;
} Node;

typedef struct {
  const char* path;
  SourceKind kind;
  size_t input_index;
} Input;

typedef struct {
  ValueType type;
  double number;
  Node* video;
  Node* audio;
  ssize_t subtitle_input;
} Value;

typedef struct {
  char* data;
  size_t length;
  size_t capacity;
} StringBuilder;

typedef struct {
  Arena* arena;
  Node** nodes;
  size_t node_count;
  size_t node_capacity;
  Input* inputs;
  size_t input_count;
  size_t input_capacity;
  Value stack[1024];
  size_t stack_count;
  ProbeCache* probes;
  Error* error;
} Compiler;

static bool compile_error(Compiler* compiler, int line, const char* format, ...) {
  compiler->error->line = line;
  va_list args;
  va_start(args, format);
  vsnprintf(compiler->error->message, sizeof(compiler->error->message), format, args);
  va_end(args);
  return false;
}

static bool builder_reserve(StringBuilder* builder, size_t extra) {
  if (extra > SIZE_MAX - builder->length - 1) {
    return false;
  }
  size_t needed = builder->length + extra + 1;
  if (needed <= builder->capacity) {
    return true;
  }
  size_t capacity = builder->capacity == 0 ? 1024 : builder->capacity;
  while (capacity < needed) {
    if (capacity > SIZE_MAX / 2) {
      capacity = needed;
      break;
    }
    capacity *= 2;
  }
  char* data = realloc(builder->data, capacity);
  if (data == NULL) {
    return false;
  }
  builder->data = data;
  builder->capacity = capacity;
  return true;
}

static bool builder_append(StringBuilder* builder, const char* text) {
  size_t length = strlen(text);
  if (!builder_reserve(builder, length)) {
    return false;
  }
  memcpy(builder->data + builder->length, text, length + 1);
  builder->length += length;
  return true;
}

static bool builder_printf(StringBuilder* builder, const char* format, ...) {
  va_list args;
  va_start(args, format);
  va_list copy;
  va_copy(copy, args);
  int count = vsnprintf(NULL, 0, format, copy);
  va_end(copy);
  if (count < 0 || !builder_reserve(builder, (size_t)count)) {
    va_end(args);
    return false;
  }
  vsnprintf(builder->data + builder->length, (size_t)count + 1, format, args);
  va_end(args);
  builder->length += (size_t)count;
  return true;
}

static char* arena_string(Arena* arena, const char* text) {
  size_t length = strlen(text) + 1;
  char* copy = arena_alloc(arena, length);
  if (copy != NULL) {
    memcpy(copy, text, length);
  }
  return copy;
}

static bool grow_array(Arena* arena, void** array, size_t count, size_t* capacity, size_t item_size,
                       size_t alignment) {
  if (count < *capacity) {
    return true;
  }
  size_t new_capacity = *capacity == 0 ? 16 : *capacity * 2;
  if (new_capacity > SIZE_MAX / item_size) {
    return false;
  }
  void* grown = arena_alloc_aligned(arena, new_capacity * item_size, alignment);
  if (grown == NULL) {
    return false;
  }
  if (*array != NULL) {
    memcpy(grown, *array, count * item_size);
  }
  *array = grown;
  *capacity = new_capacity;
  return true;
}

static Node* new_node(Compiler* compiler, NodeKind kind, ValueType type, Node* left, Node* right) {
  if (!grow_array(compiler->arena, (void** )&compiler->nodes, compiler->node_count,
                  &compiler->node_capacity, sizeof(*compiler->nodes), _Alignof(Node* ))) {
    return NULL;
  }
  Node* node = arena_alloc(compiler->arena, sizeof(*node));
  if (node == NULL) {
    return NULL;
  }
  *node = (Node){
    .kind = kind,
    .type = type,
    .input = {left, right},
    .id = compiler->node_count,
  };
  compiler->nodes[compiler->node_count++] = node;
  return node;
}

static bool add_input(Compiler* compiler, const char* path, SourceKind kind, size_t* index) {
  if (!grow_array(compiler->arena, (void** )&compiler->inputs, compiler->input_count,
                  &compiler->input_capacity, sizeof(*compiler->inputs), _Alignof(Input))) {
    return false;
  }
  *index = compiler->input_count;
  compiler->inputs[compiler->input_count++] = (Input){
    .path = path,
    .kind = kind,
    .input_index = *index,
  };
  return true;
}

static Duration duration_sum(Duration left, Duration right) {
  if (!left.known || !right.known) {
    return (Duration){0};
  }
  return (Duration){.known = true, .seconds = left.seconds + right.seconds};
}

static Duration duration_max(Duration left, Duration right) {
  if (!left.known || !right.known) {
    return (Duration){0};
  }
  return (Duration){.known = true, .seconds = fmax(left.seconds, right.seconds)};
}

static Node* source_node(Compiler* compiler, ValueType type, size_t source_index, Duration duration) {
  Node* node = new_node(compiler, NODE_SOURCE, type, NULL, NULL);
  if (node != NULL) {
    node->source_index = source_index;
    node->duration = duration;
  }
  return node;
}

static bool push_value(Compiler* compiler, Value value, int line) {
  if (compiler->stack_count == sizeof(compiler->stack) / sizeof(compiler->stack[0])) {
    return compile_error(compiler, line, "compiler stack limit exceeded");
  }
  compiler->stack[compiler->stack_count++] = value;
  return true;
}

static Value pop_value(Compiler* compiler) {
  return compiler->stack[--compiler->stack_count];
}

static Duration value_duration(Value value) {
  if (value.type == TYPE_VIDEO) {
    return value.video->duration;
  }
  if (value.type == TYPE_AUDIO) {
    return value.audio->duration;
  }
  return duration_max(value.video->duration, value.audio->duration);
}

static bool compile_source(Compiler* compiler, Instruction instruction) {
  size_t input_index = 0;
  if (!add_input(compiler, instruction.as.source.path, instruction.as.source.kind, &input_index)) {
    return compile_error(compiler, instruction.line, "out of memory");
  }

  Value value = {.type = (ValueType)instruction.as.source.kind, .subtitle_input = -1};
  if (instruction.as.source.kind == SRC_SUBTITLES) {
    value.subtitle_input = (ssize_t)input_index;
    return push_value(compiler, value, instruction.line);
  }

  ProbeResult probe;
  if (!probe_cache_get(compiler->probes, instruction.as.source.path, &probe, compiler->error)) {
    compiler->error->line = instruction.line;
    return false;
  }
  if ((instruction.as.source.kind == SRC_VIDEO || instruction.as.source.kind == SRC_COMBINED) &&
      !probe.has_video) {
    return compile_error(compiler, instruction.line, "source '%s' has no video stream",
                         instruction.as.source.path);
  }
  if ((instruction.as.source.kind == SRC_AUDIO || instruction.as.source.kind == SRC_COMBINED) &&
      !probe.has_audio) {
    return compile_error(compiler, instruction.line, "source '%s' has no audio stream",
                         instruction.as.source.path);
  }

  if (instruction.as.source.kind == SRC_VIDEO || instruction.as.source.kind == SRC_COMBINED) {
    Duration duration = {probe.video_duration_known, probe.video_duration};
    value.video = source_node(compiler, TYPE_VIDEO, input_index, duration);
  }
  if (instruction.as.source.kind == SRC_AUDIO || instruction.as.source.kind == SRC_COMBINED) {
    Duration duration = {probe.audio_duration_known, probe.audio_duration};
    value.audio = source_node(compiler, TYPE_AUDIO, input_index, duration);
  }
  if ((value.video == NULL && instruction.as.source.kind != SRC_AUDIO) ||
      (value.audio == NULL && instruction.as.source.kind != SRC_VIDEO)) {
    return compile_error(compiler, instruction.line, "out of memory");
  }
  return push_value(compiler, value, instruction.line);
}

static Node* stream_node(Value value) {
  return value.type == TYPE_VIDEO ? value.video : value.audio;
}

static Value stream_value(ValueType type, Node* node) {
  Value value = {.type = type, .subtitle_input = -1};
  if (type == TYPE_VIDEO) {
    value.video = node;
  } else {
    value.audio = node;
  }
  return value;
}

static bool compile_stack_operation(Compiler* compiler, Operation operation, int line) {
  Value* stack = compiler->stack;
  size_t count = compiler->stack_count;
  (void)line;
  switch (operation) {
    case OP_DUP:
      return push_value(compiler, stack[count - 1], line);
    case OP_DROP:
      compiler->stack_count--;
      return true;
    case OP_SWAP: {
      Value temporary = stack[count - 1];
      stack[count - 1] = stack[count - 2];
      stack[count - 2] = temporary;
      return true;
    }
    case OP_OVER:
      return push_value(compiler, stack[count - 2], line);
    case OP_ROT: {
      Value first = stack[count - 3];
      stack[count - 3] = stack[count - 2];
      stack[count - 2] = stack[count - 1];
      stack[count - 1] = first;
      return true;
    }
    default:
      return false;
  }
}

static bool compile_operation(Compiler* compiler, Instruction instruction) {
  Operation operation = instruction.as.op;
  int line = instruction.line;
  if (operation <= OP_ROT) {
    return compile_stack_operation(compiler, operation, line);
  }
  if (operation == OP_GET_VIDEO || operation == OP_GET_AUDIO || operation == OP_SPLIT) {
    Value combined = pop_value(compiler);
    if (operation != OP_GET_AUDIO && !push_value(compiler, stream_value(TYPE_VIDEO, combined.video), line)) {
      return false;
    }
    if (operation != OP_GET_VIDEO && !push_value(compiler, stream_value(TYPE_AUDIO, combined.audio), line)) {
      return false;
    }
    return true;
  }
  if (operation == OP_LENGTH) {
    Duration duration = value_duration(pop_value(compiler));
    if (!duration.known) {
      return compile_error(compiler, line, "stream duration is unknown");
    }
    return push_value(compiler, (Value){.type = TYPE_DURATION, .number = duration.seconds}, line);
  }
  if (operation == OP_SILENCE) {
    Value duration = pop_value(compiler);
    Node* node = new_node(compiler, NODE_SILENCE, TYPE_AUDIO, NULL, NULL);
    if (node == NULL) {
      return compile_error(compiler, line, "out of memory");
    }
    node->first = duration.number;
    node->duration = (Duration){true, duration.number};
    return push_value(compiler, stream_value(TYPE_AUDIO, node), line);
  }
  if (operation == OP_MUX) {
    Value audio = pop_value(compiler);
    Value video = pop_value(compiler);
    Value combined = {.type = TYPE_COMBINED, .video = video.video, .audio = audio.audio,
                      .subtitle_input = -1};
    return push_value(compiler, combined, line);
  }
  if (operation == OP_ATTACH) {
    Value subtitles = pop_value(compiler);
    Value combined = pop_value(compiler);
    combined.subtitle_input = subtitles.subtitle_input;
    return push_value(compiler, combined, line);
  }
  if (operation >= OP_ADD && operation <= OP_GE) {
    Value right = pop_value(compiler);
    Value left = pop_value(compiler);
    Value result = {0};
    if (operation == OP_ADD) result.number = left.number + right.number;
    if (operation == OP_SUB) result.number = left.number - right.number;
    if (operation == OP_MUL) result.number = left.number * right.number;
    if (operation == OP_DIV) {
      if (right.number == 0) return compile_error(compiler, line, "division by zero");
      result.number = left.number / right.number;
    }
    if (operation >= OP_EQ) {
      result.type = TYPE_BOOL;
      if (operation == OP_EQ) result.number = left.number == right.number;
      if (operation == OP_NE) result.number = left.number != right.number;
      if (operation == OP_LT) result.number = left.number < right.number;
      if (operation == OP_LE) result.number = left.number <= right.number;
      if (operation == OP_GT) result.number = left.number > right.number;
      if (operation == OP_GE) result.number = left.number >= right.number;
    } else {
      result.type = left.type;
      if (!isfinite(result.number)) return compile_error(compiler, line, "non-finite arithmetic result");
      if (result.type == TYPE_DURATION && result.number < 0) {
        return compile_error(compiler, line, "duration arithmetic produced a negative value");
      }
    }
    return push_value(compiler, result, line);
  }
  if (operation == OP_AND || operation == OP_OR) {
    Value right = pop_value(compiler);
    Value left = pop_value(compiler);
    double result = operation == OP_AND ? left.number && right.number : left.number || right.number;
    return push_value(compiler, (Value){.type = TYPE_BOOL, .number = result}, line);
  }
  if (operation == OP_NOT) {
    Value value = pop_value(compiler);
    value.number = !value.number;
    return push_value(compiler, value, line);
  }

  Value parameter = pop_value(compiler);
  if (operation == OP_SPEED && parameter.number <= 0) {
    return compile_error(compiler, line, "speed must be greater than zero");
  }
  Value right = {0};
  Value left = pop_value(compiler);
  NodeKind kind;
  Node* left_node = stream_node(left);
  Node* right_node = NULL;
  ValueType result_type = left.type;

  if (operation == OP_CONCAT || operation == OP_MIX) {
    right = parameter;
    right_node = stream_node(right);
    kind = operation == OP_CONCAT ? NODE_CONCAT : NODE_MIX;
  } else {
    switch (operation) {
      case OP_SLICE:
        right = left;
        left = pop_value(compiler);
        left_node = stream_node(left);
        result_type = left.type;
        kind = NODE_SLICE;
        break;
      case OP_TAKE: kind = NODE_TAKE; break;
      case OP_DELAY: kind = NODE_DELAY; break;
      case OP_VOLUME: kind = NODE_VOLUME; break;
      case OP_SPEED: kind = NODE_SPEED; break;
      case OP_FADE_IN: kind = NODE_FADE_IN; break;
      case OP_FADE_OUT: kind = NODE_FADE_OUT; break;
      default: return compile_error(compiler, line, "unsupported operation");
    }
  }

  Node* node = new_node(compiler, kind, result_type, left_node, right_node);
  if (node == NULL) {
    return compile_error(compiler, line, "out of memory");
  }
  node->first = parameter.number;
  node->duration = left_node->duration;
  if (operation == OP_SLICE) {
    node->first = right.number;
    node->second = parameter.number;
    if (left_node->duration.known) {
      double remaining = fmax(0, left_node->duration.seconds - node->first);
      node->duration = (Duration){true, fmin(node->second, remaining)};
    } else {
      node->duration = (Duration){true, node->second};
    }
  } else if (operation == OP_TAKE) {
    node->duration = left_node->duration.known
                       ? (Duration){true, fmin(left_node->duration.seconds, parameter.number)}
                       : (Duration){true, parameter.number};
  } else if (operation == OP_CONCAT) {
    node->duration = duration_sum(left_node->duration, right_node->duration);
  } else if (operation == OP_DELAY) {
    node->duration = left_node->duration.known
                       ? (Duration){true, left_node->duration.seconds + parameter.number}
                       : (Duration){0};
  } else if (operation == OP_MIX) {
    node->duration = duration_max(left_node->duration, right_node->duration);
  } else if (operation == OP_SPEED) {
    node->duration = left_node->duration.known
                       ? (Duration){true, left_node->duration.seconds / parameter.number}
                       : (Duration){0};
  } else if (operation == OP_FADE_OUT) {
    if (!left_node->duration.known) {
      return compile_error(compiler, line, "fade-out requires a known stream duration");
    }
    node->second = fmax(0, left_node->duration.seconds - parameter.number);
  }
  return push_value(compiler, stream_value(result_type, node), line);
}

static bool operation_at(const Program* program, size_t index, Operation operation) {
  return program->items[index].kind == INST_OPERATION && program->items[index].as.op == operation;
}

static bool find_if(const Program* program, size_t start, size_t limit,
                    size_t* else_index, size_t* end_index, Error* error) {
  unsigned depth = 1;
  *else_index = SIZE_MAX;
  for (size_t i = start + 1; i < limit; i++) {
    if (operation_at(program, i, OP_IF)) depth++;
    if (operation_at(program, i, OP_END) && --depth == 0) {
      *end_index = i;
      return true;
    }
    if (operation_at(program, i, OP_ELSE) && depth == 1) *else_index = i;
  }
  error->line = program->items[start].line;
  snprintf(error->message, sizeof(error->message), "if without matching end");
  return false;
}

static bool find_loop(const Program* program, size_t start, size_t limit,
                      size_t* while_index, size_t* repeat_index, Error* error) {
  unsigned depth = 1;
  *while_index = SIZE_MAX;
  for (size_t i = start + 1; i < limit; i++) {
    if (operation_at(program, i, OP_BEGIN)) depth++;
    if (operation_at(program, i, OP_REPEAT) && --depth == 0) {
      *repeat_index = i;
      return *while_index != SIZE_MAX;
    }
    if (operation_at(program, i, OP_WHILE) && depth == 1) *while_index = i;
  }
  error->line = program->items[start].line;
  snprintf(error->message, sizeof(error->message), "begin without matching repeat");
  return false;
}

static bool execute_range(Compiler* compiler, const Program* program, size_t start, size_t end) {
  for (size_t i = start; i < end; i++) {
    Instruction instruction = program->items[i];
    if (instruction.kind == INST_SOURCE) {
      if (!compile_source(compiler, instruction)) {
        return false;
      }
    } else if (instruction.kind == INST_DURATION || instruction.kind == INST_SCALAR ||
               instruction.kind == INST_BOOL) {
      Value value = {
        .type = instruction.kind == INST_DURATION ? TYPE_DURATION
                : instruction.kind == INST_SCALAR ? TYPE_SCALAR : TYPE_BOOL,
        .number = instruction.as.number,
        .subtitle_input = -1,
      };
      if (!push_value(compiler, value, instruction.line)) {
        return false;
      }
    } else if (instruction.as.op == OP_IF) {
      Value condition = pop_value(compiler);
      size_t else_index, end_index;
      if (!find_if(program, i, end, &else_index, &end_index, compiler->error)) return false;
      if (condition.number != 0) {
        size_t true_end = else_index == SIZE_MAX ? end_index : else_index;
        if (!execute_range(compiler, program, i + 1, true_end)) return false;
      } else if (else_index != SIZE_MAX &&
                 !execute_range(compiler, program, else_index + 1, end_index)) {
        return false;
      }
      i = end_index;
    } else if (instruction.as.op == OP_BEGIN) {
      size_t while_index, repeat_index;
      if (!find_loop(program, i, end, &while_index, &repeat_index, compiler->error)) return false;
      size_t iterations = 0;
      for (;;) {
        if (!execute_range(compiler, program, i + 1, while_index)) return false;
        Value condition = pop_value(compiler);
        if (condition.number == 0) break;
        if (iterations++ == 10000) {
          return compile_error(compiler, instruction.line, "loop exceeded 10000 iterations");
        }
        if (!execute_range(compiler, program, while_index + 1, repeat_index)) return false;
      }
      i = repeat_index;
    } else if (instruction.as.op == OP_ELSE || instruction.as.op == OP_END ||
               instruction.as.op == OP_WHILE || instruction.as.op == OP_REPEAT) {
      return compile_error(compiler, instruction.line, "unmatched control-flow marker");
    } else if (!compile_operation(compiler, instruction)) {
      return false;
    }
  }
  return true;
}

static bool append_separator(StringBuilder* graph) {
  return graph->length == 0 || builder_append(graph, ";");
}

static bool node_label(StringBuilder* label, Node* node) {
  if (node->kind == NODE_SOURCE && node->consumers == 1) {
    return builder_printf(label, "%zu:%c:0", node->source_index,
                          node->type == TYPE_VIDEO ? 'v' : 'a');
  }
  unsigned use = node->next_consumer++;
  if (node->consumers > 1) {
    return builder_printf(label, "n%zu_%u", node->id, use);
  }
  return builder_printf(label, "n%zu", node->id);
}

static bool append_atempo(StringBuilder* graph, double speed) {
  bool first = true;
  while (speed > 2.0 + 1e-12) {
    if (!first && !builder_append(graph, ",")) return false;
    if (!builder_append(graph, "atempo=2")) return false;
    speed /= 2.0;
    first = false;
  }
  while (speed < 0.5 - 1e-12) {
    if (!first && !builder_append(graph, ",")) return false;
    if (!builder_append(graph, "atempo=0.5")) return false;
    speed /= 0.5;
    first = false;
  }
  if (!first && !builder_append(graph, ",")) return false;
  return builder_printf(graph, "atempo=%.9g", speed);
}

static bool emit_filter(Compiler* compiler, StringBuilder* graph, Node* node) {
  StringBuilder left = {0};
  StringBuilder right = {0};
  bool ok = true;
  if (node->input[0] != NULL) ok = node_label(&left, node->input[0]);
  if (ok && node->input[1] != NULL) ok = node_label(&right, node->input[1]);
  if (!ok || !append_separator(graph)) goto done;

  if (node->input[0] != NULL && !builder_printf(graph, "[%s]", left.data)) goto done;
  if (node->input[1] != NULL && !builder_printf(graph, "[%s]", right.data)) goto done;
  switch (node->kind) {
    case NODE_SLICE:
      ok = builder_printf(graph, "%s=start=%.9g:duration=%.9g,%s=PTS-STARTPTS",
                          node->type == TYPE_VIDEO ? "trim" : "atrim", node->first, node->second,
                          node->type == TYPE_VIDEO ? "setpts" : "asetpts");
      break;
    case NODE_TAKE:
      ok = builder_printf(graph, "%s=duration=%.9g,%s=PTS-STARTPTS",
                          node->type == TYPE_VIDEO ? "trim" : "atrim", node->first,
                          node->type == TYPE_VIDEO ? "setpts" : "asetpts");
      break;
    case NODE_CONCAT:
      ok = builder_printf(graph, "concat=n=2:v=%d:a=%d", node->type == TYPE_VIDEO,
                          node->type == TYPE_AUDIO);
      break;
    case NODE_DELAY:
      ok = node->type == TYPE_VIDEO
             ? builder_printf(graph, "setpts=PTS+%.9g/TB", node->first)
             : builder_printf(graph, "adelay=delays=%.0f:all=1", node->first * 1000.0);
      break;
    case NODE_VOLUME: ok = builder_printf(graph, "volume=%.9g", node->first); break;
    case NODE_MIX: ok = builder_append(graph, "amix=inputs=2:duration=longest"); break;
    case NODE_SILENCE:
      ok = builder_printf(graph, "anullsrc=r=48000:cl=stereo,atrim=duration=%.9g", node->first);
      break;
    case NODE_SPEED:
      ok = node->type == TYPE_VIDEO ? builder_printf(graph, "setpts=PTS/%.9g", node->first)
                                    : append_atempo(graph, node->first);
      break;
    case NODE_FADE_IN:
      ok = builder_printf(graph, "%s=t=in:st=0:d=%.9g",
                          node->type == TYPE_VIDEO ? "fade" : "afade", node->first);
      break;
    case NODE_FADE_OUT:
      ok = builder_printf(graph, "%s=t=out:st=%.9g:d=%.9g",
                          node->type == TYPE_VIDEO ? "fade" : "afade", node->second, node->first);
      break;
    default: ok = false; break;
  }
  if (!ok) goto done;

  if (node->consumers > 1) {
    if (!builder_printf(graph, "[b%zu];[b%zu]%s=%u", node->id, node->id,
                        node->type == TYPE_VIDEO ? "split" : "asplit", node->consumers)) goto done;
    for (unsigned i = 0; i < node->consumers; i++) {
      if (!builder_printf(graph, "[n%zu_%u]", node->id, i)) goto done;
    }
  } else if (!builder_printf(graph, "[n%zu]", node->id)) {
    goto done;
  }
  ok = true;

done:
  free(left.data);
  free(right.data);
  if (!ok) return compile_error(compiler, 0, "out of memory while building filter graph");
  return true;
}

static bool emit_source_split(Compiler* compiler, StringBuilder* graph, Node* node) {
  if (node->consumers <= 1) return true;
  if (!append_separator(graph) ||
      !builder_printf(graph, "[%zu:%c:0]%s=%u", node->source_index,
                      node->type == TYPE_VIDEO ? 'v' : 'a',
                      node->type == TYPE_VIDEO ? "split" : "asplit", node->consumers)) {
    return compile_error(compiler, 0, "out of memory while building filter graph");
  }
  for (unsigned i = 0; i < node->consumers; i++) {
    if (!builder_printf(graph, "[n%zu_%u]", node->id, i)) {
      return compile_error(compiler, 0, "out of memory while building filter graph");
    }
  }
  return true;
}

static bool emit_final(Compiler* compiler, StringBuilder* graph, Node* node, const char* label) {
  StringBuilder input = {0};
  bool ok = node_label(&input, node) && append_separator(graph) &&
            builder_printf(graph, "[%s]%s[%s]", input.data,
                           node->type == TYPE_VIDEO ? "null" : "anull", label);
  free(input.data);
  if (!ok) return compile_error(compiler, 0, "out of memory while building final mapping");
  return true;
}

static void mark_reachable(Node* node) {
  if (node == NULL || node->reachable) {
    return;
  }
  node->reachable = true;
  for (size_t i = 0; i < 2; i++) {
    if (node->input[i] != NULL) {
      node->input[i]->consumers++;
      mark_reachable(node->input[i]);
    }
  }
}

static bool compact_inputs(Compiler* compiler, Value* final) {
  bool* used = arena_alloc(compiler->arena, compiler->input_count * sizeof(*used));
  size_t* remap = arena_alloc(compiler->arena, compiler->input_count * sizeof(*remap));
  if ((used == NULL || remap == NULL) && compiler->input_count != 0) {
    return compile_error(compiler, 0, "out of memory while compacting inputs");
  }
  memset(used, 0, compiler->input_count * sizeof(*used));
  for (size_t i = 0; i < compiler->node_count; i++) {
    Node* node = compiler->nodes[i];
    if (node->reachable && node->kind == NODE_SOURCE) {
      used[node->source_index] = true;
    }
  }
  if (final->subtitle_input >= 0) {
    used[final->subtitle_input] = true;
  }

  size_t retained = 0;
  for (size_t i = 0; i < compiler->input_count; i++) {
    if (used[i]) {
      remap[i] = retained;
      compiler->inputs[retained] = compiler->inputs[i];
      compiler->inputs[retained].input_index = retained;
      retained++;
    }
  }
  for (size_t i = 0; i < compiler->node_count; i++) {
    Node* node = compiler->nodes[i];
    if (node->reachable && node->kind == NODE_SOURCE) {
      node->source_index = remap[node->source_index];
    }
  }
  if (final->subtitle_input >= 0) {
    final->subtitle_input = (ssize_t)remap[final->subtitle_input];
  }
  compiler->input_count = retained;
  return true;
}

static bool build_graph(Compiler* compiler, Value* final, StringBuilder* graph) {
  if (final->video != NULL) {
    final->video->consumers++;
    mark_reachable(final->video);
  }
  if (final->audio != NULL) {
    final->audio->consumers++;
    mark_reachable(final->audio);
  }
  if (!compact_inputs(compiler, final)) {
    return false;
  }
  for (size_t i = 0; i < compiler->node_count; i++) {
    Node* node = compiler->nodes[i];
    if (!node->reachable) continue;
    if (node->kind == NODE_SOURCE) {
      if (!emit_source_split(compiler, graph, node)) return false;
    } else if (!emit_filter(compiler, graph, node)) {
      return false;
    }
  }
  if (final->video != NULL && !emit_final(compiler, graph, final->video, "vout")) return false;
  if (final->audio != NULL && !emit_final(compiler, graph, final->audio, "aout")) return false;
  return true;
}

static bool add_argument(CompiledCommand* command, size_t* capacity, const char* argument) {
  if (command->argc + 1 >= *capacity) {
    size_t new_capacity = *capacity == 0 ? 16 : *capacity * 2;
    char** argv = arena_alloc_aligned(&command->arena, new_capacity * sizeof(*argv), _Alignof(char* ));
    if (argv == NULL) return false;
    if (command->argv != NULL) memcpy(argv, command->argv, command->argc * sizeof(*argv));
    command->argv = argv;
    *capacity = new_capacity;
  }
  command->argv[command->argc++] = arena_string(&command->arena, argument);
  command->argv[command->argc] = NULL;
  return command->argv[command->argc - 1] != NULL;
}

static bool shell_safe_char(char c) {
  return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') ||
         c == '_' || c == '-' || c == '.' || c == '/' || c == ':' || c == '=';
}

static bool append_shell_argument(StringBuilder* builder, const char* argument) {
  bool safe = *argument != '\0';
  for (const char* p = argument; *p != '\0'; p++) safe = safe && shell_safe_char(*p);
  if (safe) return builder_append(builder, argument);
  if (!builder_append(builder, "'")) return false;
  for (const char* p = argument; *p != '\0'; p++) {
    if (*p == '\'') {
      if (!builder_append(builder, "'\\''")) return false;
    } else {
      char character[2] = {*p, '\0'};
      if (!builder_append(builder, character)) return false;
    }
  }
  return builder_append(builder, "'");
}

bool compile_program(const Program* program, const TypedProgram* typed, ProbeCache* probes,
                     const char* output_path, CompiledCommand* out, Error* error) {
  (void)typed;
  *out = (CompiledCommand){.arena = arena_create()};
  Compiler compiler = {.arena = &out->arena, .probes = probes, .error = error};
  if (!execute_range(&compiler, program, 0, program->count)) goto failure;
  Value final = compiler.stack[0];
  StringBuilder graph = {0};
  if (!build_graph(&compiler, &final, &graph)) {
    free(graph.data);
    goto failure;
  }
  out->filter_complex = arena_string(&out->arena, graph.data == NULL ? "" : graph.data);
  free(graph.data);
  if (out->filter_complex == NULL) goto memory_failure;

  size_t argv_capacity = 0;
#define ARG(text) do { if (!add_argument(out, &argv_capacity, (text))) goto memory_failure; } while (0)
  ARG("ffmpeg");
  ARG("-y");
  for (size_t i = 0; i < compiler.input_count; i++) {
    ARG("-i");
    ARG(compiler.inputs[i].path);
  }
  ARG("-filter_complex");
  ARG(out->filter_complex);
  if (final.video != NULL) { ARG("-map"); ARG("[vout]"); }
  if (final.audio != NULL) { ARG("-map"); ARG("[aout]"); }
  if (final.subtitle_input >= 0) {
    char subtitle_map[64];
    snprintf(subtitle_map, sizeof(subtitle_map), "%zd:s:0", final.subtitle_input);
    ARG("-map"); ARG(subtitle_map); ARG("-c:s"); ARG("mov_text");
  }
  if (final.video != NULL) { ARG("-c:v"); ARG("libx264"); }
  if (final.audio != NULL) { ARG("-c:a"); ARG("aac"); }
  ARG(output_path);
#undef ARG

  StringBuilder shell = {0};
  for (size_t i = 0; i < out->argc; i++) {
    if (i != 0 && !builder_append(&shell, " ")) goto shell_failure;
    if (!append_shell_argument(&shell, out->argv[i])) goto shell_failure;
  }
  out->shell_command = arena_string(&out->arena, shell.data);
  free(shell.data);
  if (out->shell_command == NULL) goto memory_failure;
  return true;

shell_failure:
  free(shell.data);
memory_failure:
  error->line = 0;
  snprintf(error->message, sizeof(error->message), "out of memory");
failure:
  compiled_command_destroy(out);
  return false;
}

void compiled_command_destroy(CompiledCommand* command) {
  arena_destroy(&command->arena);
  *command = (CompiledCommand){0};
}

bool execute_command(const CompiledCommand* command, int* exit_code, Error* error) {
  pid_t child = fork();
  if (child < 0) {
    error->line = 0;
    snprintf(error->message, sizeof(error->message), "cannot start ffmpeg");
    return false;
  }
  if (child == 0) {
    execvp(command->argv[0], command->argv);
    _exit(127);
  }
  int status = 0;
  while (waitpid(child, &status, 0) < 0) {
    if (errno != EINTR) {
      error->line = 0;
      snprintf(error->message, sizeof(error->message), "could not wait for ffmpeg");
      return false;
    }
  }
  *exit_code = WIFEXITED(status) ? WEXITSTATUS(status) : 128 + WTERMSIG(status);
  return true;
}