#define STRVIEW_IMPLEMENTATION
#include "strview.h"

#include "parser.h"

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

static const char* operation_names[] = {
  "dup", "drop", "swap", "over", "rot", "get-video", "get-audio", "get-subtitles", "split",
  "video-at", "audio-at", "subtitles-at", "take-video", "take-audio", "take-subtitles",
  "drop-video", "drop-audio", "drop-subtitles", "add-video", "add-audio", "add-subtitles",
  "replace-video", "replace-audio", "replace-subtitles", "move-video", "move-audio",
  "move-subtitles", "length",
  "slice", "take", "concat", "delay", "mux", "attach-subtitles", "video-to-combined",
  "burn-subtitles", "volume", "mix", "silence",
  "speed", "fade-in", "fade-out", "add", "sub", "mul", "div", "eq", "ne", "lt", "le",
  "gt", "ge", "and", "or", "not", "if", "else", "end", "begin", "while", "repeat",
};

const char* type_name(ValueType type) {
  static const char* names[] = {
    "VIDEO", "AUDIO", "COMBINED", "SUBTITLES", "BURN_STYLE", "INDEX", "DURATION", "SCALAR", "BOOL",
  };
  return names[type];
}

const char* operation_name(Operation operation) {
  return operation_names[operation];
}

static bool fail(Error* error, int line, const char* format, const char* arg) {
  error->line = line;
  snprintf(error->message, sizeof(error->message), format, arg);
  return false;
}

static char* copy_view(Arena* arena, strview view) {
  char* copy = arena_alloc(arena, view.len + 1);
  if (copy == NULL) {
    return NULL;
  }
  memcpy(copy, view.data, view.len);
  copy[view.len] = '\0';
  return copy;
}

static bool push_instruction(Program* program, Instruction instruction) {
  if (program->count == program->capacity) {
    size_t new_capacity = program->capacity == 0 ? 32 : program->capacity * 2;
    Instruction* items = arena_alloc_aligned(
      &program->arena, new_capacity * sizeof(*items), _Alignof(Instruction));
    if (items == NULL) {
      return false;
    }
    if (program->items != NULL) {
      memcpy(items, program->items, program->count * sizeof(*items));
    }
    program->items = items;
    program->capacity = new_capacity;
  }
  program->items[program->count++] = instruction;
  return true;
}

static bool next_token(strview* input, strview* token, bool* quoted, Error* error, int line) {
  sv_trim_left(input);
  *quoted = false;
  if (input->len == 0) {
    return false;
  }

  if (input->data[0] == '"') {
    *quoted = true;
    sv_chop_left(input, 1);
    size_t length = 0;
    while (length < input->len && input->data[length] != '"') {
      if (input->data[length] == '\\') {
        return fail(error, line,
                    "escapes are not supported in quoted paths%s", "");
      }
      length++;
    }
    if (length == input->len) {
      return fail(error, line, "unterminated quoted string%s", "");
    }
    *token = sv_chop_left(input, length);
    sv_chop_left(input, 1);
    return true;
  }

  size_t length = 0;
  while (length < input->len && !sv_space_predicate(input->data[length])) {
    length++;
  }
  *token = sv_chop_left(input, length);
  return true;
}

static bool parse_number(strview input, double* result) {
  char buffer[128];
  if (input.len == 0 || input.len >= sizeof(buffer)) {
    return false;
  }
  memcpy(buffer, input.data, input.len);
  buffer[input.len] = '\0';

  char* end = NULL;
  errno = 0;
  *result = strtod(buffer, &end);
  return errno == 0 && *end == '\0' && isfinite(*result);
}

static bool parse_index(strview input, double* result) {
  if (!parse_number(input, result) || *result < 0 || floor(*result) != *result ||
      *result > (double)SIZE_MAX) {
    return false;
  }
  return true;
}

