using UnityEngine;
using System.Collections;

// Test-Script für Animation einer Substance zur Laufzeit
public class DynamicMaterial : MonoBehaviour {

    // das prozedurale Material, das wir dynamisch anpassen wollen
    public ProceduralMaterial substance = null;

    // den Spieler brauchen wir, weil wir die
    // Substance anhand seiner Position ändern
    public Transform player = null;

    // wie oft pro Sekunde soll die Substance neu berechnet werden?
    // hier erlauben wir Werte zwischen 0.1 und 60: 0.1, weil es
    // bei 0 eine Division durch 0 geben würde, und das wollen wir nicht,
    // und 0.1 bedeutet "alle 10 Sekunden" (längere Intervalle machen
    // nicht wirklich Sinn)
    // 60, weil mehr als 60 Mal pro Sekunde macht auch keinen Sinn,
    // das sieht kein Mensch mehr und es würde nur sinnlos Performance
    // opfern
    [Range(0.1F, 60F)]
    public float updateFrequencyHz = 1F;


    // in welchem Bereich bewegt sich der Spieler?
    // das brauchen wir, um die Position zu "normalisieren" (s.u.)
    public Vector3 minLocation = new Vector3(-100F, 0F, -100F);
    public Vector3 maxLocation = new Vector3(100F, 0F, 100F);


    // einige Werte zum Animieren 
    
    // (Color nimmt Werte von 0-1, daher Umrechnung von 0-255)
    public Color planeColorMin = new Color(0F, 9F/255F, 77F/255F);
    public Color planeColorMax = new Color(48F/255F, 93F / 255F, 124F / 255F);

    public Color gridColorMin = new Color(1F, 118F / 255F, 0F);
    public Color gridColorMax = new Color(0F, 1F, 1F); // 0, 255, 255 is easy ;-)


    // einige Werte zum Testen:

    // damit kann eine Debug-Ausgabe am Bildschirm aktiviert werden
    public bool doDebug = false;

    // wenn man das auf true setzt, läuft das in der Update-Methode
    public bool everyFrame = false;

    // wenn man das auf true setzt, werden die Texturen im Main-thread
    // neu gebaut, was zu Ruckeln führen sollte (sollte man also nie tun,
    // außer um es zu testen ;-) )
    public bool rebuildImmediately = false;

    // Wie sollen beim Berechnen der Texturen die Prozessoren genutzt werden?
    public ProceduralProcessorUsage processorUsage = ProceduralProcessorUsage.One;

    // das sollte man normalerweise natürlich nicht tun - aber zum Testen
    // ist das ganz nützlich
    void Update() {
        if (everyFrame) {
            UpdateMaterial();
        }
    }

    // hier machen wir die eigentliche Arbeit:
    // paramA, paramB und paramC liegen jeweils zwischen 0 und 1
    public void UpdateMaterial() {
        // zuerst ermitteln wir die normalisierte Spieler-Position,
        // also die Position des Spielers im Wertebereich 0-1
        // Die Methode CalculateNormalizedPosition() ist weiter
        // unten im Script implementiert
        Vector3 normalizedPos = CalculateNormalizedPosition();

        // das hier brauchen wir nur zum Debuggen:
        if (doDebug) {
            debugValues = normalizedPos;
        }

        // stelle sicher, dass die Parameter wirklich im
        // erwarteten Wertebereich liegen (paramB wird derzeit 
        // nicht genutzt):
        float paramA = Mathf.Clamp(normalizedPos.x, 0, 1);
        //float paramB = Mathf.Clamp(normalizedPos.y, 0, 1);
        float paramC = Mathf.Clamp(normalizedPos.z, 0, 1);


        // Schritt 1: jetzt berechnen wir Werte für die Parameter:

        // paramB verwenden wir nicht, weil Y bei uns immer 0 ist!
        Color planeColor = Color.Lerp(planeColorMin, planeColorMax, paramA);
        Color gridColor = Color.Lerp(gridColorMin, gridColorMax, paramC);

        // die Breite ist von beiden Werten abhängig
        float gridWidth = (paramA + paramC) / 2F;
        // und muss in einem sinnvollen Bereich liegen
        gridWidth *= 0.2F;
        gridWidth += 0.01F;


        // Schritt 2: Und setzen diese Werte für das Material:
        // WICHTIG: Die inputName-Parameter finden wir in den Tooltips
        // im Inspector - das ist hier nur "zufällig" der gleiche, wie
        // der Variablenname mit dem eigentlichen Wert
        substance.SetProceduralColor("planeColor", planeColor);
        substance.SetProceduralColor("gridColor", gridColor);
        substance.SetProceduralFloat("gridWidth", gridWidth);


        // Schritt 3: zuletzt muss die Textur neu berechnet werden
        if (rebuildImmediately) {
            // sofort - führt ggf. zu ruckeln (sollte man nicht machen, 
            // außer wenn man die Berechnung des Materials im Profiler
            // sehen möchte)
            substance.RebuildTexturesImmediately();
        } else {
            // rechnen wir noch (von vorher?)
            if (!substance.isProcessing) {
                // nein? dann go, wenn du wieder Zeit hast ;-)
                // RebuildTextures *sollte* kein Ruckeln verursachen
                // ... tut es aber bei mir gerade ... Bug ist geschickt ;-)
                substance.RebuildTextures();
            }
        }
    }



