﻿/****************************************************************************
 * 
 * <copyright file="BackwardMaterialBuilder.cs" company="Klakos">
 * Copyright (c) 2010, 2010 All Rights Reserved, http://klakos.com
 * 
 * LICENCE : GPLv2 [http://www.gnu.org/licenses/gpl-2.0.html]
 * 
 * This source code is intended only as a supplement to Unity
 * Development Tools and/or on-line documentation. See these other
 * materials for detailed information regarding Unity code samples.
 *
 * THIS CODE AND INFORMATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY 
 * KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
 * PARTICULAR PURPOSE.
 * 
 * </copyright>
 * 
 * <author>Gauthier BOAGLIO</author>
 * <email>gauthier __at__ klakos.com</email>
 * <date>2011-05-10</date>
 * 
 * <summary>
 * Basic tool (Unity [Copyright (c)] 3.3.0f3 addon) for easy setup of 
 * Double sided / Dual Materials Shading Technics - MORE INFORMATIONS
 * HERE : http://klakos.com/?p=2020, [ARTICLE : Advanced Waving Flag 
 * Shader for Unity (Double sided, Alpha shadow support)]
 * </summary>
 * 
 * *************************************************************************/


/****************************************************************************
 * <usage>
 * 
 * Note : Regular => Backward only...
 * ----
 * 
 * Install :
 * -------
 * Just drop the “BackwardMaterialBuilder.cs” (ScriptableWizard) script into 
 * the “Assets/Editor” directory of the Unity project.
 * 
 * Prerequisites :
 * -------------
 * 
 * 1- Naming conventions for shaders :
 * - Front/Forward shaders Name has to end with “Regular” suffix
 * - Back/Backward shaders Name has to end with “Backward” suffix
 * 
 * Those conventions deal with the Shaders name (not the Materials name)
 * 
 * [CODE]
 * 
 * Shader "Selfmade/for-2sided/FlagWave Advanced Regular" 
 * { 
 * 
 * Properties 
 * { 
 *  	// Usual stuffs
 * 	    _Color ("Main Color", Color) = (1,1,1,1)
 * 	    _SpecColor ("Specular Color", Color) = (0.5, 0.5, 0.5, 0)
 * 	    .
 * 	    .
 * 	    .
 * [/CODE]
 * 
 * 2- Material assets need to be made by hand and this tool will only 
 *      help to set them up correctly.
 * 
 * 3- The different shaders may have been previously created.
 * 
 * Usage :
 * -----
 * To access the script, select an object (in the Hierarchy or Project pane). 
 * Then right-click on the context menu icon of the 
 * Renderer (either Mesh or SkinnedMesh Renderer), and choose “Set 2-sided Materials” :
 * The related window displays a selectable list of all the “Regular” shaded 
 * materials from the Renderer materials list :
 * Then select one of those Materials and press “Proceed”.
 * 
 * If the corresponding “Backward” Shader exists in you Project, 3 different 
 * things may happen :
 * 
 * - Synchronize props form “Regular” to “Backward”, if Mats are existing
 * - Create the “Backward” one if not existing (not setting the Shader)
 * - Erase or not the actual Shader of the current “Target Backup Material”
 * 
 * Then drop a hand made target Material of your choice from your “Assets” folder 
 * into the “Target Backup Material” field.
 * 
 * Option “Force Shader”, will set the right existing “Backward” Shader to the 
 * targeted Material.
 * If unchecked, the Shader of the original dropped Material will remain the same.
 * In many cases, this option has to be checked (but you'll lose all the current
 * settings of the target Material).
 * 
 * </usage>
 * 
 * *************************************************************************/


using UnityEngine;
using UnityEditor;
using System.Collections;
using System.Collections.Generic;
using System.IO;


public class BackwardMaterialBuilder : ScriptableWizard
{
    # region Main Window Handler
    private static BackwardMaterialBuilder m_Win;
    # endregion


    # region Operating Stuffs

    public static bool doCreateBWMaterial = false;
    public static Renderer targetRenderer;
    private Material regularMaterial;
    private Material backwardMaterial;

    List<Material> materials = new List<Material>();

    static string FRONT_FACE_SUFFIX = "Regular";
    static string BACK_FACE_SUFFIX = "Backward";

    // Target material
    private Material matToSaveTo;

    private bool showMaterialCreatorWindow = false;
    private bool forceShader = false;

    # endregion


    # region GUI stuffs

    Rect windowRect = new Rect(100, 100, 500, 200);
    //Vector2 scrollPos = Vector2.zero;
    int intSelectedMat = 0;
    Dictionary<string, Material> matsMap = new Dictionary<string, Material>();
    List<string> matNames = new List<string>();

    # endregion


    # region Context Menu Settings

    // Context menu:
    [MenuItem("CONTEXT/MeshRenderer/Set 2-sided Materials")]
    static void UpdateBackwardMaterialForMeshRenderer(MenuCommand cmd)
    {
        RunWizard(cmd);
    }

    // Validator:
    [MenuItem("CONTEXT/MeshRenderer/Set 2-sided Materials", true)]
    static bool ValidateBackwardMaterialForMeshRendererContext(MenuCommand cmd)
    {
        return cmd.context is MeshRenderer;
    }

