#define _POSIX_C_SOURCE 200809L
#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;
  size_t source_stream_index;
  const char* source_path;
  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 {
  Node** items;
  size_t count;
} TrackList;

typedef struct {
  ValueType type;
  double number;
  TrackList videos;
  TrackList audios;
  TrackList subtitles;
} 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 TrackList list_single(Compiler* compiler, Node* node) {
  Node** items = arena_alloc_aligned(compiler->arena, sizeof(*items), _Alignof(Node*));
  if (items != NULL) items[0] = node;
  return (TrackList){.items = items, .count = items == NULL ? 0 : 1};
}

static TrackList* value_list(Value* value, ValueType type) {
  if (type == TYPE_VIDEO) return &value->videos;
  if (type == TYPE_AUDIO) return &value->audios;
  return &value->subtitles;
}

static const TrackList* value_const_list(const Value* value, ValueType type) {
  if (type == TYPE_VIDEO) return &value->videos;
  if (type == TYPE_AUDIO) return &value->audios;
  return &value->subtitles;
}

static bool list_copy_with(Compiler* compiler, const TrackList* source, size_t count,
                           Node* append, TrackList* out) {
  size_t total = count + (append != NULL);
  Node** items = total == 0 ? NULL : arena_alloc_aligned(
    compiler->arena, total * sizeof(*items), _Alignof(Node*));
  if (items == NULL && total != 0) return false;
  if (count != 0) memcpy(items, source->items, count * sizeof(*items));
  if (append != NULL) items[count] = append;
  *out = (TrackList){.items = items, .count = total};
  return true;
}

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) {
  Duration result = {0};
  bool any = false;
  const TrackList* lists[] = {&value.videos, &value.audios, &value.subtitles};
  for (size_t kind = 0; kind < 3; kind++) {
    for (size_t i = 0; i < lists[kind]->count; i++) {
      Duration current = lists[kind]->items[i]->duration;
      if (!any) { result = current; any = true; }
      else result = duration_max(result, current);
    }
  }
  return any ? result : (Duration){0};
}

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");
  }

  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 && !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 && !probe.has_audio) {
    return compile_error(compiler, instruction.line, "source '%s' has no audio stream",
                         instruction.as.source.path);
  }

  Value value = {.type = (ValueType)instruction.as.source.kind};
  ProbeStream legacy[3];
  ProbeStream* streams = probe.streams;
  size_t stream_count = probe.stream_count;
  if (stream_count == 0) {
    if (probe.has_video) legacy[stream_count++] = (ProbeStream){TYPE_VIDEO, 0,
      probe.video_duration_known, probe.video_duration};
    if (probe.has_audio) legacy[stream_count++] = (ProbeStream){TYPE_AUDIO, probe.has_video ? 1 : 0,
      probe.audio_duration_known, probe.audio_duration};
    if (instruction.as.source.kind == SRC_SUBTITLES) legacy[stream_count++] = (ProbeStream){
      TYPE_SUBTITLES, 0, probe.subtitle_duration_known, probe.subtitle_duration};
    streams = legacy;
  }
  for (size_t i = 0; i < stream_count; i++) {
    ValueType type = streams[i].type;
    bool wanted = instruction.as.source.kind == SRC_COMBINED ||
                  (ValueType)instruction.as.source.kind == type;
    TrackList* list = value_list(&value, type);
    if (!wanted || (instruction.as.source.kind != SRC_COMBINED && list->count != 0)) continue;
    Node* node = source_node(compiler, type, input_index,
                             (Duration){streams[i].duration_known, streams[i].duration});
    if (node == NULL) return compile_error(compiler, instruction.line, "out of memory");
    node->source_stream_index = streams[i].stream_index;
    node->source_path = instruction.as.source.path;
    TrackList grown;
    if (!list_copy_with(compiler, list, list->count, node, &grown)) {
      return compile_error(compiler, instruction.line, "out of memory");
    }
    *list = grown;
  }
  if (instruction.as.source.kind == SRC_SUBTITLES && value.subtitles.count == 0) {
    return compile_error(compiler, instruction.line, "source '%s' has no subtitle stream",
                         instruction.as.source.path);
  }
  if (instruction.as.source.kind == SRC_COMBINED && value.videos.count == 0 &&
      value.audios.count == 0 && value.subtitles.count == 0) {
    return compile_error(compiler, instruction.line, "source '%s' has no supported media streams",
                         instruction.as.source.path);
  }
  return push_value(compiler, value, instruction.line);
}

