#include "lights.hpp"
#include "scene.hpp"


GlobalLight::GlobalLight(unsigned brightness)
    : Light(true), val(MIN((light_t)brightness,100.0)/100.0) {
    assert(val > 0.0 && val <= 1.0);
}


bool GlobalLight::reachable(const Object*) const {
    return true;
}


bool GlobalLight::occlude_box(const Object* o, vertex_t& bound_min, vertex_t& bound_max) const {
    return false;
}


light_t GlobalLight::get(std::vector<const Object*>&, const vertex_t&, const vertex_t*, const vertex_t&) const {
    return val;
}


RayLight::RayLight(unsigned brightness)
    : Light(false), intensity(MIN((light_t)brightness,100.0)/100.0) {
    assert(intensity > 0.0 && intensity <= 1.0);
}


bool RayLight::occlude_box(const Object* o, vertex_t& bound_min, vertex_t& bound_max) const {
    return false;
}


bool RayLight::reachable(const Object*) const {
    return true;
}


light_t RayLight::get(std::vector<const Object*>&, const vertex_t&, const vertex_t* nray, const vertex_t& nnrm) const {
    light_t fac = nray->dot(nnrm); // A * B = |A| * |B| * cos(A,B) - TODO: is not linear?
    if (fac < 0.0) {
        fac *= -1.0; // assume an angle between 0° and 90° is meant
    }
    return fac * intensity; // TODO: len dropoff?
}


PointLight::PointLight(const vertex_t& p, Img::rgb_t c, unsigned brightness, unsigned len)
    : Light(true, c), pos(p), intensity(MIN((light_t)brightness,100.0)/100.0), maxlen(len), maxsqlen(len*len) {
    assert(intensity > 0.0 && intensity <= 1.0);
}


bool PointLight::occlude_box(const Object* o, vertex_t& bound_min, vertex_t& bound_max) const {
    bound_min = vertex_t::minof(o->bound_min, pos);
    bound_max = vertex_t::maxof(o->bound_max, pos);
    return true;
}


bool PointLight::reachable(const Object* o) const {
    return o->sqdist(pos, maxlen, maxsqlen) < maxsqlen;
}


light_t PointLight::get(std::vector<const Object*>& objects, const vertex_t& hit, const vertex_t*, const vertex_t& nnrm) const {
    vertex_t lray = pos-hit;
    ray_t ray(hit, lray);

    coord_t lsqlen = lray.sqlen();
    if (lsqlen >= maxsqlen) { // dropoff <= 0.0
        return 0.0;
    }
    light_t dropoff;
    #ifdef PLIGHT_SQ_DROPOFF
        dropoff = 1.0 - (lsqlen / maxsqlen);
    #else
        dropoff = 1.0 - (sqrt(lsqlen) / (coord_t)maxlen);
    #endif

    lray.norm();
    const coord_t angle = nnrm.dot(lray);
    if (angle <= 0.0) { // TODO: provide max angle (and direction?) to get a spotlight
        return 0.0;
    } else {
        dropoff *= angle; // make configurable?
    }

    if (Scene::intersect(ray, objects)) {
        return 0.0; // occluded
    }

    return intensity * dropoff;
}


AreaLight::AreaLight(const vertex_t& aa, const vertex_t& bb, unsigned brightness, unsigned len):
        Light(true),
        intensity(MIN((light_t)brightness, 100.0)/100.0),
        maxlen(len), maxsqlen(pow2(len)), maxmaxlen(len+bb.len()), maxmaxsqlen(pow2(len+bb.len())),
        bound_min(vertex_t::minof(aa, aa+bb)), bound_max(vertex_t::maxof(aa, aa+bb)),
        samples(MAX(1, config.alight_samples.i)) {
    minp = bound_min;
    vertex_t dirp = bound_max - minp;

    assert(dirp.nulls() == 1);
    int a_os, b_os;
    if (dirp.v[0] == 0.0) {
        a_os = 1;
        b_os = 2;
    } else if (dirp.v[1] == 0.0) {
        a_os = 0;
        b_os = 2;
    } else {
        a_os = 0;
        b_os = 1;
    }

    stepa.v[a_os] = dirp.v[a_os]/(samples+1);
    stepb.v[b_os] = dirp.v[b_os]/(samples+1);
    starta.v[a_os] = 0.5*stepa.v[a_os];
    startb.v[b_os] = 0.5*stepb.v[b_os];
}


bool AreaLight::occlude_box(const Object* o, vertex_t& bmin, vertex_t& bmax) const {
    bmin = vertex_t::minof(o->bound_min, bound_min);
    bmax = vertex_t::maxof(o->bound_max, bound_max);
    return true;
}


bool AreaLight::reachable(const Object* o) const {
    return o->sqdist(minp, maxmaxlen, maxmaxsqlen) < maxmaxsqlen;
}


light_t AreaLight::get(std::vector<const Object*>& objects, const vertex_t& hit, const vertex_t*, const vertex_t& nnrm) const {
    if (!samples) {
        return 0.0;
    }

    const vertex_t dir = minp - hit;
    coord_t dirsqlen = dir.sqlen();
    if (dirsqlen > maxmaxsqlen) {
        return 0.0; // too far
    }

    coord_t angle = nnrm.dot(dir.normed());
    if (angle <= 0.0) {
        return 0.0;
    }

    coord_t dists = 0.0;
    coord_t angles = 0.0;
    unsigned hits = 0;
    ray_t ray(hit, vertex_t(0, 0, 0));
    vertex_t aos = starta;
    for (unsigned a=0; a<samples; ++a) {
        vertex_t bos = startb;
        for (unsigned b=0; b<samples; ++b) {
            ray.direction = dir + aos + bos;
            coord_t dist = ray.direction.sqlen();
            if (dist <= maxsqlen) {
                if (!Scene::intersect(ray, objects)) {
                    ++hits;
#if 1
                    dist = sqrt(dist); // have to norm anyhow for angle
                    ray.direction /= dist;
                    angle = fabs(nnrm.dot(ray.direction)); // 0: 90/270, 1: 0/360
                    dists += dist/maxlen;
#else
                    dists += dist/maxsqlen;
#endif
                    angles += angle;
                }
            }
            bos += stepb;
        }
        aos += stepa;
    }
    if (!hits) {
        return 0.0; // occluded
    }

    dists = 1.0 - (dists/((coord_t)hits));
    angles /= (coord_t)hits;

    return intensity *
           ((light_t)hits/(light_t)(samples*samples)) *
           dists *
           (dists + (angles*(1.0-dists))); // s.t. angles get more important when far away (are always bigger when near)
}