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

public class MaterialDrainValue : MonoBehaviour
{
    // Accumulator that ensures each material in a scene has a unique index number
    static int s_currentIndex;
    
    // Unique index of this material drain instance
    public int Index { get; private set; }
    
    // Returns true if our depletion value is at or above 1
    public bool IsDepleted { get { return m_depletionPerc >= 1f; } }
    [Range(0, 1)] private float m_depletionPerc = 0f;

    private float m_totalEnergy;
    private float m_objVolume = 0;
    private float m_cubedRoot;
    
	private Material[] m_objMats, m_energyMats;
	private List<Color> m_objColors;

    public enum DrainType
    {
        ColorChanger,
        Tinkercad,
        Llama
    }
    private DrainType m_drainType;

    private Build m_buildScript;

    [Header("Debug")]
    [SerializeField] bool enableDebugMessages = false;
    
    void Start ()
    {
        // !!! --- DEVELOPER NOTE --- !!!
        // ColorChanger components are initialized in Awake to enforce initialization order
        // The order should always be ColorChanger first, THEN MaterialDrainValue

        // Set the index to the global accumulator value, increasing it by one
        Index = s_currentIndex++;

        // Differentiate our setup procedures per our components
        // If we find none of these components, default to color changer
        if (GetComponent<Tinkercad>())
            m_drainType = DrainType.Tinkercad;
        else if (GetComponent<ColorChanger>())
            m_drainType = DrainType.ColorChanger;
        else if (GetComponent<Llama>())
            m_drainType = DrainType.Llama;
        else
            m_drainType = DrainType.ColorChanger;

        // Set up our material drain values according to the drain type
        switch (m_drainType)
        {
            case (DrainType.ColorChanger):
                ColorChangerSetUp();
                break;
            case (DrainType.Tinkercad):
                TinkerSetUp();
                break;
            case (DrainType.Llama):
                LlamaSetUp();
                break;
        }

        // Get the build manager
        GameObject _bm = GameObject.FindGameObjectWithTag("Build Manager");

        // If none was found, return
        if (!_bm)
        {
            if (enableDebugMessages)
                Debug.Log("No Build Manager Found");
            return;
        }

        // Otherwise, get the Build component and store it in our reference field
        m_buildScript = _bm.GetComponent<Build>();
    }

    /// <summary>
    /// Static method that resets the material index accumulator to zero
    /// </summary>
    public static void ResetCurrentMaterialIndex()
    {
        s_currentIndex = 0;
    }

    /// <summary>
    /// Returns the material drain value instance with the given index value
    /// </summary>
    /// <param name="_index"></param>
    /// <returns></returns>
    public static MaterialDrainValue GetMaterialDrainValue(int _index)
    {
        // Get all material drain values in the scene
        MaterialDrainValue[] _materialDrainValues = Resources.FindObjectsOfTypeAll<MaterialDrainValue>();

        // Find the instance that matches our index and return it
        for(int i = 0; i < _materialDrainValues.Length; i++)
            if (_materialDrainValues[i].Index == _index)
                return _materialDrainValues[i];

        // Otherwise, return null
        return null;
    }

    /// <summary>
    /// Set up tinkercad defaults
    /// </summary>
	void TinkerSetUp ()
    {
		m_objMats = new Material[GetComponent<Tinkercad> ().rends.Length];
		m_energyMats = new Material[GetComponent<Tinkercad> ().rends.Length];
		m_objColors = new List<Color> ();
		m_objColors.Clear ();
		Vector3 lhw = Vector3.one;
		bool gotVolume = false;

		for (int i = 0; i < GetComponent<Tinkercad> ().rends.Length; i++) {
			m_objMats [i] = GetComponent<Tinkercad> ().rends [i].material;
			m_energyMats[i] = GetComponent<Tinkercad> ().rends [i].sharedMaterial;
			m_objColors.Add (GetComponent <Tinkercad> ().totalColors [i]);
			if (gotVolume)
				continue;
			for (int c = 0; c < transform.childCount; c++) {
				if (transform.GetChild (i) != transform.GetChild (c) && transform.GetChild (i).GetComponent<MeshRenderer> () && transform.GetChild (c).GetComponent<MeshRenderer> ()) {
					if (transform.GetChild (i).GetComponent<MeshRenderer> ().bounds.size == transform.GetChild (c).GetComponent<MeshRenderer> ().bounds.size) {
						lhw = transform.GetChild (i).GetComponent<MeshRenderer> ().bounds.size;
						gotVolume = true;
						break;
					}
				}
			}
		}

		m_objVolume = lhw.x * lhw.y * lhw.z;
		m_cubedRoot = Mathf.Pow (m_objVolume, 1f / 3f);
		m_totalEnergy = ((69f / 1672f) * Mathf.Pow (m_cubedRoot, 2)) + ((1467f / 1672f) * m_cubedRoot) - (175f / 418f);
	}