static Node* stream_node(Value value) {
  const TrackList* list = value_const_list(&value, value.type);
  return list->count == 0 ? NULL : list->items[0];
}

static Value stream_value(Compiler* compiler, ValueType type, Node* node) {
  Value value = {.type = type};
  *value_list(&value, type) = list_single(compiler, 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 ValueType collection_operation_type(Operation operation) {
  switch (operation) {
    case OP_VIDEO_AT: case OP_TAKE_VIDEO: case OP_DROP_VIDEO: case OP_ADD_VIDEO:
    case OP_REPLACE_VIDEO: case OP_MOVE_VIDEO: return TYPE_VIDEO;
    case OP_AUDIO_AT: case OP_TAKE_AUDIO: case OP_DROP_AUDIO: case OP_ADD_AUDIO:
    case OP_REPLACE_AUDIO: case OP_MOVE_AUDIO: return TYPE_AUDIO;
    default: return TYPE_SUBTITLES;
  }
}

static bool list_remove(Compiler* compiler, const TrackList* source, size_t index, TrackList* out) {
  if (index >= source->count) return false;
  Node** items = source->count == 1 ? NULL : arena_alloc_aligned(
    compiler->arena, (source->count - 1) * sizeof(*items), _Alignof(Node*));
  if (items == NULL && source->count != 1) return false;
  if (index != 0) memcpy(items, source->items, index * sizeof(*items));
  if (index + 1 < source->count) {
    memcpy(items + index, source->items + index + 1,
           (source->count - index - 1) * sizeof(*items));
  }
  *out = (TrackList){items, source->count - 1};
  return true;
}

static bool list_replace(Compiler* compiler, const TrackList* source, size_t index,
                         Node* node, TrackList* out) {
  if (index >= source->count || !list_copy_with(compiler, source, source->count, NULL, out)) return false;
  out->items[index] = node;
  return true;
}

static bool list_move(Compiler* compiler, const TrackList* source, size_t from, size_t to,
                      TrackList* out) {
  if (from >= source->count || to >= source->count ||
      !list_copy_with(compiler, source, source->count, NULL, out)) return false;
  Node* moved = out->items[from];
  if (from < to) memmove(out->items + from, out->items + from + 1, (to - from) * sizeof(Node*));
  if (from > to) memmove(out->items + to + 1, out->items + to, (from - to) * sizeof(Node*));
  out->items[to] = moved;
  return true;
}

static Node* edit_node(Compiler* compiler, NodeKind kind, ValueType type, Node* left, Node* right,
                       double first, double second, int line) {
  Node* node = new_node(compiler, kind, type, left, right);
  if (node == NULL) { compile_error(compiler, line, "out of memory"); return NULL; }
  node->first = first;
  node->second = second;
  node->duration = left->duration;
  if (kind == NODE_SLICE) {
    if (left->duration.known) {
      double remaining = fmax(0, left->duration.seconds - first);
      node->duration = (Duration){true, fmin(second, remaining)};
    } else node->duration = (Duration){true, second};
  } else if (kind == NODE_TAKE) {
    node->duration = left->duration.known
      ? (Duration){true, fmin(left->duration.seconds, first)} : (Duration){true, first};
  } else if (kind == NODE_CONCAT) {
    node->duration = duration_sum(left->duration, right->duration);
  } else if (kind == NODE_DELAY) {
    node->duration = left->duration.known
      ? (Duration){true, left->duration.seconds + first} : (Duration){0};
  } else if (kind == NODE_MIX) {
    node->duration = duration_max(left->duration, right->duration);
  } else if (kind == NODE_SPEED) {
    node->duration = left->duration.known
      ? (Duration){true, left->duration.seconds / first} : (Duration){0};
  } else if (kind == NODE_FADE_OUT) {
    if (!left->duration.known) {
      compile_error(compiler, line, "fade-out requires a known stream duration");
      return NULL;
    }
    node->second = fmax(0, left->duration.seconds - first);
  }
  return node;
}

static bool transform_list(Compiler* compiler, const TrackList* input, ValueType type,
                           NodeKind kind, double first, double second, TrackList* output, int line) {
  Node** items = input->count == 0 ? NULL : arena_alloc_aligned(
    compiler->arena, input->count * sizeof(*items), _Alignof(Node*));
  if (items == NULL && input->count != 0) return compile_error(compiler, line, "out of memory");
  for (size_t i = 0; i < input->count; i++) {
    items[i] = edit_node(compiler, kind, type, input->items[i], NULL, first, second, line);
    if (items[i] == NULL) return false;
  }
  *output = (TrackList){items, input->count};
  return true;
}

static bool concat_lists(Compiler* compiler, const TrackList* left, const TrackList* right,
                         ValueType type, TrackList* output, int line) {
  if (left->count != right->count) {
    return compile_error(compiler, line, "combined concat requires matching track counts");
  }
  Node** items = left->count == 0 ? NULL : arena_alloc_aligned(
    compiler->arena, left->count * sizeof(*items), _Alignof(Node*));
  if (items == NULL && left->count != 0) return compile_error(compiler, line, "out of memory");
  for (size_t i = 0; i < left->count; i++) {
    items[i] = edit_node(compiler, NODE_CONCAT, type, left->items[i], right->items[i], 0, 0, line);
    if (items[i] == NULL) return false;
  }
  *output = (TrackList){items, left->count};
  return true;
}

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_GET_SUBTITLES || operation == OP_SPLIT) {
    Value combined = pop_value(compiler);
    ValueType type = operation == OP_GET_AUDIO ? TYPE_AUDIO
                     : operation == OP_GET_SUBTITLES ? TYPE_SUBTITLES : TYPE_VIDEO;
    const TrackList* list = value_const_list(&combined, type);
    if (list->count == 0) return compile_error(compiler, line, "combined value has no %s track", type_name(type));
    if (!push_value(compiler, stream_value(compiler, type, list->items[0]), line)) return false;
    if (operation == OP_SPLIT) {
      if (combined.audios.count == 0) return compile_error(compiler, line, "combined value has no AUDIO track");
      if (!push_value(compiler, stream_value(compiler, TYPE_AUDIO, combined.audios.items[0]), line)) return false;
    }
    return true;
  }
  if ((operation >= OP_VIDEO_AT && operation <= OP_TAKE_SUBTITLES) ||
      (operation >= OP_DROP_VIDEO && operation <= OP_MOVE_SUBTITLES)) {
    ValueType type = collection_operation_type(operation);
    if (operation >= OP_VIDEO_AT && operation <= OP_DROP_SUBTITLES) {
      size_t index = (size_t)pop_value(compiler).number;
      Value combined = pop_value(compiler);
      TrackList* list = value_list(&combined, type);
      if (index >= list->count) return compile_error(compiler, line, "%s index %zu is out of range", type_name(type), index);
      Node* selected = list->items[index];
      bool removes = operation >= OP_TAKE_VIDEO && operation <= OP_DROP_SUBTITLES;
      if (removes) {
        TrackList changed;
        if (!list_remove(compiler, list, index, &changed)) return compile_error(compiler, line, "out of memory");
        *list = changed;
      }
      if (!push_value(compiler, combined, line)) return false;
      if (operation <= OP_TAKE_SUBTITLES && !push_value(
            compiler, stream_value(compiler, type, selected), line)) return false;
      return true;
    }
    if (operation >= OP_ADD_VIDEO && operation <= OP_ADD_SUBTITLES) {
      Value track = pop_value(compiler);
      Value combined = pop_value(compiler);
      TrackList* list = value_list(&combined, type);
      TrackList changed;
      if (!list_copy_with(compiler, list, list->count, stream_node(track), &changed)) {
        return compile_error(compiler, line, "out of memory");
      }
      *list = changed;
      return push_value(compiler, combined, line);
    }
    if (operation >= OP_REPLACE_VIDEO && operation <= OP_REPLACE_SUBTITLES) {
      Value track = pop_value(compiler);
      size_t index = (size_t)pop_value(compiler).number;
      Value combined = pop_value(compiler);
      TrackList* list = value_list(&combined, type);
      TrackList changed;
      if (!list_replace(compiler, list, index, stream_node(track), &changed)) {
        return compile_error(compiler, line, "%s index %zu is out of range", type_name(type), index);
      }
      *list = changed;
      return push_value(compiler, combined, line);
    }
    size_t to = (size_t)pop_value(compiler).number;
    size_t from = (size_t)pop_value(compiler).number;
    Value combined = pop_value(compiler);
    TrackList* list = value_list(&combined, type);
    TrackList changed;
    if (!list_move(compiler, list, from, to, &changed)) {
      return compile_error(compiler, line, "%s move indexes are out of range", type_name(type));
    }
    *list = changed;
    return push_value(compiler, combined, line);
  }
  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(compiler, TYPE_AUDIO, node), line);
  }
  if (operation == OP_MUX) {
    Value audio = pop_value(compiler);
    Value video = pop_value(compiler);
    Value combined = {.type = TYPE_COMBINED, .videos = video.videos, .audios = audio.audios};
    return push_value(compiler, combined, line);
  }
  if (operation == OP_ATTACH) {
    Value subtitles = pop_value(compiler);
    Value combined = pop_value(compiler);
    TrackList changed;
    if (!list_copy_with(compiler, &combined.subtitles, combined.subtitles.count,
                        stream_node(subtitles), &changed)) return compile_error(compiler, line, "out of memory");
    combined.subtitles = changed;
    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;
    if (operation == OP_CONCAT && left.type == TYPE_COMBINED) {
      Value combined = {.type = TYPE_COMBINED};
      if (!concat_lists(compiler, &left.videos, &right.videos, TYPE_VIDEO, &combined.videos, line) ||
          !concat_lists(compiler, &left.audios, &right.audios, TYPE_AUDIO, &combined.audios, line) ||
          !concat_lists(compiler, &left.subtitles, &right.subtitles, TYPE_SUBTITLES,
                        &combined.subtitles, line)) return false;
      return push_value(compiler, combined, line);
    }
    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");
    }
  }

  if (left.type == TYPE_COMBINED) {
    Value combined = {.type = TYPE_COMBINED};
    if (!transform_list(compiler, &left.videos, TYPE_VIDEO, kind,
                        operation == OP_SLICE ? right.number : parameter.number,
                        operation == OP_SLICE ? parameter.number : 0, &combined.videos, line) ||
        !transform_list(compiler, &left.audios, TYPE_AUDIO, kind,
                        operation == OP_SLICE ? right.number : parameter.number,
                        operation == OP_SLICE ? parameter.number : 0, &combined.audios, line) ||
        !transform_list(compiler, &left.subtitles, TYPE_SUBTITLES, kind,
                        operation == OP_SLICE ? right.number : parameter.number,
                        operation == OP_SLICE ? parameter.number : 0, &combined.subtitles, line)) return false;
    return push_value(compiler, combined, line);
  }

  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(compiler, 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 || instruction.kind == INST_INDEX) {
      Value value = {
        .type = instruction.kind == INST_DURATION ? TYPE_DURATION
                : instruction.kind == INST_SCALAR ? TYPE_SCALAR
                : instruction.kind == INST_INDEX ? TYPE_INDEX : TYPE_BOOL,
        .number = instruction.as.number,
      };
      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:%zu", node->source_index, node->source_stream_index);
  }
  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:%zu]%s=%u", node->source_index, node->source_stream_index,
                      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) {
  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 && node->type != TYPE_SUBTITLES) {
      used[node->source_index] = 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->type != TYPE_SUBTITLES) {
      node->source_index = remap[node->source_index];
    }
  }
  compiler->input_count = retained;
  return true;
}