    // Context menu:
    [MenuItem("CONTEXT/SkinnedMeshRenderer/Set 2-sided Materials")]
    static void UpdateBackwardMaterialForSkinnedMeshRenderer(MenuCommand cmd)
    {
        RunWizard(cmd);
    }

    // Validator:
    [MenuItem("CONTEXT/SkinnedMeshRenderer/Set 2-sided Materials", true)]
    static bool ValidateBackwardMaterialForSkinnedMeshRendererContext(MenuCommand cmd)
    {
        return cmd.context is SkinnedMeshRenderer;
    }

    static void RunWizard(MenuCommand cmd)
    {
        BackwardMaterialBuilder.doCreateBWMaterial = (BackwardMaterialBuilder.targetRenderer == null);
        BackwardMaterialBuilder.targetRenderer = (Renderer)cmd.context;
        m_Win = (BackwardMaterialBuilder)ScriptableWizard.DisplayWizard("Create & Update Backwards Materials", typeof(BackwardMaterialBuilder));
    }

    # endregion


    # region Let's Go

    void OnGUI()
    {
        if (m_Win != null)
        {
            //GUILayout.BeginScrollView(scrollPos);

            // Listing "Regular" (Front faces dedicated) Materials
            // Cleanup
            List<string> to_remove = new List<string>();
            foreach (KeyValuePair<string, Material> kv in matsMap)
            {
                if (!MaterialAlreadyExists(kv.Value)) to_remove.Add(kv.Key);
            }
            foreach (string mat_name in to_remove)
            {
                matsMap.Remove(mat_name);
                matNames.Remove(mat_name);
            }
            // Rebuild
            foreach (Material mat in targetRenderer.sharedMaterials)
            {
                if (!(matsMap.ContainsKey(mat.shader.name)) && IsRegularMaterial(mat))
                {
                    matsMap.Add(mat.shader.name, mat);
                    matNames.Add(mat.shader.name);
                }
            }

            // Display
            if (matNames.Count > 0)
                intSelectedMat = GUILayout.SelectionGrid(intSelectedMat, matNames.ToArray(), 1);
            else
            {
                GUILayout.Label("Nothing to operate on ! \nThe Renderer of the object \"" + targetRenderer.gameObject.name +
                                "\" must contain at least one Material with a \"Regular\" shader");
                if (GUILayout.Button("Abort"))
                {
                    m_Win.Close();
                }
                return;
            }

            GUILayout.Space(50);

            regularMaterial = matsMap[matNames[intSelectedMat]];

            if (GUILayout.Button("Proceed"))
            {
                Proceed();
            }
            else if (showMaterialCreatorWindow && m_Win != null)
            {
                BeginWindows();
                windowRect = GUILayout.Window(1, windowRect, DoMaterialCreatorWindow, "Regular => Backward");
                EndWindows();
            }

            //GUILayout.EndScrollView();
        }
    }


    // Processing
    void Proceed()
    {
        // Synchronization for existing "Regular & Backward" Materials
        // foreach (Material mat1 in targetrenderer.sharedMaterials)
        Material mat1 = regularMaterial;

        if (mat1 != null)
        {
            if (mat1.shader.name.EndsWith(FRONT_FACE_SUFFIX))
            {
                Debug.Log("Regular Mat found : " + mat1.shader.name);
                // Try finding the 'Backward' one
                Material mat22 = null;
                foreach (Material mat2 in targetRenderer.sharedMaterials)
                {
                    string bw_shader_name = mat1.shader.name.Replace(FRONT_FACE_SUFFIX, BACK_FACE_SUFFIX);
                    Shader bw_shader = Shader.Find(bw_shader_name);
                    if (mat2 != mat1 && mat2.shader.name == bw_shader_name)
                    {
                        Debug.Log("Backward Mat found : " + mat2.shader.name);
                        // Synchronize & Break
                        if (bw_shader != null)
                        {
                            mat2.shader = bw_shader;
                            mat2.CopyPropertiesFromMaterial(mat1);
                            mat22 = mat2;
                        }
                        break;
                    }
                }
                // Do synchronization
                if (mat22 != null)
                {
                    EditorUtility.DisplayDialog("Info !", "The Regular material [SHADER : \"" + mat1.shader.name + "\"] \n\nAND \n\nthe Backward " +
                                                "material [SHADER : \"" + mat22.shader.name + "\"] \nhave been successfuly synchronized ...\n\n" +
                                                "Please give a click in the \"Inspector Tab\" to show the changes ;-)",
                                                "Ok");
                    Debug.Log("Synch. done !");
                }
                // Try to setup the "Backward" Material from existing shader
                else
                {
                    bool doCreate = EditorUtility.DisplayDialog("Warning !", "The Regular material \"" + mat1.shader.name + "\" was found," +
                                                                "\nbut no related Backward one.\n" +
                                                                "Try to create the Backward material ?",
                                                                "Ok", "Cancel");
                    if (doCreate)
                    {
                        // Try finding the right shader
                        string shader_to_find = mat1.shader.name.Replace(FRONT_FACE_SUFFIX, BACK_FACE_SUFFIX);
                        Shader shader1 = Shader.Find(shader_to_find);

                        // Shader not found
                        if (shader1 == null)
                        {
                            bool read = EditorUtility.DisplayDialog("Warning !", "The Backward shader \"" + shader_to_find + "\" could not be found !\n" +
                                                        "RTFM : To create a \"Backward\" shader from any other regular one, just add a \"CULL Front\" " +
                                                        "instruction at the beginning of your original shader.\n" +
                                                        "Then, invert the back faces normals by adding a \"-\"n in the normals unpacking instruction. Ex : " +
                                                        "\"o.Normal = -UnpackNormal(tex2D(_BumpMap, IN.uv_BumpMap));\" ...",
                                                        "Ok");
                            if (read) m_Win.Close();
                        }
                        // Shader found : continue with Material setup
                        else
                        {
                            regularMaterial = mat1;

                            Debug.Log("Creating material : \"" + shader1.name + "\" ...");
                            showMaterialCreatorWindow = true;
                        }
                    }
                }
            }
            else
            {
                bool try_again = EditorUtility.DisplayDialog("Warning !", "The material named \"" + mat1.shader.name + "\" should be a regular one ...", "Try Again", "Cancel");
                if (!try_again) m_Win.Close();
            }
        }
        else
        {
            EditorUtility.DisplayDialog("Warning !", "The material named \"" + mat1.shader.name + "\" should be != null ...", "Ok");
        }
    }


