package org.sunflow.core.photonmap;

import java.util.ArrayList;

import org.sunflow.core.CausticPhotonMapInterface;
import org.sunflow.core.LightSample;
import org.sunflow.core.Options;
import org.sunflow.core.Ray;
import org.sunflow.core.ShadingState;
import org.sunflow.image.Color;
import org.sunflow.math.BoundingBox;
import org.sunflow.math.Point3;
import org.sunflow.math.Vector3;
import org.sunflow.system.Timer;
import org.sunflow.system.UI;
import org.sunflow.system.UI.Module;

public final class CausticPhotonMap implements CausticPhotonMapInterface {
    private ArrayList<Photon> photonList;
    private Photon[] photons;
    private int storedPhotons;
    private int halfStoredPhotons;
    private int log2n;
    private int gatherNum;
    private float gatherRadius;
    private BoundingBox bounds;
    private float filterValue;
    private float maxPower;
    private float maxRadius;
    private int numEmit;

    public void prepare(Options options, BoundingBox sceneBounds) {
        // get options
        numEmit = options.getInt("caustics.emit", 10000);
        gatherNum = options.getInt("caustics.gather", 50);
        gatherRadius = options.getFloat("caustics.radius", 0.5f);
        filterValue = options.getFloat("caustics.filter", 1.1f);
        // init
        bounds = new BoundingBox();
        maxPower = 0;
        maxRadius = 0;
        photonList = new ArrayList<Photon>();
        photonList.add(null);
        photons = null;
        storedPhotons = halfStoredPhotons = 0;
    }

    private void locatePhotons(NearestPhotons np) {
        float[] dist1d2 = new float[log2n];
        int[] chosen = new int[log2n];
        int i = 1;
        int level = 0;
        int cameFrom;
        while (true) {
            while (i < halfStoredPhotons) {
                float dist1d = photons[i].getDist1(np.px, np.py, np.pz);
                dist1d2[level] = dist1d * dist1d;
                i += i;
                if (dist1d > 0.0f)
                    i++;
                chosen[level++] = i;
            }
            np.checkAddNearest(photons[i]);
            do {
                cameFrom = i;
                i >>= 1;
                level--;
                if (i == 0)
                    return;
            } while ((dist1d2[level] >= np.dist2[0]) || (cameFrom != chosen[level]));
            np.checkAddNearest(photons[i]);
            i = chosen[level++] ^ 1;
        }
    }

    private void balance() {
        if (storedPhotons == 0)
            return;
        photons = photonList.toArray(new Photon[photonList.size()]);
        photonList = null;
        Photon[] temp = new Photon[storedPhotons + 1];
        balanceSegment(temp, 1, 1, storedPhotons);
        photons = temp;
        halfStoredPhotons = storedPhotons / 2;
        log2n = (int) Math.ceil(Math.log(storedPhotons) / Math.log(2.0));
    }

