using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Windows.Forms;
using System.Diagnostics;

namespace Tutor_RT
{
    public partial class Form1 : Form
    {
        private double xmin, xmax, ymin, ymax;
        private double xFactor, yFactor;
        private Bitmap _backBuffer, HHG;
        V3 ex, ey, ez;
        Camera camera;
        Light light;
        Plane terrain;
        Sphere sphere_1, sphere_2, sphere_3;
        int img_width, img_height;
        List<Object> scene_objects = new List<Object>();
        double ambientlight, fuzzy;

        public Form1()
        {
            InitializeComponent();
            this.DoubleBuffered = true; //otherwise it's flickering
            xmax = 2.5;
            xmin = -2.5;
            //ymax = (xmax - xmin)/1920.0*1200.0;
            ymin = 0;
            ex = new V3(1, 0, 0); ey = new V3(0, 1, 0); ez = new V3(0, 0, 1);
            camera = new Camera(new V3(0, 1.5, +4), ex, ey, ez);
            terrain = new Plane(ey, 0, new Color_0_to_1(1, Color.White));
            scene_objects.Add(terrain);//Index 0
            sphere_1 = new Sphere(1 * ex + 1 * ey - 0.5 * ez, 0.75, new Color_0_to_1(1, Color.DarkGreen));
            scene_objects.Add(sphere_1);//Index 1
            sphere_2 = new Sphere(-1.75 * ex + 1 * ey - 1 * ez, 0.75, new Color_0_to_1(1, Color.Red));
            scene_objects.Add(sphere_2);//Index 2
            sphere_3 = new Sphere(-0.5 * ex + 2 * ey - 1 * ez, 0.75, new Color_0_to_1(1, Color.DarkViolet));
            scene_objects.Add(sphere_3);//Index 3
            light = new Light(-7 * ex + 20 * ey + 10 * ez, new Color_0_to_1(0, Color.WhiteSmoke));
            ambientlight = 0.5;//amount of light from surrounding; 0.1 dark white tiles
            fuzzy = 0.000001;
            HHG = new Bitmap(System.Environment.CurrentDirectory + "\\HHG.png");
            this.Text = "Calculations may take some time"; 
        }

        public int xX(double x)
        {
            return (int)Math.Round(xFactor * (x - xmin));
        }

        public int yY(double y)
        {
            return (int)Math.Round(yFactor * (ymax - y));
        }

        public List<double> find_pt_ob(Ray g)//find intersectionpoint(pt) and the object(ob)
        {//first: calculate all intersections
            List<double> L = new List<double>();//reserve
            L.Add(0); L.Add(0);//L[0]=lambda; L[1]=object index in scene_objects
            for (int n = 0; n < scene_objects.Count; n++)
                L.Add(((AObject)scene_objects[n]).Lambda_min(g));
            L[0] = L.Max(); L[1] = -1;//and then find the closest intersection to the screen
            if (L[0] > 0) //in front of the camera
                for (int n = 2; n < L.Count; n++)
                    if (0 < L[n] && L[n] <= L[0])
                    {//... now we look for smaller ones 
                        L[0] = L[n];
                        L[1] = n - 2;//0:terrain; 1:sphere
                    }//"n - 2": remember L[0] and L[1] are reserved
            return L;
        }

        public void raytracer(Graphics g)
        {
            img_width = this.ClientSize.Width; img_height = this.ClientSize.Height;
            //img_width = 640; img_height = 480;
            ymax = (xmax - xmin) / img_width * img_height;
            xFactor = img_width / (xmax - xmin);
            yFactor = img_height / (ymax - ymin);
            double dx = (xmax - xmin) / img_width, dy = (ymax - ymin) / img_height;
            double x1 = xmin, y1 = ymax;

            for (int x = 0; x < img_width; x++)
            {//ray tracing: how many rays we follow? img_width * img_height rays with its reflections
                for (int y = 0; y < img_height; y++)
                {//Set up a ray from the camera to the screen = image plane
                    Ray h = new Ray(camera.getcampos, new V3(x1, y1, 0) - camera.getcampos);
                    List<double> lambda = find_pt_ob(h);
                    if (lambda[1] == -1)//object is infinity
                        g.FillRectangle(new SolidBrush(Color.Black), xX(x1), yY(y1), 1, 1);
                    else//we hit terrain=plane or a sphere and calculate an appropriate color                   
                    {//determine point of intersection(pti)=initial point for further rays r, v ...
                        V3 pti = h.get_a + h.get_u * lambda[0];//... calculated with u
                        Color_0_to_1 intersection_color = getColorAt(pti, h.get_u, (int)lambda[1]);
                        g.FillRectangle(new SolidBrush(intersection_color.WinColor()), xX(x1), yY(y1), 1, 1);
                    }
                    y1 -= dy;
                }
                x1 += dx;
                y1 = ymax;
            }
        }