    // Ab hier steht Code, der prüft, ob alles passt, und
    // weiter unten eine Coroutine, die dafür sorgt, dass
    // UpdateSubstance jeweils zur richtigen Zeit (anhand
    // der Frequenz) aufgerufen wird, mit dem richtigen
    // Eingabeparameter


    void Awake() {
        // da wir hier zwei unterschiedliche Sachen machen, und
        // CheckProceduralMaterial recht lang und aufwändig ist, 
        // lagern wir die gesamte Logik in zwei Methoden aus, die
        // wir von Awake aus aufrufen:
        CheckPlayer();
        CheckProceduralMaterial();
        // sind wir noch aktiv? nur dann Folgendes:
        if (this.enabled) {
            ConfigureSubstanceForDynamicUpdates();
        }
    }

    private void ConfigureSubstanceForDynamicUpdates() {
        // setze global, wie die Substance-Engine den Prozessor
        // nutzen darf
        ProceduralMaterial.substanceProcessorUsage = processorUsage;

        // an diesen Werten nichts ändern, weil das zu teuer wäre
        substance.CacheProceduralProperty("glowAmount", true);
        substance.CacheProceduralProperty("glowSize", true);
        substance.CacheProceduralProperty("blurIntensity", true);
    }

    // ist ein Spieler zugewiesen? wenn nicht: versuche einen zu finden
    private void CheckPlayer() {
        // wurde ein player zugewiesen?
        if (player == null) {
            // nein? dann suchen wir uns halt einen:
            GameObject playerGameObject = GameObject.FindGameObjectWithTag("Player");

            // haben wir einen mit Tag "Player" gefunden?
            if (playerGameObject == null) {
                // nein ... das ist schlecht:

                // eine Warnung in die Konsole ausgeben (dadurch, dass this übergeben
                // wird, kann man diese Warnung in der Konsole anklicken und sieht dann
                // in der Hierarchy das Objekt, von dem sie ausgegeben wurde):
                Debug.LogWarningFormat(this,
                    "DynamicMaterial of {0} needs to either have a player assigned or "
                    + "a player GameObject that is tagged with 'Player'",
                    this.name);
                // die Komponente abschalten, damit sie keinen Ärger macht
                this.enabled = false;

                // hier ist nichts mehr für uns zu tun - raus aus CheckPlayer()
                return;
            }

            // wir wollen ja eigentlich die Transform-Komponente
            player = playerGameObject.transform;
        }
    }

