#include <stdio.h>

#include "directxmath/directxmath.h"

#include "collada/types.h"
#include "collada/instance_types.h"

namespace collada::animate {

  struct frame_ix {
    int f0;
    int f1;
  };

  static inline frame_ix find_frame_ix(types::source const& source, float t)
  {
    for (int i = 0; i < source.count - 1; i++) {
      if (source.float_array[i] <= t && source.float_array[i+1] > t) {
        return {i, i + 1};
      }
    }
    return {source.count - 1, 0};
  }

  static inline float linear_interpolate_iv(types::source const& source, frame_ix frame_ix, float t)
  {
    float prev = source.float_array[(frame_ix.f0) * source.stride];
    float next = source.float_array[(frame_ix.f1) * source.stride];
    return (t - prev) / (next - prev);
  }

  static inline float linear_interpolate_value(types::source const& source, frame_ix frame_ix, int parameter_ix, float iv)
  {
    float prev = source.float_array[(frame_ix.f0) * source.stride + parameter_ix];
    float next = source.float_array[(frame_ix.f1) * source.stride + parameter_ix];
    return prev + iv * (next - prev);
  }

  static inline XMMATRIX linear_interpolate_matrix(types::source const& source, frame_ix frame_ix, float iv)
  {
    XMFLOAT4X4 const * prev = (XMFLOAT4X4 const *)&source.float_array[(frame_ix.f0) * source.stride];
    XMFLOAT4X4 const * next = (XMFLOAT4X4 const *)&source.float_array[(frame_ix.f1) * source.stride];

    XMVECTOR prev_scale;
    XMVECTOR prev_rotate;
    XMVECTOR prev_translate;

    bool prev_srt = XMMatrixDecompose(&prev_scale, &prev_rotate, &prev_translate, XMMatrixTranspose(XMLoadFloat4x4(prev)));
    assert(prev_srt == true);

    XMVECTOR next_scale;
    XMVECTOR next_rotate;
    XMVECTOR next_translate;

    bool next_srt = XMMatrixDecompose(&next_scale, &next_rotate, &next_translate, XMMatrixTranspose(XMLoadFloat4x4(next)));
    assert(next_srt == true);

    XMVECTOR scale = XMVectorLerp(prev_scale, next_scale, iv);
    XMVECTOR rotate = XMQuaternionSlerp(prev_rotate, next_rotate, iv);
    XMVECTOR translate = XMVectorLerp(prev_translate, next_translate, iv);

    return XMMatrixAffineTransformation(scale,
                                        XMVectorZero(),
                                        rotate,
                                        translate);
  }

  static inline float pow3(float f)
  {
    return f * f * f;
  }

  static inline float pow2(float f)
  {
    return f * f;
  }

  static inline XMVECTOR bezier(XMVECTOR p0, XMVECTOR c0, XMVECTOR c1, XMVECTOR p1, float s)
  {
    return
        p0 * pow3(1 - s)
      + 3 * c0 * s * pow2(1 - s)
      + 3 * c1 * pow2(s) * (1 - s)
      + p1 * pow3(s);
  }

  static inline float bezier_binary_search(XMVECTOR p0, XMVECTOR c0, XMVECTOR c1, XMVECTOR p1, float want)
  {
    float low = 0.0f;
    float high = 1.0f;

    int iterations = 0;
    while (iterations < 20) {
      iterations += 1;

      float s = (high + low) * 0.5f;
      XMVECTOR bs = bezier(p0, c0, c1, p1, s);
      float t = XMVectorGetX(bs);

      const float epsilon = 0.001f;
      if (fabsf(t - want) < epsilon) {
        return XMVectorGetY(bs);
      }

      if (t > want) {
        high = s;
      } else {
        low = s;
      }
    }

    fprintf(stderr, "%f %f\n", XMVectorGetX(p0), XMVectorGetY(p0));
    fprintf(stderr, "%f %f\n", XMVectorGetX(c0), XMVectorGetY(c0));
    fprintf(stderr, "%f %f\n", XMVectorGetX(c1), XMVectorGetY(c1));
    fprintf(stderr, "%f %f\n", XMVectorGetX(p1), XMVectorGetY(p1));
    assert(false);
  }

  static inline XMFLOAT2 const * tangent_index(types::source const& source, int frame_ix, int parameter_ix)
  {
    int ix = frame_ix * source.stride + parameter_ix * 2;
    return (XMFLOAT2 const *)&source.float_array[ix];
  }