        public Color_0_to_1 getColorAt(V3 A, V3 u, int ico)//ico: index of closest object
        {//co is closest object we=eye=camera see; A: initial point; u: u of intersection ray
            Color_0_to_1 co_color;
            //A:--------terrain-HHG-------------------------------------------------------------------------
            if (ico == 0)//only terrain
            {
                int xx = Math.Min(xX(A.X), HHG.Width - 5), yy = Math.Min(xX(A.Z - 0.6), HHG.Height - 5);
                xx = xx < 0 ? 0 : xx; yy = yy < 0 ? 0 : yy;
                co_color = new Color_0_to_1(0, HHG.GetPixel(xx, yy));
            }
            else co_color = ((AObject)scene_objects[ico]).getColor;
            Color_0_to_1 final_color = ambientlight * co_color; //first portion of color
                                                                //B:--------------Color-from-the-r-direction-------------------------------
            V3 co_n = ((AObject)scene_objects[ico]).get_n(A);
            V3 r = u - 2 * co_n * u * co_n;//calculate refection r = u-2*<u,n0>*n0
            if (ico != 0)//except terrain
            {
                List<double> lambda_ref = find_pt_ob(new Ray(A, r));//ray x=A+lam*r hits the scene
                if (lambda_ref[1] != -1)//ray hits plane or sphere
                    if (lambda_ref[0] > fuzzy)
                    {//r only affects the final color if it is reflected from something                       
                        V3 a2 = A + r * lambda_ref[0];//a further intersection point ...
                        double s2 = ((AObject)scene_objects[(int)lambda_ref[1]]).getColor.C01_S;
                        /*recursive-call*/
                        final_color = final_color + s2 * getColorAt(a2, r, (int)lambda_ref[1]);
                    }//...and the second portion of color
            }
            //C:----------Contribution of the light sources
            V3 v = light.get_p - A;
            double cos_alpha = co_n * v.normalize;//<n0,v0> = cos(alpha)*1*1
            if (cos_alpha > 0)//that is if lightsource is in front and not behind(cos<0)
            {
                List<double> lambda2 = find_pt_ob(new Ray(A, v));//ray for shadow or not shadow
                bool noShadow = true;//c = 2, da 0 für lambda u. 1 für obj. reserviert sind
                for (int c = 2; c < lambda2.Count; c++)
                    if (fuzzy < lambda2[c] && lambda2[c] < v.Length)
                    { noShadow = false; break; }
                if (noShadow)
                    final_color = final_color + cos_alpha * co_color * light.getColor;
            }
            //D:----------------reflective-survace-------------
            if (ico != 0)//except terrain
            {
                double cos_beta = r.normalize * v.normalize;
                if (cos_beta > 0)//mirroring
                    final_color = final_color + Math.Pow(cos_beta, 10) * co_color.C01_S * light.getColor;
            }
            return final_color.clip();
        }

        private void Form1_Paint(object sender, PaintEventArgs e)
        {
            _backBuffer = new Bitmap(this.ClientSize.Width, this.ClientSize.Height);
            Cursor.Current = Cursors.WaitCursor;
            Stopwatch dt = new Stopwatch(); dt.Start();
            raytracer(Graphics.FromImage(_backBuffer));
            dt.Stop();
            e.Graphics.DrawImageUnscaled(_backBuffer, 0, 0);//Copy back buffer to screen
            Cursor.Current = Cursors.Default;
            this.Text = "Time of calculation:  dt = " + dt.ElapsedMilliseconds + "ms";
        }
    }
}