static bool parse_duration(strview input, double* result) {
  if (input.len > 2 && sv_ends_with(input, svlit("ms"))) {
    sv_chop_right(&input, 2);
    return parse_number(input, result) && (*result /= 1000.0) >= 0;
  }
  if (input.len > 1 && input.data[input.len - 1] == 's') {
    sv_chop_right(&input, 1);
    return parse_number(input, result) && *result >= 0;
  }

  bool found = false;
  size_t colon = sv_find_char(input, ':', &found);
  if (!found) {
    return false;
  }
  strview minutes_view = sv_from_data(input.data, colon);
  strview seconds_view =
    sv_from_data(input.data + colon + 1, input.len - colon - 1);
  double minutes = 0;
  double seconds = 0;
  if (!parse_number(minutes_view, &minutes) ||
      !parse_number(seconds_view, &seconds) || minutes < 0 || seconds < 0 ||
      seconds >= 60) {
    return false;
  }
  *result = minutes * 60 + seconds;
  return true;
}

static bool source_kind(strview word, SourceKind* kind) {
  if (sv_equal(word, svlit("video"))) {
    *kind = SRC_VIDEO;
  } else if (sv_equal(word, svlit("audio"))) {
    *kind = SRC_AUDIO;
  } else if (sv_equal(word, svlit("combined"))) {
    *kind = SRC_COMBINED;
  } else if (sv_equal(word, svlit("subtitles"))) {
    *kind = SRC_SUBTITLES;
  } else {
    return false;
  }
  return true;
}

static int find_operation(strview word) {
  for (int i = 0; i < OP_COUNT; i++) {
    if (sv_equal(word, sv_from_cstr(operation_names[i]))) {
      return i;
    }
  }
  return -1;
}

static char* expand_string(Arena* arena, strview input, size_t argument_count,
                           const char* const* arguments, Error* error, int line) {
  size_t output_size = 1;
  for (size_t i = 0; i < input.len;) {
    if (input.data[i] != '$' || i + 1 == input.len || !sv_numeric_predicate(input.data[i + 1])) {
      output_size++;
      i++;
      continue;
    }
    size_t index = 0;
    i++;
    if (input.data[i] == '0') {
      fail(error, line, "invalid script argument in '%s'", copy_view(arena, input));
      return NULL;
    }
    while (i < input.len && sv_numeric_predicate(input.data[i])) {
      unsigned digit = (unsigned)(input.data[i] - '0');
      if (index > (SIZE_MAX - digit) / 10) {
        fail(error, line, "invalid script argument in '%s'", copy_view(arena, input));
        return NULL;
      }
      index = index * 10 + digit;
      i++;
    }
    if (index == 0 || index > argument_count) {
      fail(error, line, "invalid or missing script argument in '%s'", copy_view(arena, input));
      return NULL;
    }
    size_t length = sv_from_cstr(arguments[index - 1]).len;
    if (length > SIZE_MAX - output_size) {
      fail(error, line, "expanded string is too large%s", "");
      return NULL;
    }
    output_size += length;
  }

  char* output = arena_alloc(arena, output_size);
  if (output == NULL) {
    fail(error, line, "out of memory%s", "");
    return NULL;
  }
  size_t written = 0;
  for (size_t i = 0; i < input.len;) {
    if (input.data[i] != '$' || i + 1 == input.len || !sv_numeric_predicate(input.data[i + 1])) {
      output[written++] = input.data[i++];
      continue;
    }
    size_t index = 0;
    i++;
    while (i < input.len && sv_numeric_predicate(input.data[i])) {
      index = index * 10 + (unsigned)(input.data[i] - '0');
      i++;
    }
    strview argument = sv_from_cstr(arguments[index - 1]);
    memcpy(output + written, argument.data, argument.len);
    written += argument.len;
  }
  output[written] = '\0';
  return output;
}