    private void balanceSegment(Photon[] temp, int index, int start, int end) {
        int median = 1;
        while ((4 * median) <= (end - start + 1))
            median += median;
        if ((3 * median) <= (end - start + 1)) {
            median += median;
            median += (start - 1);
        } else
            median = end - median + 1;
        int axis = Photon.SPLIT_Z;
        Vector3 extents = bounds.getExtents();
        if ((extents.x > extents.y) && (extents.x > extents.z))
            axis = Photon.SPLIT_X;
        else if (extents.y > extents.z)
            axis = Photon.SPLIT_Y;
        int left = start;
        int right = end;
        while (right > left) {
            double v = photons[right].getCoord(axis);
            int i = left - 1;
            int j = right;
            while (true) {
                while (photons[++i].getCoord(axis) < v) {
                }
                while ((photons[--j].getCoord(axis) > v) && (j > left)) {
                }
                if (i >= j)
                    break;
                swap(i, j);
            }
            swap(i, right);
            if (i >= median)
                right = i - 1;
            if (i <= median)
                left = i + 1;
        }
        temp[index] = photons[median];
        temp[index].setSplitAxis(axis);
        if (median > start) {
            if (start < (median - 1)) {
                float tmp;
                switch (axis) {
                    case Photon.SPLIT_X:
                        tmp = bounds.getMaximum().x;
                        bounds.getMaximum().x = temp[index].x;
                        balanceSegment(temp, 2 * index, start, median - 1);
                        bounds.getMaximum().x = tmp;
                        break;
                    case Photon.SPLIT_Y:
                        tmp = bounds.getMaximum().y;
                        bounds.getMaximum().y = temp[index].y;
                        balanceSegment(temp, 2 * index, start, median - 1);
                        bounds.getMaximum().y = tmp;
                        break;
                    default:
                        tmp = bounds.getMaximum().z;
                        bounds.getMaximum().z = temp[index].z;
                        balanceSegment(temp, 2 * index, start, median - 1);
                        bounds.getMaximum().z = tmp;
                }
            } else
                temp[2 * index] = photons[start];
        }
        if (median < end) {
            if ((median + 1) < end) {
                float tmp;
                switch (axis) {
                    case Photon.SPLIT_X:
                        tmp = bounds.getMinimum().x;
                        bounds.getMinimum().x = temp[index].x;
                        balanceSegment(temp, (2 * index) + 1, median + 1, end);
                        bounds.getMinimum().x = tmp;
                        break;
                    case Photon.SPLIT_Y:
                        tmp = bounds.getMinimum().y;
                        bounds.getMinimum().y = temp[index].y;
                        balanceSegment(temp, (2 * index) + 1, median + 1, end);
                        bounds.getMinimum().y = tmp;
                        break;
                    default:
                        tmp = bounds.getMinimum().z;
                        bounds.getMinimum().z = temp[index].z;
                        balanceSegment(temp, (2 * index) + 1, median + 1, end);
                        bounds.getMinimum().z = tmp;
                }
            } else
                temp[(2 * index) + 1] = photons[end];
        }
    }

    private void swap(int i, int j) {
        Photon tmp = photons[i];
        photons[i] = photons[j];
        photons[j] = tmp;
    }

    public void store(ShadingState state, Vector3 dir, Color power, Color diffuse) {
        if (((state.getDiffuseDepth() == 0) && (state.getReflectionDepth() > 0 || state.getRefractionDepth() > 0))) {
            // this is a caustic photon
            Photon p = new Photon(state.getPoint(), dir, power);
            synchronized (this) {
                storedPhotons++;
                photonList.add(p);
                bounds.include(new Point3(p.x, p.y, p.z));
                maxPower = Math.max(maxPower, power.getMax());
            }
        }
    }

    public void init() {
        UI.printInfo(Module.LIGHT, "Balancing caustics photon map ...");
        Timer t = new Timer();
        t.start();
        balance();
        t.end();
        UI.printInfo(Module.LIGHT, "Caustic photon map:");
        UI.printInfo(Module.LIGHT, "  * Photons stored:   %d", storedPhotons);
        UI.printInfo(Module.LIGHT, "  * Photons/estimate: %d", gatherNum);
        maxRadius = 1.4f * (float) Math.sqrt(maxPower * gatherNum);
        UI.printInfo(Module.LIGHT, "  * Estimate radius:  %.3f", gatherRadius);
        UI.printInfo(Module.LIGHT, "  * Maximum radius:   %.3f", maxRadius);
        UI.printInfo(Module.LIGHT, "  * Balancing time:   %s", t.toString());
        if (gatherRadius > maxRadius)
            gatherRadius = maxRadius;
    }

    public void getSamples(ShadingState state) {
        if (storedPhotons == 0)
            return;
        NearestPhotons np = new NearestPhotons(state.getPoint(), gatherNum, gatherRadius * gatherRadius);
        locatePhotons(np);
        if (np.found < 8)
            return;
        Point3 ppos = new Point3();
        Vector3 pdir = new Vector3();
        Vector3 pvec = new Vector3();
        float invArea = 1.0f / ((float) Math.PI * np.dist2[0]);
        float maxNDist = np.dist2[0] * 0.05f;
        float f2r2 = 1.0f / (filterValue * filterValue * np.dist2[0]);
        float fInv = 1.0f / (1.0f - 2.0f / (3.0f * filterValue));
        for (int i = 1; i <= np.found; i++) {
            Photon phot = np.index[i];
            Vector3.decode(phot.dir, pdir);
            float cos = -Vector3.dot(pdir, state.getNormal());
            if (cos > 0.001) {
                ppos.set(phot.x, phot.y, phot.z);
                Point3.sub(ppos, state.getPoint(), pvec);
                float pcos = Vector3.dot(pvec, state.getNormal());
                if ((pcos < maxNDist) && (pcos > -maxNDist)) {
                    LightSample sample = new LightSample();
                    sample.setShadowRay(new Ray(state.getPoint(), pdir.negate()));
                    sample.setRadiance(new Color().setRGBE(np.index[i].power).mul(invArea / cos), Color.BLACK);
                    sample.getDiffuseRadiance().mul((1.0f - (float) Math.sqrt(np.dist2[i] * f2r2)) * fInv);
                    state.addSample(sample);
                }
            }
        }
    }

