/*
 * Copyright (C) 2016 Google, Inc.
 *
 * 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 <cassert>
#include <cmath>
#include <array>
#include <glm/gtc/matrix_transform.hpp>
#include "Simulation.h"

namespace {

class MeshPicker {
   public:
    MeshPicker()
        : pattern_({{
              Meshes::MESH_PYRAMID,
              Meshes::MESH_ICOSPHERE,
              Meshes::MESH_TEAPOT,
              Meshes::MESH_PYRAMID,
              Meshes::MESH_ICOSPHERE,
              Meshes::MESH_PYRAMID,
              Meshes::MESH_PYRAMID,
              Meshes::MESH_PYRAMID,
              Meshes::MESH_PYRAMID,
              Meshes::MESH_PYRAMID,
          }}),
          cur_(-1) {}

    Meshes::Type pick() {
        cur_ = (cur_ + 1) % pattern_.size();
        return pattern_[cur_];
    }

    float scale(Meshes::Type type) const {
        float base = 0.005f;

        switch (type) {
            case Meshes::MESH_PYRAMID:
            default:
                return base * 1.0f;
            case Meshes::MESH_ICOSPHERE:
                return base * 3.0f;
            case Meshes::MESH_TEAPOT:
                return base * 10.0f;
        }
    }

   private:
    const std::array<Meshes::Type, 10> pattern_;
    int cur_;
};

class ColorPicker {
   public:
    ColorPicker(unsigned int rng_seed) : rng_(rng_seed), red_(0.0f, 1.0f), green_(0.0f, 1.0f), blue_(0.0f, 1.0f) {}

    glm::vec3 pick() { return glm::vec3{red_(rng_), green_(rng_), blue_(rng_)}; }

   private:
    std::mt19937 rng_;
    std::uniform_real_distribution<float> red_;
    std::uniform_real_distribution<float> green_;
    std::uniform_real_distribution<float> blue_;
};

}  // namespace

Animation::Animation(unsigned int rng_seed, float scale) : rng_(rng_seed), dir_(-1.0f, 1.0f), speed_(0.1f, 1.0f) {
    float x = dir_(rng_);
    float y = dir_(rng_);
    float z = dir_(rng_);
    if (std::abs(x) + std::abs(y) + std::abs(z) == 0.0f) x = 1.0f;

    current_.axis = glm::normalize(glm::vec3(x, y, z));

    current_.speed = speed_(rng_);
    current_.scale = scale;

    current_.matrix = glm::scale(glm::mat4(1.0f), glm::vec3(current_.scale));
}

glm::mat4 Animation::transformation(float t) {
    current_.matrix = glm::rotate(current_.matrix, current_.speed * t, current_.axis);

    return current_.matrix;
}

class Curve {
   public:
    virtual ~Curve() {}
    virtual glm::vec3 evaluate(float t) = 0;
};

namespace {

enum CurveType {
    CURVE_RANDOM,
    CURVE_CIRCLE,
    CURVE_COUNT,
};

class RandomCurve : public Curve {
   public:
    RandomCurve(unsigned int rng_seed)
        : rng_(rng_seed),
          direction_(-0.3f, 0.3f),
          duration_(1.0f, 5.0f),
          segment_start_(0.0f),
          segment_direction_(0.0f),
          time_start_(0.0f),
          time_duration_(0.0f) {}

    glm::vec3 evaluate(float t) {
        if (t >= time_start_ + time_duration_) new_segment(t);

        pos_ += unit_dir_ * (t - last_);
        last_ = t;

        return pos_;
    }

   private:
    void new_segment(float time_start) {
        segment_start_ += segment_direction_;
        segment_direction_ = glm::vec3(direction_(rng_), direction_(rng_), direction_(rng_));

        time_start_ = time_start;
        time_duration_ = duration_(rng_);

        unit_dir_ = segment_direction_ / time_duration_;
        pos_ = segment_start_;
        last_ = time_start_;
    }

    std::mt19937 rng_;
    std::uniform_real_distribution<float> direction_;
    std::uniform_real_distribution<float> duration_;

    glm::vec3 segment_start_;
    glm::vec3 segment_direction_;
    float time_start_;
    float time_duration_;

    glm::vec3 unit_dir_;
    glm::vec3 pos_;
    float last_;
};

class CircleCurve : public Curve {
   public:
    CircleCurve(float radius, glm::vec3 axis) : r_(radius) {
        glm::vec3 a;

        if (axis.x != 0.0f) {
            a.x = -axis.z / axis.x;
            a.y = 0.0f;
            a.z = 1.0f;
        } else if (axis.y != 0.0f) {
            a.x = 1.0f;
            a.y = -axis.x / axis.y;
            a.z = 0.0f;
        } else {
            a.x = 1.0f;
            a.y = 0.0f;
            a.z = -axis.x / axis.z;
        }

        a_ = glm::normalize(a);
        b_ = glm::normalize(glm::cross(a_, axis));
    }

    glm::vec3 evaluate(float t) {
        return (a_ * (glm::vec3(std::cos(t)) - glm::vec3(1.0f)) + b_ * glm::vec3(std::sin(t))) * glm::vec3(r_);
    }

   private:
    float r_;
    glm::vec3 a_;
    glm::vec3 b_;
};

}  // namespace

Path::Path(unsigned int rng_seed) : rng_(rng_seed), type_(0, CURVE_COUNT - 1), duration_(5.0f, 20.0f) {
    // trigger a subpath generation
    current_.end = -1.0f;
    current_.now = 0.0f;
}

glm::vec3 Path::position(float t) {
    current_.now += t;

    while (current_.now >= current_.end) generate_subpath();

    return current_.origin + current_.curve->evaluate(current_.now - current_.start);
}

void Path::generate_subpath() {
    float duration = duration_(rng_);
    CurveType type = static_cast<CurveType>(type_(rng_));

    if (current_.curve) {
        current_.origin += current_.curve->evaluate(current_.end - current_.start);
        current_.origin = glm::mod(current_.origin, glm::vec3(2.0f));
        current_.start = current_.end;
    } else {
        std::uniform_real_distribution<float> origin(0.0f, 2.0f);
        current_.origin = glm::vec3(origin(rng_), origin(rng_), origin(rng_));
        current_.start = current_.now;
    }

    current_.end = current_.start + duration;

    Curve *curve;

    switch (type) {
        case CURVE_RANDOM:
            curve = new RandomCurve(rng_());
            break;
        case CURVE_CIRCLE: {
            std::uniform_real_distribution<float> dir(-1.0f, 1.0f);
            glm::vec3 axis(dir(rng_), dir(rng_), dir(rng_));
            if (axis.x == 0.0f && axis.y == 0.0f && axis.z == 0.0f) axis.x = 1.0f;

            std::uniform_real_distribution<float> radius_(0.02f, 0.2f);
            curve = new CircleCurve(radius_(rng_), axis);
        } break;
        default:
            assert(!"unreachable");
            curve = nullptr;
            break;
    }

    current_.curve.reset(curve);
}

Simulation::Simulation(int object_count) : random_dev_() {
    MeshPicker mesh;
    ColorPicker color(random_dev_());

    objects_.reserve(object_count);
    for (int i = 0; i < object_count; i++) {
        Meshes::Type type = mesh.pick();
        float scale = mesh.scale(type);

        objects_.emplace_back(Object{
            type,
            glm::vec3(0.5f + 0.5f * (float)i / object_count),
            color.pick(),
            Animation(random_dev_(), scale),
            Path(random_dev_()),
        });
    }
}

void Simulation::set_frame_data_size(uint32_t size) {
    uint32_t offset = 0;
    for (auto &obj : objects_) {
        obj.frame_data_offset = offset;
        offset += size;
    }
}

void Simulation::update(float time, int begin, int end) {
    for (int i = begin; i < end; i++) {
        auto &obj = objects_[i];

        glm::vec3 pos = obj.path.position(time);
        glm::mat4 trans = obj.animation.transformation(time);
        obj.model = glm::translate(glm::mat4(1.0f), pos) * trans;
    }
}
