/*
 *
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 *
 */

#include <string.h>
#include <assert.h>
#include <ctype.h>
#include "transform.h"

typedef struct {
  const char *start;
  size_t size;
} pn_group_t;

#define MAX_GROUP (64)

typedef struct {
  size_t groups;
  pn_group_t group[MAX_GROUP];
} pn_matcher_t;

typedef struct {
  pn_string_t *pattern;
  pn_string_t *substitution;
} pn_rule_t;

struct pn_transform_t {
  pn_list_t *rules;
  pn_matcher_t matcher;
  bool matched;
};

static void pn_rule_finalize(void *object)
{
  pn_rule_t *rule = (pn_rule_t *) object;
  pn_free(rule->pattern);
  pn_free(rule->substitution);
}

#define CID_pn_rule CID_pn_object
#define pn_rule_initialize NULL
#define pn_rule_hashcode NULL
#define pn_rule_compare NULL
#define pn_rule_inspect NULL

pn_rule_t *pn_rule(const char *pattern, const char *substitution)
{
  static const pn_class_t clazz = PN_CLASS(pn_rule);
  pn_rule_t *rule = (pn_rule_t *) pn_class_new(&clazz, sizeof(pn_rule_t));
  rule->pattern = pn_string(pattern);
  rule->substitution = pn_string(substitution);
  return rule;
}

static void pn_transform_finalize(void *object)
{
  pn_transform_t *transform = (pn_transform_t *) object;
  pn_free(transform->rules);
}

#define CID_pn_transform CID_pn_object
#define pn_transform_initialize NULL
#define pn_transform_hashcode NULL
#define pn_transform_compare NULL
#define pn_transform_inspect NULL

pn_transform_t *pn_transform()
{
  static const pn_class_t clazz = PN_CLASS(pn_transform);
  pn_transform_t *transform = (pn_transform_t *) pn_class_new(&clazz, sizeof(pn_transform_t));
  transform->rules = pn_list(PN_OBJECT, 0);
  transform->matched = false;
  return transform;
}

void pn_transform_rule(pn_transform_t *transform, const char *pattern,
                       const char *substitution)
{
  assert(transform);
  pn_rule_t *rule = pn_rule(pattern, substitution);
  pn_list_add(transform->rules, rule);
  pn_decref(rule);
}

static void pni_sub(pn_matcher_t *matcher, size_t group, const char *text, size_t matched)
{
  if (group > matcher->groups) {
    matcher->groups = group;
  }
  matcher->group[group].start = text - matched;
  matcher->group[group].size = matched;
}

static bool pni_match_r(pn_matcher_t *matcher, const char *pattern, const char *text, size_t group, size_t matched)
{
  bool match;

  char p = *pattern;
  char c = *text;

  switch (p) {
  case '\0': return c == '\0';
  case '%':
  case '*':
    switch (c) {
    case '\0':
      match = pni_match_r(matcher, pattern + 1, text, group + 1, 0);
      if (match) pni_sub(matcher, group, text, matched);
      return match;
    case '/':
      if (p == '%') {
        match = pni_match_r(matcher, pattern + 1, text, group + 1, 0);
        if (match) pni_sub(matcher, group, text, matched);
        return match;
      }
    // Fallthrough
    default:
      match = pni_match_r(matcher, pattern, text + 1, group, matched + 1);
      if (!match) {
        match = pni_match_r(matcher, pattern + 1, text, group + 1, 0);
        if (match) pni_sub(matcher, group, text, matched);
      }
      return match;
    }
  default:
    return c == p && pni_match_r(matcher, pattern + 1, text + 1, group, 0);
  }
}

static bool pni_match(pn_matcher_t *matcher, const char *pattern, const char *text)
{
  text = text ? text : "";
  matcher->groups = 0;
  if (pni_match_r(matcher, pattern, text, 1, 0)) {
    matcher->group[0].start = text;
    matcher->group[0].size = strlen(text);
    return true;
  } else {
    matcher->groups = 0;
    return false;
  }
}

static size_t pni_substitute(pn_matcher_t *matcher, const char *pattern, char *dest, size_t limit)
{
  size_t result = 0;

  while (*pattern) {
    switch (*pattern) {
    case '$':
      pattern++;
      if (*pattern == '$') {
        if (result < limit) {
          *dest++ = *pattern;
        }
        pattern++;
        result++;
      } else {
        size_t idx = 0;
        while (isdigit(*pattern)) {
          idx *= 10;
          idx += *pattern++ - '0';
        }

        if (idx <= matcher->groups) {
          pn_group_t *group = &matcher->group[idx];
          for (size_t i = 0; i < group->size; i++) {
            if (result < limit) {
              *dest++ = group->start[i];
            }
            result++;
          }
        }
      }
      break;
    default:
      if (result < limit) {
        *dest++ = *pattern;
      }
      pattern++;
      result++;
      break;
    }
  }

  if (result < limit) {
    *dest = '\0';
  }

  return result;
}

int pn_transform_apply(pn_transform_t *transform, const char *src,
                       pn_string_t *dst)
{
  for (size_t i = 0; i < pn_list_size(transform->rules); i++)
  {
    pn_rule_t *rule = (pn_rule_t *) pn_list_get(transform->rules, i);
    if (pni_match(&transform->matcher, pn_string_get(rule->pattern), src)) {
      transform->matched = true;
      if (!pn_string_get(rule->substitution)) {
        return pn_string_set(dst, NULL);
      }

      while (true) {
        size_t capacity = pn_string_capacity(dst);
        size_t n = pni_substitute(&transform->matcher,
                                  pn_string_get(rule->substitution),
                                  pn_string_buffer(dst), capacity);
        int err = pn_string_resize(dst, n);
        if (err) return err;
        if (n <= capacity) {
          return 0;
        }
      }
    }
  }

  transform->matched = false;
  return pn_string_set(dst, src);
}

bool pn_transform_matched(pn_transform_t *transform)
{
  return transform->matched;
}

int pn_transform_get_substitutions(pn_transform_t *transform,
                                   pn_list_t *substitutions)
{
  int size = pn_list_size(transform->rules);
  for (size_t i = 0; i < (size_t)size; i++) {
    pn_rule_t *rule = (pn_rule_t *)pn_list_get(transform->rules, i);
    pn_list_add(substitutions, rule->substitution);
  }

  return size;
}
