﻿using UnityEngine;
using System.Collections;
using System.Collections.Generic;

public class MirrorCameraScript : MonoBehaviour
{
    public GameObject MirrorObject;

    private Renderer mirrorRenderer;
    private Material mirrorMaterial;

    private MirrorScript mirrorScript;
    private Camera cameraObject;
    private RenderTexture reflectionTexture;
    private Matrix4x4 reflectionMatrix;
    private int oldReflectionTextureSize;
    private static bool renderingMirror;

    private void Start()
    {
        mirrorScript = GetComponentInParent<MirrorScript>();
        cameraObject = GetComponent<Camera>();
        //cameraObject.enabled = true;

        if (mirrorScript.AddFlareLayer)
        {
            cameraObject.gameObject.AddComponent<FlareLayer>();
        }

        mirrorRenderer = MirrorObject.GetComponent<Renderer>();
        if (Application.isPlaying)
        {
            mirrorRenderer.sharedMaterial = mirrorRenderer.material;
        }
        mirrorMaterial = mirrorRenderer.sharedMaterial;

        CreateRenderTexture();
    }

    private void CreateRenderTexture()
    {
        if (reflectionTexture == null || oldReflectionTextureSize != mirrorScript.TextureSize)
        {
            if (reflectionTexture)
            {
                DestroyImmediate(reflectionTexture);
            }
            reflectionTexture = new RenderTexture(mirrorScript.TextureSize, mirrorScript.TextureSize, 16);
            reflectionTexture.filterMode = FilterMode.Bilinear;
            reflectionTexture.antiAliasing = 1;
            reflectionTexture.name = "MirrorRenderTexture_" + GetInstanceID();
            reflectionTexture.hideFlags = HideFlags.HideAndDontSave;
            reflectionTexture.autoGenerateMips = false;
            reflectionTexture.wrapMode = TextureWrapMode.Clamp;
            mirrorMaterial.SetTexture("_MainTex", reflectionTexture);
            oldReflectionTextureSize = mirrorScript.TextureSize;
        }

        if (cameraObject.targetTexture != reflectionTexture)
        {
            cameraObject.targetTexture = reflectionTexture;
        }
    }

    private void Update()
    {
        CreateRenderTexture();
    }

    private void UpdateCameraProperties(Camera src, Camera dest)
    {
        dest.clearFlags = src.clearFlags;
        dest.backgroundColor = src.backgroundColor;
        if (src.clearFlags == CameraClearFlags.Skybox)
        {
            Skybox sky = src.GetComponent<Skybox>();
            Skybox mysky = dest.GetComponent<Skybox>();
            if (!sky || !sky.material)
            {
                mysky.enabled = false;
            }
            else
            {
                mysky.enabled = true;
                mysky.material = sky.material;
            }
        }

        dest.orthographic = src.orthographic;
        dest.orthographicSize = src.orthographicSize;
        if (mirrorScript.AspectRatio > 0.0f)
        {
            dest.aspect = mirrorScript.AspectRatio;
        }
        else
        {
            dest.aspect = src.aspect;
        }
		dest.renderingPath = src.renderingPath;
    }