    // falls kein dynamisches Material zugewiesen ist, versuche
    // eines am aktuellen GameObject zu ermitteln
    private void CheckProceduralMaterial() {
        // Kann diese Plattform das überhaupt?
        if (!ProceduralMaterial.isSupported) {
            Debug.LogWarningFormat(
                "Platform {0} or this device does not support procedural materials",
                Application.platform);
        }

        // falls die Substance nicht zugewiesen ist
        if (substance == null) {
            // Materialien hängen am MeshRenderer
            MeshRenderer renderer = GetComponent<MeshRenderer>();
            
            // gibt's hier überhaupt einen MeshRenderer?
            if (renderer != null) {
                
                // wir suchen das erste Material, das eine Substance ist,
                // dazu laufen wir über alle Materialien
                foreach (Material material in renderer.materials) {
                    
                    // ist material nicht nur einfach Material, sondern ProceduralMaterial?
                    if (material is ProceduralMaterial) {
                        
                        // ja? ok, dann weise es also solches (Typecasting) 
                        // der Variable substance zu
                        substance = (ProceduralMaterial)material;
                        
                        // fertig, wir brauchen nicht mehr weiter suchen
                        break;
                    }
                }
            }

            // immer noch null? Ja => keines gefunden => den Benutzer 
            // warnen und die Komponente abschalten!
            if (substance == null) {
                Debug.LogWarningFormat(this,
                    "DynamicMaterial on {0} has no procedural material assigned => disabled!",
                    this.name);

                this.enabled = false;

                return;
            }
        }

        // unterstützt diese Substance überhaupt dynamische Änderungen?
        if (substance.loadingBehavior == ProceduralLoadingBehavior.BakeAndDiscard) {
            // nein, dann halt nicht:
            Debug.LogWarningFormat(this,
                "Substance {0} is not configured for dynamic changes - do not use Bake and Discard!",
                substance.name);
            this.enabled = false;
            return;
        }
    }

    // kurz bevor das Spiel losgeht starten wir eine Coroutine
    void Start() {
        StartCoroutine(UpdateMaterialLoop());
    }

    // eine Schleife, die regelmäßig etwas macht ("UpdateMaterial")
    // und dann wartet
    public IEnumerator UpdateMaterialLoop() {
        // Endlosschleife - aber das wollen wir so:
        while (true) {
            // wenn wir das sowieso in Update machen, brauchen wir es hier nicht
            if (!everyFrame) {
                // jetzt die eigentliche Arbeit (siehe oben in der Klasse)
                UpdateMaterial();
            }

            // warte anhand der Frequenz
            // Anmerkung: Division ist relativ teuer, daher könnte
            // man diesen Wert auch in einer Variablen speichern
            // aber das hier wird so selten ausgeführt, dass sich
            // der Aufwand nicht lohnt
            yield return new WaitForSeconds(1F / updateFrequencyHz);
        }
    }

    // aktuelle normalizedPos
    private Vector3 debugValues;

    // male mit dem alten UnityGUI GUI-System eine Debug-Ausgabe auf den Bildschirm
    void OnGUI() {
        if (doDebug) {
            GUI.Label(
                new Rect(10, 10, 200, 20),
                string.Format("Normalized Pos: {0}", debugValues));
        }
    }

    private Vector3 CalculateNormalizedPosition() {
        // aktuelle Spielerposition
        Vector3 playerPos = player.position;

        // Schritt 1: Die kleinste Position soll (0, 0, 0) sein:
        Vector3 normalizedPos = playerPos - minLocation;

        // Schritt 2: Die maximale Position soll (1, 1, 1) sein:
        Vector3 normalizedMaxPos = maxLocation - minLocation;

        // die Division um auf 1 zu kommen pro Komponente, und
        // da wir den Fall "ist 0" ausschließen müssen in einer 
        // eigenen Methode
        normalizedPos.x = NormalizePos(normalizedPos.x, normalizedMaxPos.x);
        normalizedPos.y = NormalizePos(normalizedPos.y, normalizedMaxPos.y);
        normalizedPos.z = NormalizePos(normalizedPos.z, normalizedMaxPos.z);

        return normalizedPos;
    }

    private float NormalizePos(float current, float max) {
        // Ternary / Bedingter Operator, siehe auch:
        // Deutsch: Operator ?: (C#-Referenz)
        // http://msdn.microsoft.com/de-de/library/ty67wk28.aspx
        // Englisch: ?: Operator (C# Reference)
        // http://msdn.microsoft.com/en-us/library/ty67wk28.aspx
        return (max != 0) 
            ? current / max 
            : 0;
    }

}
