package com.srbenoit.render;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.Stroke;
import java.awt.geom.Line2D;
import java.awt.geom.Path2D;
import java.awt.image.BufferedImage;
import com.srbenoit.geom.BasedVector3;
import com.srbenoit.geom.Vector3;
import com.srbenoit.log.LoggedObject;

/**
 * A render pipeline that can take a <code>Scene</code>, a <code>Camera</code>, and a <code>
 * BufferedImage</code> and generate a rendered view of the scene in the image.
 */
public class RenderPipeline extends LoggedObject {

    /**
     * Constructs a new <code>RenderPipeline</code>.
     */
    public RenderPipeline() {

        super();
    }

    /**
     * Renders the scene.
     *
     * @param  scene   the scene to render
     * @param  camera  the camera to use to render the scene
     * @param  image   the <code>BufferedImage</code> to which to render
     */
    public void render(final Scene scene, final Camera camera, final BufferedImage image) {

        double aspect;

        // Transform world objects into view space (atomic operation, locks the scene) and
        // build a self-consistent scene in view space so world objects can be modified without
        // impacting the render process.  This computes the normal vectors at each view vertex.
        scene.worldToView(camera);

        // Do back-face culling and lighting
        cullBackfaces(scene);
        lightFaces(scene);

        // We now transform into normalized device coordinates, based on the aspect ratio of the
        // target image.  Normal vectors are not transformed here.
        aspect = (double) image.getWidth() / (double) image.getHeight();
        camera.setAspect(aspect);
        scene.viewToNormalized(camera);

        // TODO: clip faces to the view frustum and near/far clip planes

        scene.normalizedToScreen(camera, image.getWidth(), image.getHeight());

        rasterize(scene, image);
    }

    /**
     * Test faces to see whether they are back-facing, and cull those that are.
     *
     * @param  scene  the scene
     */
    private void cullBackfaces(final Scene scene) {

        ViewFaceIterator iter;
        ViewFace face;
        ViewVertex vert;
        double dot;
        Vector3 vec;

        vec = new Vector3();

        iter = new ViewFaceIterator(scene);

        while (iter.hasNext()) {
            face = iter.next();
            vert = face.getVertex0();
            vec.setVec(vert.getPosX(), vert.getPosY(), vert.getPosZ());
            dot = vec.dot(face);
            face.setCulled(dot > 0);
        }
    }

    /**
     * Computes the light values at each face based on the face normal and the vectors to all light
     * sources.
     *
     * @param  scene  the scene
     */
    private void lightFaces(final Scene scene) {

        ViewFaceIterator iter;
        ViewFace face;
        ViewVertex vert;
        int numLights;
        Light light;
        Color col;
        double red;
        double grn;
        double blu;
        Vector3 vecToLight;
        double dot;

        iter = new ViewFaceIterator(scene);
        numLights = scene.numLights();
        vecToLight = new Vector3();

        if (numLights > 0) {

            while (iter.hasNext()) {
                face = iter.next();

                if (face.isCulled()) {
                    continue;
                }

                vert = face.getVertex0();

                red = 0.3f;
                grn = 0.3f;
                blu = 0.3f;

                for (int i = 0; i < numLights; i++) {
                    light = scene.getViewLight(i);
                    col = light.getColor();
                    vecToLight.vectorBetween(vert, light);
                    vecToLight.normalize();
                    dot = vecToLight.dot(face);
                    red += dot * col.getRed() / 255.0;
                    grn += dot * col.getGreen() / 255.0;
                    blu += dot * col.getBlue() / 255.0;
                }

                if (red < 0.1) {
                    red = 0.1;
                }

                if (grn < 0.1) {
                    grn = 0.1;
                }

                if (blu < 0.1) {
                    blu = 0.1;
                }

                face.setColor(red, grn, blu);
            }
        }
    }

    /**
     * Rasterizes the scene onto the image.
     *
     * @param  scene  the scene to render
     * @param  image  the image onto which to draw the scene
     */
    private void rasterize(final Scene scene, final BufferedImage image) {

        Graphics2D grx;
        int count;
        BasedVector3 vec;
        ViewFaceIterator iter;
        ViewVertex vert0;
        ViewVertex vert1;
        ViewVertex vert2;
        ViewFace face;
        Path2D path;
        Line2D line;
        Stroke orig;

        // Now we rasterize (for now, just a wireframe)
        grx = (Graphics2D) image.getGraphics();
        grx.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        grx.setColor(Color.BLACK);
        grx.fillRect(0, 0, image.getWidth(), image.getHeight());

        // Draw all the based vectors
        grx.setColor(Color.RED);
        count = scene.numBasedVectors();

        orig = grx.getStroke();
        grx.setStroke(new BasicStroke(3));

        for (int i = 0; i < count; i++) {
            vec = scene.getBasedVector(i);

            line = new Line2D.Double(vec.getPosX(), vec.getPosY(), vec.getVecX(), vec.getVecY());
            grx.draw(line);
        }

        grx.setStroke(orig);

        iter = new ViewFaceIterator(scene);

        while (iter.hasNext()) {
            face = iter.next();

            if (face.isCulled()) {
                continue;
            }

            vert0 = face.getVertex0();
            vert1 = face.getVertex1();
            vert2 = face.getVertex2();

            grx.setColor(face.getColor());

            path = new Path2D.Double();
            line = new Line2D.Double(vert0.getPosX(), vert0.getPosY(), vert1.getPosX(),
                    vert1.getPosY());
            path.append(line, true);
            line = new Line2D.Double(vert1.getPosX(), vert1.getPosY(), vert2.getPosX(),
                    vert2.getPosY());
            path.append(line, true);
            line = new Line2D.Double(vert2.getPosX(), vert2.getPosY(), vert0.getPosX(),
                    vert0.getPosY());
            path.append(line, true);
            grx.fill(path);

            grx.setColor(new Color(0, 0, 0, 128));
            grx.draw(path);
        }
    }
}