    /// <summary>
    /// Set up Color Changer defaults
    /// </summary>
	void ColorChangerSetUp ()
    {
		if (!GetComponent<MeshRenderer> ())
			return;

		m_objMats = GetComponent<MeshRenderer> ().materials;
		m_energyMats = GetComponent<MeshRenderer> ().sharedMaterials;
		m_objColors = new List<Color> ();
		m_objColors.Clear ();
		int matInt = 0;
		foreach (Material mat in m_objMats) {
			if (mat.shader != Shader.Find ("Custom/BattleRoyaleDefault")) {
				mat.shader = Shader.Find ("Custom/BattleRoyaleDefault");
			}

			if (!GetComponent <ColorChanger> ()) {
				m_objColors.Add (mat.color);
			} else {
				m_objColors.Add (GetComponent <ColorChanger> ().totalColors [matInt]);
			}
			matInt++;
		}

		Vector3 lhw = GetComponent<MeshRenderer> ().bounds.size;
		m_objVolume = lhw.x * lhw.y * lhw.z;
		m_cubedRoot = Mathf.Pow (m_objVolume, 1f / 3f);
		m_totalEnergy = ((69f / 1672f) * Mathf.Pow (m_cubedRoot, 2)) + ((1467f / 1672f) * m_cubedRoot) - (175f / 418f);
	}

    /// <summary>
    /// Set up Llama defaults
    /// </summary>
	void LlamaSetUp ()
    {
		m_objMats = GetComponent<MeshRenderer> ().materials;

		m_energyMats = GetComponent<MeshRenderer> ().sharedMaterials;

		m_objColors = new List<Color> ();

		m_objColors.Clear ();

		m_objColors.Add (GetComponent<MeshRenderer> ().material.color);

		m_totalEnergy = 250f;
	}

    /// <summary>
    /// Returns true if the given player is able to drain from this object
    /// </summary>
    /// <param name="_drainingPlayer"></param>
    /// <returns></returns>
	public bool TryDrain (PlayerBase _drainingPlayer)
    {
        // If the material is depleted or the player trying to drain is full, return false
		if (IsDepleted || _drainingPlayer.gainedEnergy + _drainingPlayer.BuildEnergies.TotalEnergy () >= 500)
            return false;
        
        // Old object volume behavior
		//if (objVolume <= 1f)
		//	gScale += drainRate / 1000 / 0.25f;// * Time.deltaTime;
		//else
		//	gScale += drainRate / 1000 / Mathf.Log(objVolume);// * Time.deltaTime;

        // Applies a multiplier in the amount of energy drained per frame if this object is a llama
		float _drainMultiplier = (m_drainType == DrainType.Llama) ? 15f : 5f;

        // Calculate our change in energy as a percentage of our maximum energy
		float _energyDelta = (Time.deltaTime * _drainMultiplier) / m_totalEnergy;

        if (enableDebugMessages)
            Debug.Log("Energy drained this frame is " + _energyDelta.ToString());

        // Notify our build script that we are updating the grayscale of an object for network serialization
		if (m_buildScript)
			m_buildScript.AddGrayScaleUpdate (Index, _energyDelta);

        // Accrue energy gained by the player by the frame time and our drain multiplier
		_drainingPlayer.gainedEnergy += (Time.deltaTime * _drainMultiplier);

        // If the gained energy value is somehow below zero, clamp it to zero
		if (_drainingPlayer.gainedEnergy < 0)
			_drainingPlayer.gainedEnergy = 0;

        // Update drain values
        HandleDrain(_energyDelta);

		return true;
	}

    /// <summary>
    /// Updates drain-related values
    /// </summary>
    /// <param name="_drainPerc"></param>
    public void HandleDrain(float _drainPerc)
    {
        // Accrue value to our depletion percentage
        // If the value would exceed 1, clamp to 1
        m_depletionPerc = m_depletionPerc + _drainPerc > 1 ? 1 : m_depletionPerc + _drainPerc;

        if (enableDebugMessages)
            Debug.Log("Material is " + m_depletionPerc.ToString() + " percent drained!");

        // For each material in our material list, set the grayscale value to our depletion percentage
        foreach (Material mat in m_objMats)
            if (mat)
                mat.SetFloat("_Grayscale", m_depletionPerc);
    }