static bool build_graph(Compiler* compiler, Value* final, StringBuilder* graph) {
  TrackList* lists[] = {&final->videos, &final->audios, &final->subtitles};
  for (size_t kind = 0; kind < 3; kind++) {
    for (size_t i = 0; i < lists[kind]->count; i++) {
      lists[kind]->items[i]->consumers++;
      mark_reachable(lists[kind]->items[i]);
    }
  }
  if (!compact_inputs(compiler)) {
    return false;
  }
  for (size_t i = 0; i < compiler->node_count; i++) {
    Node* node = compiler->nodes[i];
    if (!node->reachable) continue;
    if (node->type == TYPE_SUBTITLES) {
      continue;
    } else if (node->kind == NODE_SOURCE) {
      if (!emit_source_split(compiler, graph, node)) return false;
    } else if (!emit_filter(compiler, graph, node)) {
      return false;
    }
  }
  for (size_t i = 0; i < final->videos.count; i++) {
    char label[64];
    snprintf(label, sizeof(label), i == 0 ? "vout" : "vout%zu", i);
    if (!emit_final(compiler, graph, final->videos.items[i], label)) return false;
  }
  for (size_t i = 0; i < final->audios.count; i++) {
    char label[64];
    snprintf(label, sizeof(label), i == 0 ? "aout" : "aout%zu", i);
    if (!emit_final(compiler, graph, final->audios.items[i], label)) 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 StagedCommand* add_stage(CompiledCommand* command) {
  if (command->stage_count == command->stage_capacity) {
    size_t capacity = command->stage_capacity == 0 ? 8 : command->stage_capacity * 2;
    StagedCommand* stages = arena_alloc_aligned(&command->arena, capacity * sizeof(*stages),
                                                 _Alignof(StagedCommand));
    if (stages == NULL) return NULL;
    if (command->stages != NULL) {
      memcpy(stages, command->stages, command->stage_count * sizeof(*stages));
    }
    command->stages = stages;
    command->stage_capacity = capacity;
  }
  StagedCommand* stage = &command->stages[command->stage_count++];
  *stage = (StagedCommand){0};
  return stage;
}

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

static const char* temporary_path(CompiledCommand* command, Error* error) {
  char pattern[] = "/tmp/vedit-subtitle-XXXXXX";
  int fd = mkstemp(pattern);
  if (fd < 0) {
    error->line = 0;
    snprintf(error->message, sizeof(error->message), "cannot create subtitle temporary file");
    return NULL;
  }
  close(fd);
  if (command->temporary_count == command->temporary_capacity) {
    size_t capacity = command->temporary_capacity == 0 ? 8 : command->temporary_capacity * 2;
    const char** paths = arena_alloc_aligned(&command->arena, capacity * sizeof(*paths),
                                              _Alignof(const char*));
    if (paths == NULL) { unlink(pattern); return NULL; }
    if (command->temporary_paths != NULL) {
      memcpy(paths, command->temporary_paths, command->temporary_count * sizeof(*paths));
    }
    command->temporary_paths = paths;
    command->temporary_capacity = capacity;
  }
  const char* path = arena_string(&command->arena, pattern);
  if (path == NULL) { unlink(pattern); return NULL; }
  command->temporary_paths[command->temporary_count++] = path;
  return path;
}

static bool add_subtitle_stage(CompiledCommand* command, Node* node, Compiler* compiler,
                               const char** output, Error* error) {
  const char* left = NULL;
  const char* right = NULL;
  if (node->kind != NODE_SOURCE &&
      !add_subtitle_stage(command, node->input[0], compiler, &left, error)) return false;
  if (node->kind == NODE_CONCAT &&
      !add_subtitle_stage(command, node->input[1], compiler, &right, error)) return false;
  const char* path = temporary_path(command, error);
  if (path == NULL) return false;
  StagedCommand* stage = add_stage(command);
  if (stage == NULL) return false;
  stage->subtitle_output = path;
  if (node->kind == NODE_SLICE || node->kind == NODE_TAKE) {
    stage->clip_subtitles = true;
    stage->clip_duration = node->duration.seconds;
  }
  size_t capacity = 0;
#define SARG(text) do { if (!add_stage_argument(command, stage, &capacity, (text))) return false; } while (0)
  SARG("ffmpeg"); SARG("-y");
  char number[64];
  if (node->kind == NODE_SOURCE) {
    SARG("-i"); SARG(node->source_path);
    snprintf(number, sizeof(number), "0:%zu", node->source_stream_index);
    SARG("-map"); SARG(number);
  } else if (node->kind == NODE_SLICE) {
    snprintf(number, sizeof(number), "%.9g", node->first);
    SARG("-ss"); SARG(number);
    snprintf(number, sizeof(number), "%.9g", node->second);
    SARG("-t"); SARG(number); SARG("-i"); SARG(left); SARG("-map"); SARG("0:s:0");
  } else if (node->kind == NODE_TAKE) {
    snprintf(number, sizeof(number), "%.9g", node->first);
    SARG("-t"); SARG(number); SARG("-i"); SARG(left); SARG("-map"); SARG("0:s:0");
  } else if (node->kind == NODE_DELAY) {
    snprintf(number, sizeof(number), "%.9g", node->first);
    SARG("-itsoffset"); SARG(number); SARG("-i"); SARG(left); SARG("-map"); SARG("0:s:0");
  } else if (node->kind == NODE_SPEED) {
    snprintf(number, sizeof(number), "%.9g", 1.0 / node->first);
    SARG("-itsscale"); SARG(number); SARG("-i"); SARG(left); SARG("-map"); SARG("0:s:0");
  } else if (node->kind == NODE_CONCAT) {
    const char* list = temporary_path(command, error);
    if (list == NULL) return false;
    FILE* file = fopen(list, "wb");
    if (file == NULL) return compile_error(compiler, 0, "cannot write subtitle concat list");
    bool wrote = fprintf(file, "file '%s'\nfile '%s'\n", left, right) >= 0;
    bool closed = fclose(file) == 0;
    if (!wrote || !closed) {
      return compile_error(compiler, 0, "cannot write subtitle concat list");
    }
    SARG("-f"); SARG("concat"); SARG("-safe"); SARG("0"); SARG("-i"); SARG(list);
    SARG("-map"); SARG("0:s:0");
  } else {
    return compile_error(compiler, 0, "unsupported subtitle operation");
  }
  SARG("-f"); SARG("srt"); SARG(path);
#undef SARG
  *output = path;
  return true;
}

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 const char* output_subtitle_codec(const char* path) {
  const char* extension = strrchr(path, '.');
  if (extension != NULL &&
      (strcmp(extension, ".mp4") == 0 || strcmp(extension, ".mov") == 0 ||
       strcmp(extension, ".m4v") == 0)) return "mov_text";
  return "srt";
}

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];
  if (final.videos.count == 0 && final.audios.count == 0 && final.subtitles.count == 0) {
    compile_error(&compiler, 0, "final media value contains no tracks");
    goto failure;
  }
  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;

  const char** subtitle_paths = final.subtitles.count == 0 ? NULL : arena_alloc_aligned(
    &out->arena, final.subtitles.count * sizeof(*subtitle_paths), _Alignof(const char*));
  if (subtitle_paths == NULL && final.subtitles.count != 0) goto memory_failure;
  for (size_t i = 0; i < final.subtitles.count; i++) {
    if (!add_subtitle_stage(out, final.subtitles.items[i], &compiler, &subtitle_paths[i], error)) {
      goto 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);
  }
  for (size_t i = 0; i < final.subtitles.count; i++) { ARG("-i"); ARG(subtitle_paths[i]); }
  if (out->filter_complex[0] != '\0') { ARG("-filter_complex"); ARG(out->filter_complex); }
  for (size_t i = 0; i < final.videos.count; i++) {
    char label[64];
    snprintf(label, sizeof(label), i == 0 ? "[vout]" : "[vout%zu]", i);
    ARG("-map"); ARG(label);
  }
  for (size_t i = 0; i < final.audios.count; i++) {
    char label[64];
    snprintf(label, sizeof(label), i == 0 ? "[aout]" : "[aout%zu]", i);
    ARG("-map"); ARG(label);
  }
  for (size_t i = 0; i < final.subtitles.count; i++) {
    char subtitle_map[64];
    snprintf(subtitle_map, sizeof(subtitle_map), "%zu:s:0", compiler.input_count + i);
    ARG("-map"); ARG(subtitle_map);
  }
  if (final.subtitles.count != 0) { ARG("-c:s"); ARG(output_subtitle_codec(output_path)); }
  if (final.videos.count != 0) { ARG("-c:v"); ARG("libx264"); }
  if (final.audios.count != 0) { ARG("-c:a"); ARG("aac"); }
  ARG(output_path);
#undef ARG

  StringBuilder shell = {0};
  for (size_t stage_index = 0; stage_index < out->stage_count; stage_index++) {
    StagedCommand* stage = &out->stages[stage_index];
    if (shell.length != 0 && !builder_append(&shell, " && ")) goto shell_failure;
    for (size_t i = 0; i < stage->argc; i++) {
      if (i != 0 && !builder_append(&shell, " ")) goto shell_failure;
      if (!append_shell_argument(&shell, stage->argv[i])) goto shell_failure;
    }
  }
  if (shell.length != 0 && !builder_append(&shell, " && ")) goto shell_failure;
  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) {
  for (size_t i = 0; i < command->temporary_count; i++) unlink(command->temporary_paths[i]);
  arena_destroy(&command->arena);
  *command = (CompiledCommand){0};
}