    internal void RenderMirror()
    {
        Camera cameraLookingAtThisMirror;

        // bail if we don't have a camera or renderer
        if (renderingMirror || !enabled || (cameraLookingAtThisMirror = Camera.current) == null ||
            mirrorRenderer == null || mirrorMaterial == null || !mirrorRenderer.enabled)
        {
            return;
        }

        renderingMirror = true;

        int oldPixelLightCount = QualitySettings.pixelLightCount;
        if (QualitySettings.pixelLightCount != mirrorScript.MaximumPerPixelLights)
        {
            QualitySettings.pixelLightCount = mirrorScript.MaximumPerPixelLights;
        }

        try
        {
            UpdateCameraProperties(cameraLookingAtThisMirror, cameraObject);

            if (mirrorScript.MirrorRecursion)
            {
                mirrorMaterial.EnableKeyword("MIRROR_RECURSION");
                cameraObject.ResetWorldToCameraMatrix();
                cameraObject.ResetProjectionMatrix();
                cameraObject.projectionMatrix = cameraObject.projectionMatrix * Matrix4x4.Scale(new Vector3(-1, 1, 1));
				cameraObject.cullingMask = ~(1 << 4) & mirrorScript.ReflectLayers.value;
                GL.invertCulling = true;
                cameraObject.Render();
                GL.invertCulling = false;
            }
            else
            {
                mirrorMaterial.DisableKeyword("MIRROR_RECURSION");
                Vector3 pos = transform.position;
                Vector3 normal = (mirrorScript.NormalIsForward ? transform.forward : transform.up);

                // Reflect camera around reflection plane
                float d = -Vector3.Dot(normal, pos) - mirrorScript.ClipPlaneOffset;
                Vector4 reflectionPlane = new Vector4(normal.x, normal.y, normal.z, d);
                CalculateReflectionMatrix(ref reflectionPlane);
                Vector3 oldpos = cameraObject.transform.position;
                float oldclip = cameraObject.farClipPlane;
                Vector3 newpos = reflectionMatrix.MultiplyPoint(oldpos);
                Matrix4x4 worldToCameraMatrix = cameraLookingAtThisMirror.worldToCameraMatrix * reflectionMatrix;
                cameraObject.worldToCameraMatrix = worldToCameraMatrix;

                // Clip out background
                Vector4 clipPlane = CameraSpacePlane(ref worldToCameraMatrix, ref pos, ref normal, 1.0f);
                cameraObject.projectionMatrix = cameraLookingAtThisMirror.CalculateObliqueMatrix(clipPlane);
                GL.invertCulling = true;
                cameraObject.transform.position = newpos;
                cameraObject.farClipPlane = mirrorScript.FarClipPlane;
				cameraObject.cullingMask = ~(1 << 4) & mirrorScript.ReflectLayers.value;
                cameraObject.Render();
                cameraObject.transform.position = oldpos;
                cameraObject.farClipPlane = oldclip;
                GL.invertCulling = false;
            }
        }
        finally
        {
            renderingMirror = false;
            if (QualitySettings.pixelLightCount != oldPixelLightCount)
            {
                QualitySettings.pixelLightCount = oldPixelLightCount;
            }
        }
    }

    // Cleanup all the objects we possibly have created
    private void OnDisable()
    {
        if (reflectionTexture)
        {
            DestroyImmediate(reflectionTexture);
            reflectionTexture = null;
        }
    }

    private Vector4 CameraSpacePlane(ref Matrix4x4 worldToCameraMatrix, ref Vector3 pos, ref Vector3 normal, float sideSign)
    {
        Vector3 offsetPos = pos + normal * mirrorScript.ClipPlaneOffset;
        Vector3 cpos = worldToCameraMatrix.MultiplyPoint(offsetPos);
        Vector3 cnormal = worldToCameraMatrix.MultiplyVector(normal).normalized * sideSign;
        return new Vector4(cnormal.x, cnormal.y, cnormal.z, -Vector3.Dot(cpos, cnormal));
    }

    private void CalculateReflectionMatrix(ref Vector4 plane)
    {
        // Calculates reflection matrix around the given plane

        reflectionMatrix.m00 = (1F - 2F * plane[0] * plane[0]);
        reflectionMatrix.m01 = (-2F * plane[0] * plane[1]);
        reflectionMatrix.m02 = (-2F * plane[0] * plane[2]);
        reflectionMatrix.m03 = (-2F * plane[3] * plane[0]);

        reflectionMatrix.m10 = (-2F * plane[1] * plane[0]);
        reflectionMatrix.m11 = (1F - 2F * plane[1] * plane[1]);
        reflectionMatrix.m12 = (-2F * plane[1] * plane[2]);
        reflectionMatrix.m13 = (-2F * plane[3] * plane[1]);

        reflectionMatrix.m20 = (-2F * plane[2] * plane[0]);
        reflectionMatrix.m21 = (-2F * plane[2] * plane[1]);
        reflectionMatrix.m22 = (1F - 2F * plane[2] * plane[2]);
        reflectionMatrix.m23 = (-2F * plane[3] * plane[2]);

        reflectionMatrix.m30 = 0F;
        reflectionMatrix.m31 = 0F;
        reflectionMatrix.m32 = 0F;
        reflectionMatrix.m33 = 1F;
    }

    private static void CalculateObliqueMatrix(ref Matrix4x4 projection, ref Vector4 clipPlane)
    {
        Vector4 q = projection.inverse * new Vector4(
            Sign(clipPlane.x),
            Sign(clipPlane.y),
            1.0f,
            1.0f
        );
        Vector4 c = clipPlane * (2.0F / (Vector4.Dot(clipPlane, q)));
        // third row = clip plane - fourth row
        projection[2] = c.x - projection[3];
        projection[6] = c.y - projection[7];
        projection[10] = c.z - projection[11];
        projection[14] = c.w - projection[15];
    }

    private static float Sign(float a)
    {
        if (a > 0.0f) return 1.0f;
        if (a < 0.0f) return -1.0f;
        return 0.0f;
    }
}