  static float bezier_sampler(types::sampler const * const sampler, frame_ix frame_ix, int parameter_ix, float t)
  {
    /*
      P0 is (INPUT[i] , OUTPUT[i])
      C0 (or T0) is (OUT_TANGENT[i][0] , OUT_TANGENT[i][1])
      C1 (or T1) is (IN_TANGENT[i+1][0], IN_TANGENT[i+1][1])
      P1 is (INPUT[i+1], OUTPUT[i+1])
    */

    float frame0_input = sampler->input.float_array[frame_ix.f0];
    float frame1_input = sampler->input.float_array[frame_ix.f1];

    float frame0_output = sampler->output.float_array[(frame_ix.f0) * sampler->output.stride + parameter_ix];
    float frame1_output = sampler->output.float_array[(frame_ix.f1) * sampler->output.stride + parameter_ix];

    XMVECTOR p0 = XMVectorSet(frame0_input, frame0_output, 0, 0);
    XMVECTOR c0 = XMLoadFloat2(tangent_index(sampler->out_tangent, frame_ix.f0, parameter_ix));
    XMVECTOR c1 = XMLoadFloat2(tangent_index(sampler->in_tangent, frame_ix.f1, parameter_ix));
    XMVECTOR p1 = XMVectorSet(frame1_input, frame1_output, 0, 0);

    return bezier_binary_search(p0, c0, c1, p1, t);
  }

  static void apply_transform_target(instance_types::transform& transform,
                                     enum types::target_attribute channel_target_attribute,
                                     float value)
  {
    switch (transform.type) {
    case types::transform_type::TRANSLATE: [[fallthrough]];
    case types::transform_type::SCALE:
      switch (channel_target_attribute) {
      case types::target_attribute::X: transform.vector = XMVectorSetX(transform.vector, value); return;
      case types::target_attribute::Y: transform.vector = XMVectorSetY(transform.vector, value); return;
      case types::target_attribute::Z: transform.vector = XMVectorSetZ(transform.vector, value); return;
      default: assert(false);
      }
    case types::transform_type::ROTATE:
      switch (channel_target_attribute) {
      case types::target_attribute::X: transform.vector = XMVectorSetX(transform.vector, value); return;
      case types::target_attribute::Y: transform.vector = XMVectorSetY(transform.vector, value); return;
      case types::target_attribute::Z: transform.vector = XMVectorSetZ(transform.vector, value); return;
      case types::target_attribute::ANGLE: transform.vector = XMVectorSetW(transform.vector, value); return;
      default: assert(false);
      }
    default:
      assert(false);
      break;
    }
  }

  static enum types::target_attribute const rotate_target_attributes[] = {
    types::target_attribute::X,
    types::target_attribute::Y,
    types::target_attribute::Z,
    types::target_attribute::ANGLE,
  };

  static enum types::target_attribute const translate_scale_target_attributes[] = {
    types::target_attribute::X,
    types::target_attribute::Y,
    types::target_attribute::Z,
  };

  static void animate_channel_segment(types::channel const& channel,
                                      instance_types::transform& transform,
                                      frame_ix frame_ix, float t)
  {
    enum types::target_attribute const * target_attributes = &channel.target_attribute;
    int target_attributes_count = 1;
    if (channel.target_attribute == types::target_attribute::ALL) {
      switch (transform.type) {
      case types::transform_type::TRANSLATE: [[fallthrough]];
      case types::transform_type::SCALE:
        target_attributes = translate_scale_target_attributes;
        target_attributes_count = 3;
        break;
      case types::transform_type::ROTATE:
        target_attributes = rotate_target_attributes;
        target_attributes_count = 4;
        break;
      case types::transform_type::MATRIX:
        break;
      default:
        assert(false);
        break;
      }
    }

    assert(channel.source_sampler->interpolation.stride == 1);

    if (transform.type == types::transform_type::MATRIX) {
      enum types::interpolation interpolation = channel.source_sampler->interpolation.interpolation_array[frame_ix.f0];
      assert(interpolation == types::interpolation::LINEAR);

      float iv = linear_interpolate_iv(channel.source_sampler->input, frame_ix, t);
      XMMATRIX matrix = linear_interpolate_matrix(channel.source_sampler->output, frame_ix, iv);
      transform.matrix = matrix;

    } else {
      for (int parameter_ix = 0; parameter_ix < target_attributes_count; parameter_ix++) {

        enum types::interpolation interpolation = channel.source_sampler->interpolation.interpolation_array[frame_ix.f0];

        float value;
        if (interpolation == types::interpolation::BEZIER) {
          value = bezier_sampler(channel.source_sampler, frame_ix, parameter_ix, t);
        } else {
          float iv = linear_interpolate_iv(channel.source_sampler->input, frame_ix, t);
          value = linear_interpolate_value(channel.source_sampler->output, frame_ix, parameter_ix, iv);
        }

        apply_transform_target(transform, target_attributes[parameter_ix], value);
      }
    }
  }

  void animate_node(instance_types::node& node_instance, float t)
  {
    for (int i = 0; i < node_instance.node->channels_count; i++) {
      types::channel const& channel = *node_instance.node->channels[i];
      instance_types::transform& transform = node_instance.transforms[channel.target_transform_index];

      frame_ix frame_ix = find_frame_ix(channel.source_sampler->input, t);
      //assert(frame_ix >= 0); // animation is missing a key frame

      animate_channel_segment(channel, transform, frame_ix, t);
    }
  }
}