static bool subtitle_timestamp(const char* line, long long* start, long long* end) {
  int sh, sm, ss, sms, eh, em, es, ems;
  if (sscanf(line, "%d:%d:%d,%d --> %d:%d:%d,%d",
             &sh, &sm, &ss, &sms, &eh, &em, &es, &ems) != 8) return false;
  *start = (((long long)sh * 60 + sm) * 60 + ss) * 1000 + sms;
  *end = (((long long)eh * 60 + em) * 60 + es) * 1000 + ems;
  return true;
}

static void write_subtitle_timestamp(FILE* file, long long start, long long end) {
  fprintf(file, "%02lld:%02lld:%02lld,%03lld --> %02lld:%02lld:%02lld,%03lld\n",
          start / 3600000, start / 60000 % 60, start / 1000 % 60, start % 1000,
          end / 3600000, end / 60000 % 60, end / 1000 % 60, end % 1000);
}

static bool clip_subtitle_file(const char* path, double duration, Error* error) {
  FILE* source = fopen(path, "rb");
  FILE* clipped = tmpfile();
  if (source == NULL || clipped == NULL) {
    if (source != NULL) fclose(source);
    if (clipped != NULL) fclose(clipped);
    error->line = 0;
    snprintf(error->message, sizeof(error->message), "cannot post-process subtitles");
    return false;
  }
  char* line = NULL;
  size_t capacity = 0;
  unsigned number = 1;
  long long limit = llround(duration * 1000.0);
  while (getline(&line, &capacity, source) >= 0) {
    if (line[0] == '\n' || line[0] == '\r') continue;
    if (getline(&line, &capacity, source) < 0) break;
    long long start, end;
    if (!subtitle_timestamp(line, &start, &end)) {
      free(line); fclose(source); fclose(clipped);
      error->line = 0;
      snprintf(error->message, sizeof(error->message), "cannot parse generated subtitles");
      return false;
    }
    bool keep = start < limit && end > 0;
    if (end > limit) end = limit;
    if (keep) {
      fprintf(clipped, "%u\n", number++);
      write_subtitle_timestamp(clipped, start < 0 ? 0 : start, end);
    }
    while (getline(&line, &capacity, source) >= 0 && line[0] != '\n' && line[0] != '\r') {
      if (keep) fputs(line, clipped);
    }
    if (keep) fputc('\n', clipped);
  }
  free(line);
  fclose(source);
  rewind(clipped);
  FILE* destination = fopen(path, "wb");
  if (destination == NULL) {
    fclose(clipped);
    error->line = 0;
    snprintf(error->message, sizeof(error->message), "cannot rewrite generated subtitles");
    return false;
  }
  char buffer[4096];
  size_t count;
  bool ok = true;
  while ((count = fread(buffer, 1, sizeof(buffer), clipped)) != 0) {
    if (fwrite(buffer, 1, count, destination) != count) { ok = false; break; }
  }
  ok = ok && !ferror(clipped);
  if (fclose(destination) != 0) ok = false;
  fclose(clipped);
  if (!ok) {
    error->line = 0;
    snprintf(error->message, sizeof(error->message), "cannot rewrite generated subtitles");
  }
  return ok;
}

bool execute_command(const CompiledCommand* command, int* exit_code, Error* error) {
  for (size_t i = 0; i < command->stage_count; i++) {
    pid_t stage_child = fork();
    if (stage_child < 0) {
      error->line = 0;
      snprintf(error->message, sizeof(error->message), "cannot start subtitle processing stage");
      return false;
    }
    if (stage_child == 0) {
      execvp(command->stages[i].argv[0], command->stages[i].argv);
      _exit(127);
    }
    int stage_status = 0;
    pid_t waited;
    do { waited = waitpid(stage_child, &stage_status, 0); } while (waited < 0 && errno == EINTR);
    if (waited < 0) {
      error->line = 0;
      snprintf(error->message, sizeof(error->message), "could not wait for subtitle stage");
      return false;
    }
    if (!WIFEXITED(stage_status) || WEXITSTATUS(stage_status) != 0) {
      *exit_code = WIFEXITED(stage_status) ? WEXITSTATUS(stage_status)
                                           : 128 + WTERMSIG(stage_status);
      return true;
    }
    if (command->stages[i].clip_subtitles &&
        !clip_subtitle_file(command->stages[i].subtitle_output,
                            command->stages[i].clip_duration, error)) return false;
  }
  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;
}