/*
 * Copyright 2024 Google LLC
 *
 * Licensed 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 "gtypes.h"
#include "gtransform.h"
#include "util/trig.h"

#include <string.h>

//////////////////////////////////////
/// Creating Transforms
//////////////////////////////////////
// Note that int64_t casting is required since cos/sin values are already in a 32-bit fixed point
// representation and when passed to this function. They need to be scaled by the
// Fixed_S32_16 precision (16-bits) before dividing by the TRIG_MAX_RATIO. This multiply is what
// makes the int64_t casting necessary to avoid overflowing across 32-bits.
GTransform gtransform_init_rotation(int32_t angle) {
  if (angle != 0) {
    int32_t cosine = cos_lookup(angle);
    int32_t sine = sin_lookup(angle);
    int64_t cosine_val = (cosine * ((int64_t)GTransformNumberOne.raw_value)) / TRIG_MAX_RATIO;
    int64_t sine_val = (sine * ((int64_t)GTransformNumberOne.raw_value)) / TRIG_MAX_RATIO;
    GTransformNumber a = (GTransformNumber) { .raw_value = cosine_val };
    GTransformNumber b = (GTransformNumber) { .raw_value = -sine_val };
    GTransformNumber c = (GTransformNumber) { .raw_value = sine_val };
    GTransformNumber d = (GTransformNumber) { .raw_value = cosine_val };
    return GTransform(a, b, c, d, GTransformNumberZero, GTransformNumberZero);
  } else {
    return GTransformIdentity();
  }
}

//////////////////////////////////////
/// Evaluating Transforms
//////////////////////////////////////
bool gtransform_is_identity(const GTransform * const t) {
  if (!t) {
    return false;
  }

  GTransform t_c = GTransformIdentity();

  if (memcmp(t, &t_c, sizeof(GTransform)) == 0) {
    return true;
  }

  return false;
}

bool gtransform_is_only_scale(const GTransform * const t) {
  if (!t) {
    return false;
  }

  if ((t->b.raw_value == GTransformNumberZero.raw_value) &&
      (t->c.raw_value == GTransformNumberZero.raw_value) &&
      (t->tx.raw_value == GTransformNumberZero.raw_value) &&
      (t->ty.raw_value == GTransformNumberZero.raw_value)) {
    return true;
  }

  return false;
}

bool gtransform_is_only_translation(const GTransform * const t) {
  if (!t) {
    return false;
  }

  if ((t->a.raw_value == GTransformNumberOne.raw_value) &&
      (t->b.raw_value == GTransformNumberZero.raw_value) &&
      (t->c.raw_value == GTransformNumberZero.raw_value) &&
      (t->d.raw_value == GTransformNumberOne.raw_value)) {
    return true;
  }

  return false;
}

bool gtransform_is_only_scale_or_translation(const GTransform * const t) {
  if (!t) {
    return false;
  }

  if ((t->b.raw_value != GTransformNumberZero.raw_value) ||
      (t->c.raw_value != GTransformNumberZero.raw_value)) {
    return true;
  }

  return false;
}

bool gtransform_is_equal(const GTransform * const t1, const GTransform * const t2) {
  if ((!t1) || (!t2)) {
    return false;
  }

  return memcmp(t1, t2, sizeof(GTransform)) == 0;
}

//////////////////////////////////////
/// Modifying Transforms
//////////////////////////////////////
// Note that t_new can be set to either of t1 or t2 safely to do in place muliplication
// Note this operation is not commutative. The operation is as follows t_new = t1 * t2
void gtransform_concat(GTransform *t_new, const GTransform *t1, const GTransform * t2) {
  if ((!t_new) || (!t1) || (!t2)) {
    return;
  }

  Fixed_S32_16 a_a = Fixed_S32_16_mul(t1->a, t2->a);
  Fixed_S32_16 b_c = Fixed_S32_16_mul(t1->b, t2->c);

  Fixed_S32_16 a_b = Fixed_S32_16_mul(t1->a, t2->b);
  Fixed_S32_16 b_d = Fixed_S32_16_mul(t1->b, t2->d);

  Fixed_S32_16 c_a = Fixed_S32_16_mul(t1->c, t2->a);
  Fixed_S32_16 d_c = Fixed_S32_16_mul(t1->d, t2->c);

  Fixed_S32_16 c_b = Fixed_S32_16_mul(t1->c, t2->b);
  Fixed_S32_16 d_d = Fixed_S32_16_mul(t1->d, t2->d);

  Fixed_S32_16 tx_a = Fixed_S32_16_mul(t1->tx, t2->a);
  Fixed_S32_16 ty_c = Fixed_S32_16_mul(t1->ty, t2->c);

  Fixed_S32_16 tx_b = Fixed_S32_16_mul(t1->tx, t2->b);
  Fixed_S32_16 ty_d = Fixed_S32_16_mul(t1->ty, t2->d);

  t_new->a = Fixed_S32_16_add(a_a, b_c);
  t_new->b = Fixed_S32_16_add(a_b, b_d);
  t_new->c = Fixed_S32_16_add(c_a, d_c);
  t_new->d = Fixed_S32_16_add(c_b, d_d);
  t_new->tx = Fixed_S32_16_add3(tx_a, ty_c, t2->tx);
  t_new->ty = Fixed_S32_16_add3(tx_b, ty_d, t2->ty);
}

void gtransform_scale(GTransform *t_new, GTransform *t, GTransformNumber sx, GTransformNumber sy) {
  if ((!t_new) || (!t)) {
    return;
  }

  // Copy over t to t_new and update as necessary
  if (t_new != t) {
    memcpy(t_new, t, sizeof(GTransform));
  }

  // t_new = ts*t

  // Scale X vector (a and b)
  t_new->a = Fixed_S32_16_mul(sx, t->a);
  t_new->b = Fixed_S32_16_mul(sx, t->b);

  // Scale Y vector (c and d)
  t_new->c = Fixed_S32_16_mul(sy, t->c);
  t_new->d = Fixed_S32_16_mul(sy, t->d);
}

void gtransform_translate(GTransform *t_new, GTransform *t,
                          GTransformNumber tx, GTransformNumber ty) {
  if ((!t_new) || (!t)) {
    return;
  }

  // Copy over t to t_new and update as necessary
  if (t_new != t) {
    memcpy(t_new, t, sizeof(GTransform));
  }

  // t_new = tt*t
  Fixed_S32_16 tx_a = Fixed_S32_16_mul(tx, t->a);
  Fixed_S32_16 ty_c = Fixed_S32_16_mul(ty, t->c);

  Fixed_S32_16 tx_b = Fixed_S32_16_mul(tx, t->b);
  Fixed_S32_16 ty_d = Fixed_S32_16_mul(ty, t->d);

  t_new->tx = Fixed_S32_16_add3(tx_a, ty_c, t->tx);
  t_new->ty = Fixed_S32_16_add3(tx_b, ty_d, t->ty);
}

void gtransform_rotate(GTransform *t_new, GTransform *t, int32_t angle) {
  if ((!t_new) || (!t)) {
    return;
  }

  // t_new = tr*t
  GTransform tR = gtransform_init_rotation(angle);
  gtransform_concat(t_new, &tR, t);
}

bool gtransform_invert(GTransform *t_new, GTransform *t) {
  if ((!t_new) || (!t)) {
    return false;
  }

  memcpy(t_new, t, sizeof(GTransform));
  // FIXME: NYI - copy original into t_new for now
  return false;
}

//////////////////////////////////////
/// Applying Transformations
//////////////////////////////////////
GPointPrecise gpoint_transform(GPoint point, const GTransform * const t) {
  GPointPrecise pointP = GPointPreciseFromGPoint(point);

  if (!t) {
    return pointP;
  }

  Fixed_S16_3 x_a = Fixed_S16_3_S32_16_mul(pointP.x, t->a);
  Fixed_S16_3 y_c = Fixed_S16_3_S32_16_mul(pointP.y, t->c);
  Fixed_S16_3 one_tx = Fixed_S16_3_S32_16_mul(FIXED_S16_3_ONE, t->tx);

  Fixed_S16_3 x_b = Fixed_S16_3_S32_16_mul(pointP.x, t->b);
  Fixed_S16_3 y_d = Fixed_S16_3_S32_16_mul(pointP.y, t->d);
  Fixed_S16_3 one_ty = Fixed_S16_3_S32_16_mul(FIXED_S16_3_ONE, t->ty);

  Fixed_S16_3 sum_x = Fixed_S16_3_add3(x_a, y_c, one_tx);
  Fixed_S16_3 sum_y = Fixed_S16_3_add3(x_b, y_d, one_ty);

  return GPointPrecise(sum_x.raw_value, sum_y.raw_value);
}

GVectorPrecise gvector_transform(GVector vector, const GTransform * const t) {
  GVectorPrecise vectorP = GVectorPreciseFromGVector(vector);

  if (!t) {
    return vectorP;
  }

  Fixed_S16_3 x_a = Fixed_S16_3_S32_16_mul(vectorP.dx, t->a);
  Fixed_S16_3 y_c = Fixed_S16_3_S32_16_mul(vectorP.dy, t->c);
  Fixed_S16_3 one_tx = Fixed_S16_3_S32_16_mul(FIXED_S16_3_ONE, t->tx);

  Fixed_S16_3 x_b = Fixed_S16_3_S32_16_mul(vectorP.dx, t->b);
  Fixed_S16_3 y_d = Fixed_S16_3_S32_16_mul(vectorP.dy, t->d);
  Fixed_S16_3 one_ty = Fixed_S16_3_S32_16_mul(FIXED_S16_3_ONE, t->ty);

  Fixed_S16_3 sum_x = Fixed_S16_3_add3(x_a, y_c, one_tx);
  Fixed_S16_3 sum_y = Fixed_S16_3_add3(x_b, y_d, one_ty);

  return GVectorPrecise(sum_x.raw_value, sum_y.raw_value);
}
