#define STRVIEW_IMPLEMENTATION
#include "strview.h"

#include "compiler.h"
#include "parser.h"

#include <errno.h>
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

static char* read_file(const char* path) {
  FILE* file = fopen(path, "rb");
  if (file == NULL) {
    return NULL;
  }
  if (fseek(file, 0, SEEK_END) != 0) {
    fclose(file);
    return NULL;
  }
  long length = ftell(file);
  if (length < 0 || fseek(file, 0, SEEK_SET) != 0) {
    fclose(file);
    return NULL;
  }
  char* text = malloc((size_t)length + 1);
  if (text == NULL) {
    fclose(file);
    return NULL;
  }
  size_t read_count = fread(text, 1, (size_t)length, file);
  bool ok = read_count == (size_t)length && !ferror(file);
  fclose(file);
  if (!ok) {
    free(text);
    return NULL;
  }
  text[length] = '\0';
  return text;
}

static void print_error(const char* stage, const Error* error) {
  if (error->line > 0) {
    fprintf(stderr, "%s:%d: %s\n", stage, error->line, error->message);
  } else {
    fprintf(stderr, "%s: %s\n", stage, error->message);
  }
}

static void usage(FILE* stream, const char* name) {
  fprintf(stream, "usage: %s [-n] [--ffmpeg-arg ARG ...] PROGRAM OUTPUT [SCRIPT_ARGUMENT ...]\n", name);
  fprintf(stream, "  -n  compile and print without running ffmpeg\n");
  fprintf(stream, "  --ffmpeg-arg ARG  replace default video/audio encoding options; repeat per argument\n");
}

int main(int argc, char** argv) {
  if (argc == 4 && sv_equal(sv_from_cstr(argv[1]), svlit("--internal-subtitle-clip"))) {
    char* end = NULL;
    errno = 0;
    double duration = strtod(argv[3], &end);
    Error error = {0};
    if (errno != 0 || end == argv[3] || *end != '\0' || !isfinite(duration) || duration < 0 ||
        !subtitle_clip_file(argv[2], duration, &error)) {
      if (error.message[0] == '\0') snprintf(error.message, sizeof(error.message), "invalid duration");
      print_error("subtitle clip", &error);
      return 1;
    }
    return 0;
  }
  if (argc == 5 &&
      sv_equal(sv_from_cstr(argv[1]), svlit("--internal-subtitle-concat-list"))) {
    Error error = {0};
    if (!subtitle_write_concat_list(argv[2], argv[3], argv[4], &error)) {
      print_error("subtitle concat", &error);
      return 1;
    }
    return 0;
  }
  if (argc == 4 && sv_equal(sv_from_cstr(argv[1]), svlit("--internal-write-file"))) {
    Error error = {0};
    if (!write_generated_file(argv[2], argv[3], &error)) {
      print_error("generated metadata", &error);
      return 1;
    }
    return 0;
  }
  if (argc == 4 && sv_equal(sv_from_cstr(argv[1]), svlit("--internal-copy-file"))) {
    Error error = {0};
    if (!copy_generated_file(argv[2], argv[3], &error)) {
      print_error("font attachment", &error);
      return 1;
    }
    return 0;
  }
  if (argc == 3 && sv_equal(sv_from_cstr(argv[1]), svlit("--internal-mkdir"))) {
    Error error = {0};
    if (!ensure_generated_directory(argv[2], &error)) {
      print_error("font directory", &error);
      return 1;
    }
    return 0;
  }
  const char* executable_name = argv[0];
  bool dry_run = false;
  size_t ffmpeg_argument_count = 0;
  int argument = 1;
  while (argument < argc) {
    if (sv_equal(sv_from_cstr(argv[argument]), svlit("-n"))) {
      dry_run = true;
      argument++;
    } else if (sv_equal(sv_from_cstr(argv[argument]), svlit("--ffmpeg-arg"))) {
      if (argument + 1 >= argc) {
        fprintf(stderr, "%s: --ffmpeg-arg requires an argument\n", executable_name);
        return 2;
      }
      argv[ffmpeg_argument_count++] = argv[argument + 1];
      argument += 2;
    } else if (sv_equal(sv_from_cstr(argv[argument]), svlit("--"))) {
      argument++;
      break;
    } else if (argv[argument][0] == '-') {
      fprintf(stderr, "%s: unknown option %s\n", executable_name, argv[argument]);
      usage(stderr, executable_name);
      return 2;
    } else {
      break;
    }
  }
  if (argc - argument < 2) {
    usage(stderr, executable_name);
    return 2;
  }

  const char* program_path = argv[argument];
  const char* output_path = argv[argument + 1];
  size_t script_argument_count = (size_t)(argc - argument - 2);
  const char* const* script_arguments = (const char* const*)(argv + argument + 2);
  char* text = read_file(program_path);
  if (text == NULL) {
    fprintf(stderr, "%s: cannot read %s: %s\n", executable_name, program_path, strerror(errno));
    return 1;
  }

  Error error = {0};
  Program program;
  TypedProgram typed;
  CompiledCommand command;
  ProbeCache probes;
  probe_cache_init(&probes, NULL, NULL);

  if (!parse_program_with_args(text, script_argument_count, script_arguments, &program, &error)) {
    print_error(program_path, &error);
    free(text);
    probe_cache_destroy(&probes);
    return 1;
  }
  free(text);
  if (!typecheck_program(&program, &typed, &error)) {
    print_error(program_path, &error);
    program_destroy(&program);
    probe_cache_destroy(&probes);
    return 1;
  }
  if (!compile_program_with_ffmpeg_args(&program, &typed, &probes, output_path,
                                        ffmpeg_argument_count,
                                        (const char* const*)argv,
                                        &command, &error)) {
    print_error(program_path, &error);
    typed_program_destroy(&typed);
    program_destroy(&program);
    probe_cache_destroy(&probes);
    return 1;
  }

  printf("%s\n", command.shell_command);
  int exit_code = 0;
  if (!dry_run && !execute_command(&command, &exit_code, &error)) {
    print_error("ffmpeg", &error);
    exit_code = 1;
  }

  compiled_command_destroy(&command);
  typed_program_destroy(&typed);
  program_destroy(&program);
  probe_cache_destroy(&probes);
  return exit_code;
}