bool parse_program_with_args(const char* text, size_t argument_count, const char* const* arguments,
                             Program* out, Error* error) {
  *error = (Error){0};
  *out = (Program){.arena = arena_create()};
  strview input = sv_from_cstr(text);
  int line = 1;

  while (input.len != 0) {
    strview current = sv_chop_by_char(&input, '\n');
    bool in_quotes = false;
    for (size_t i = 0; i < current.len; i++) {
      if (current.data[i] == '"') {
        in_quotes = !in_quotes;
      } else if (current.data[i] == '#' && !in_quotes) {
        current.len = i;
        break;
      }
    }
    sv_trim(&current);
    if (current.len == 0) {
      line++;
      continue;
    }

    strview word;
    bool quoted = false;
    if (!next_token(&current, &word, &quoted, error, line)) {
      goto failure;
    }
    if (quoted) {
      fail(error, line, "unexpected quoted token%s", "");
      goto failure;
    }

    Instruction instruction = {.line = line};
    SourceKind kind;
    if (sv_equal(word, svlit("burn-style"))) {
      strview style;
      bool style_quoted = false;
      if (!next_token(&current, &style, &style_quoted, error, line) || !style_quoted) {
        fail(error, line, "burn style must be quoted%s", "");
        goto failure;
      }
      instruction.kind = INST_BURN_STYLE;
      instruction.as.text = expand_string(&out->arena, style, argument_count, arguments, error, line);
      if (instruction.as.text == NULL) goto failure;
    } else if (source_kind(word, &kind)) {
      strview path;
      bool path_quoted = false;
      if (!next_token(&current, &path, &path_quoted, error, line)) {
        if (error->message[0] == '\0') {
          fail(error, line, "missing source path%s", "");
        }
        goto failure;
      }
      if (!path_quoted) {
        fail(error, line, "source path must be quoted%s", "");
        goto failure;
      }
      instruction.kind = INST_SOURCE;
      instruction.as.source.kind = kind;
      instruction.as.source.path =
        expand_string(&out->arena, path, argument_count, arguments, error, line);
      if (instruction.as.source.path == NULL) {
        if (error->message[0] == '\0') {
          fail(error, line, "out of memory%s", "");
        }
        goto failure;
      }
    } else if (sv_equal(word, svlit("true")) || sv_equal(word, svlit("false"))) {
      instruction.kind = INST_BOOL;
      instruction.as.number = sv_equal(word, svlit("true")) ? 1.0 : 0.0;
    } else if (sv_equal(word, svlit("duration")) || sv_equal(word, svlit("scalar")) ||
               sv_equal(word, svlit("index"))) {
      strview literal;
      bool literal_quoted = false;
      if (!next_token(&current, &literal, &literal_quoted, error, line) ||
          literal_quoted) {
        fail(error, line, "missing or invalid numeric literal%s", "");
        goto failure;
      }
      instruction.kind = sv_equal(word, svlit("duration")) ? INST_DURATION
                         : sv_equal(word, svlit("scalar")) ? INST_SCALAR : INST_INDEX;
      bool valid = instruction.kind == INST_DURATION
                     ? parse_duration(literal, &instruction.as.number)
                   : instruction.kind == INST_INDEX
                     ? parse_index(literal, &instruction.as.number)
                     : parse_number(literal, &instruction.as.number);
      if (!valid) {
        fail(error, line,
             instruction.kind == INST_DURATION
               ? "malformed duration '%s'"
               : instruction.kind == INST_INDEX ? "malformed index '%s'"
               : "malformed scalar '%s'",
             copy_view(&out->arena, literal));
        goto failure;
      }
    } else {
      int operation = find_operation(word);
      if (operation < 0) {
        fail(error, line, "unknown instruction '%s'",
             copy_view(&out->arena, word));
        goto failure;
      }
      instruction.kind = INST_OPERATION;
      instruction.as.op = (Operation)operation;
    }

    sv_trim(&current);
    if (current.len != 0) {
      fail(error, line, "unexpected trailing input '%s'",
           copy_view(&out->arena, current));
      goto failure;
    }
    if (!push_instruction(out, instruction)) {
      fail(error, line, "out of memory%s", "");
      goto failure;
    }
    line++;
  }
  return true;

failure:
  program_destroy(out);
  return false;
}

bool parse_program(const char* text, Program* out, Error* error) {
  return parse_program_with_args(text, 0, NULL, out, error);
}

void program_destroy(Program* program) {
  arena_destroy(&program->arena);
  *program = (Program){0};
}