    // SubWindow : Material creation from existing "Backward" shader
    public void DoMaterialCreatorWindow(int id)
    {
        matToSaveTo = (Material)EditorGUILayout.ObjectField("Target Backup Material : ", matToSaveTo, typeof(Material));
        forceShader = GUILayout.Toggle(forceShader, "Force Shader");
        if (GUILayout.Button("Proceed"))
        {
            if (matToSaveTo != null)
            {
                bool setMatOk;
                if (forceShader)
                    setMatOk = SetMaterialProperties(regularMaterial, true);
                else
                    setMatOk = SetMaterialProperties(regularMaterial, false);

                if (!setMatOk)
                {
                    EditorUtility.DisplayDialog("Error !", "Copy failed because of uncompatible shaders. \nPlease retry with \"Force Shader\" checked ...",
                                                "Ok");
                    showMaterialCreatorWindow = false;
                    return;
                }

                backwardMaterial = matToSaveTo;
                

                // Update Mats list in Renderer
                foreach (Material mat in targetRenderer.sharedMaterials)
                {
                    materials.Add(mat);
                    if (mat == regularMaterial && !MaterialAlreadyExists(backwardMaterial)) materials.Add(backwardMaterial);
                }
                targetRenderer.sharedMaterials = materials.ToArray();

                showMaterialCreatorWindow = false;
                m_Win.Close();
            }
            else
            {
                string err = "";
                if (matToSaveTo == null)
                    err = "The selected material is Null !";
                else
                    err = "The material \"" + matToSaveTo.shader.name + "\" is Null or not a \"Backward\" one !";
                EditorUtility.DisplayDialog("Warning !", err, "Ok");
            }
        }
        Repaint();
        GUI.DragWindow();
    }

    // Try setting Material properties if Source and Target Materials are Copy-Compatible
    public bool SetMaterialProperties(Material mat, bool forceShader)
    {
        if (forceShader)
        {
            string sh_name = mat.shader.name.Replace(FRONT_FACE_SUFFIX, BACK_FACE_SUFFIX);
            Shader sh = Shader.Find(sh_name);
            if (sh == null)
            {
                EditorUtility.DisplayDialog("Warning !", "The shader [" + sh_name +
                                            "] couln't be found \nand will be skipped from material copy !",
                                            "Ok");
            }
            else
                matToSaveTo.shader = sh;
        }
        if (!IsSameBasedMaterials(mat, matToSaveTo)) return false;
        matToSaveTo.CopyPropertiesFromMaterial(mat);
        return true;
    }

    // if mat is tagged as a "Regular" one
    public bool IsRegularMaterial(Material mat)
    {
        return (mat.shader.name.EndsWith(FRONT_FACE_SUFFIX));
    }

    // if mat is tagged as a "Backward" one
    public bool IsBackwardMaterial(Material mat)
    {
        return (mat.shader.name.EndsWith(BACK_FACE_SUFFIX));
    }

    // If mat1 and mat2 are design to be of the same type (means : with the same inner properties)
    public bool IsSameBasedMaterials(Material mat1, Material mat2)
    {
        string mat1name = mat1.shader.name;
        string mat2name = mat2.shader.name;
        return (mat1name.Substring(0, mat1name.LastIndexOf(' ')) == mat2name.Substring(0, mat2name.LastIndexOf(' ')));
    }

    // If mat is already present in the Renderer materials list
    public bool MaterialAlreadyExists(Material mat)
    {
        bool found = false;
        foreach (Material m in targetRenderer.sharedMaterials)
        {
            if (m == mat)
            {
                found = true;
                break;
            }
        }
        return found;
    }

    # endregion
}