    private static class NearestPhotons {
        int found;
        float px, py, pz;
        private int max;
        private boolean gotHeap;
        protected float[] dist2;
        protected Photon[] index;

        NearestPhotons(Point3 p, int n, float maxDist2) {
            max = n;
            found = 0;
            gotHeap = false;
            px = p.x;
            py = p.y;
            pz = p.z;
            dist2 = new float[n + 1];
            index = new Photon[n + 1];
            dist2[0] = maxDist2;
        }

        void reset(Point3 p, float maxDist2) {
            found = 0;
            gotHeap = false;
            px = p.x;
            py = p.y;
            pz = p.z;
            dist2[0] = maxDist2;
        }

        void checkAddNearest(Photon p) {
            float fdist2 = p.getDist2(px, py, pz);
            if (fdist2 < dist2[0]) {
                if (found < max) {
                    found++;
                    dist2[found] = fdist2;
                    index[found] = p;
                } else {
                    int j;
                    int parent;
                    if (!gotHeap) {
                        float dst2;
                        Photon phot;
                        int halfFound = found >> 1;
                        for (int k = halfFound; k >= 1; k--) {
                            parent = k;
                            phot = index[k];
                            dst2 = dist2[k];
                            while (parent <= halfFound) {
                                j = parent + parent;
                                if ((j < found) && (dist2[j] < dist2[j + 1]))
                                    j++;
                                if (dst2 >= dist2[j])
                                    break;
                                dist2[parent] = dist2[j];
                                index[parent] = index[j];
                                parent = j;
                            }
                            dist2[parent] = dst2;
                            index[parent] = phot;
                        }
                        gotHeap = true;
                    }
                    parent = 1;
                    j = 2;
                    while (j <= found) {
                        if ((j < found) && (dist2[j] < dist2[j + 1]))
                            j++;
                        if (fdist2 > dist2[j])
                            break;
                        dist2[parent] = dist2[j];
                        index[parent] = index[j];
                        parent = j;
                        j += j;
                    }
                    dist2[parent] = fdist2;
                    index[parent] = p;
                    dist2[0] = dist2[1];
                }
            }
        }
    }

    private static class Photon {
        float x;
        float y;
        float z;
        short dir;
        int power;
        int flags;

        static final int SPLIT_X = 0;
        static final int SPLIT_Y = 1;
        static final int SPLIT_Z = 2;
        static final int SPLIT_MASK = 3;

        Photon(Point3 p, Vector3 dir, Color power) {
            x = p.x;
            y = p.y;
            z = p.z;
            this.dir = dir.encode();
            this.power = power.toRGBE();
            flags = SPLIT_X;
        }

        void setSplitAxis(int axis) {
            flags &= ~SPLIT_MASK;
            flags |= axis;
        }

        float getCoord(int axis) {
            switch (axis) {
                case SPLIT_X:
                    return x;
                case SPLIT_Y:
                    return y;
                default:
                    return z;
            }
        }

        float getDist1(float px, float py, float pz) {
            switch (flags & SPLIT_MASK) {
                case SPLIT_X:
                    return px - x;
                case SPLIT_Y:
                    return py - y;
                default:
                    return pz - z;
            }
        }

        float getDist2(float px, float py, float pz) {
            float dx = x - px;
            float dy = y - py;
            float dz = z - pz;
            return (dx * dx) + (dy * dy) + (dz * dz);
        }
    }

    public boolean allowDiffuseBounced() {
        return false;
    }

    public boolean allowReflectionBounced() {
        return true;
    }

    public boolean allowRefractionBounced() {
        return true;
    }

    public int numEmit() {
        return numEmit;
    }
}