    /// <summary>
    /// Adds the total drained energy to the player's energy pool
    /// </summary>
    /// <param name="_receivingPlayer"></param>
	public void FinalizeEnergyDrain (PlayerBase _receivingPlayer)
    {
        // Set the energy gained by the player divided by the number of object colors
		_receivingPlayer.gainedEnergy = _receivingPlayer.gainedEnergy / m_objColors.Count;

        // Get the number of build energies in the player instance's list
        int _playerEnergies = _receivingPlayer.BuildEnergies.list.Count;

        // If there are no player energies, instantiate one for each object color
        if (_playerEnergies == 0)
			foreach (Color col in m_objColors)
				_receivingPlayer.BuildEnergies.list.Add (new Energy (col, _receivingPlayer.gainedEnergy));

        // If there is at least one player energy...
        if (_playerEnergies > 0)
            // For each object color...
			for (int c = 0; c < m_objColors.Count; c++)
            {
                // Create a temp value
				bool exists = false;

                // For each player energy...
                for (int i = 0; i < _playerEnergies; i++)
                    // If that player's energy color matches our object color...
                    if (_receivingPlayer.BuildEnergies.list[i].color == m_objColors[c])
                    {
                        // Add energy to that list entry
                        _receivingPlayer.BuildEnergies.list[i].AddEnergy(_receivingPlayer.gainedEnergy);

                        // Update the temp value
                        exists = true;

                        // End the loop
                        break;
                    }

                // If we did not find a matching player build energy list entry, add one!
				if(!exists)
					_receivingPlayer.BuildEnergies.list.Add (new Energy (m_objColors[c], _receivingPlayer.gainedEnergy));
			}
        
        // Reset the gained energy amount
		_receivingPlayer.gainedEnergy = 0f;
	}

    // DEVELOPER NOTE - Keep as reference for now

    //public void GetEnergyOld (PlayerBase player) {

    //	player.gainedEnergy = player.gainedEnergy / (float)energyMats.Length;

    //	if (player.buildEnergies.list.Count > 0) {
    //		int playerEnergies = player.buildEnergies.list.Count;
    //		for (int m = 0; m < energyMats.Length; m++) {
    //			bool exists = false;
    //			for (int i = 0; i < playerEnergies; i++) {
    //				if (player.buildEnergies.list[i].material.name == energyMats[m].name) {
    //					player.buildEnergies.list[i].AddEnergy (player.gainedEnergy);
    //					exists = true;
    //					break;
    //				} else {
    //					//Debug.Log (player.buildEnergies.list[i].material.name + ", " + energyMats[m].name);
    //				}
    //			}
    //			if(!exists)
    //				player.buildEnergies.list.Add (new Energy (energyMats[m], player.gainedEnergy));
    //		}
    //	} else {
    //		foreach (Material mat in energyMats)
    //			player.buildEnergies.list.Add (new Energy (mat, player.gainedEnergy));
    //	}

    //	player.gainedEnergy = 0f;
    //}
}

/// <summary>
/// Base class for storing energy.
/// </summary>
[System.Serializable]
public class Energy {
	[ReadOnly]
	public Material material;
	[ReadOnly]
	public Color color;
	[ReadOnly]
	public float amount;


	/// <summary>
	/// Creates new energy based on a material (mat), color (col), and float value (val).
	/// </summary>
	public Energy (Material mat, Color col, float val){
		material = mat;
		amount = val;
		color = col;
	}

	/// <summary>
	/// Creates new energy based on a material (mat) and float value (val).
	/// </summary>
	public Energy (Material mat, float val){
		material = mat;
		amount = val;
	}

	/// <summary>
	/// Creates new energy based on a color (col) and float value (val).
	/// </summary>
	public Energy (Color col, float val){
		color = col;
		amount = val;
	}

	/// <summary>
	/// adds more value (val) to an existing energy
	/// </summary>
	public void AddEnergy (float val){
		amount += val;
	}

    /// <summary>
	/// subtracts value (val) from an existing energy
	/// </summary>
    public void SubtractEnergy(float val) {
        amount -= val;
    }
}

[System.Serializable]
public class EnergyList {
	[ReadOnly]
	public List<Energy> list;

	public float TotalEnergy () {
		float totalEnergy = 0;
		for (int i = 0; i < list.Count; i++) {
			totalEnergy += list [i].amount;
		}
		return totalEnergy;
	}

    public bool HasEnoughEnergy(float required) {
        float total = 0f;
        for(int i = 0; i < list.Count; i++) {
            total += list[i].amount;
            if (total > required)
                return true;
        }
        return false;
    }

    public void SubtractFromList(float val) {
        for(int i = list.Count - 1; i >= 0; i--) {
            if(list[i].amount > val) {
                list[i].SubtractEnergy(val);
                val = 0;
                if (list[i].amount <= 0)
                    list.RemoveAt(i);
                return;
            }
            
            float remainder = val - list[i].amount;
            list[i].SubtractEnergy(list[i].amount);
            val = remainder;
            list.RemoveAt(i);
        }
    }
}
