using System;
using System.Collections;
using System.Collections.Generic;
using System.Xml.Serialization;
using UnityEngine;
using Random = UnityEngine.Random;

public class TargetController : MonoBehaviour
{
    public GameObject EnvironmentObj;
    public GameObject AgentObj;
    public GameObject HUDObj;
    public GameObject SceneBlockContainerObj;
    public GameObject EnemyContainerObj;
    public GameObject ParameterContainerObj;
    public GameObject EnvironmentUIObj;
    public GameObject WorldUIObj;
    // area
    public GameObject edgeUp;
    public GameObject edgeDown;
    public GameObject edgeLeft;
    public GameObject edgeRight;
    public GameObject edgeAgent_Enemy;

    //group
    public string group1Tag = "Player";
    public string group2Tag = "Enemy";

    public float minEnemyAreaX;
    [System.NonSerialized] public float maxEnemyAreaX;
    [System.NonSerialized] public float minEnemyAreaZ;
    [System.NonSerialized] public float maxEnemyAreaZ;
    [System.NonSerialized] public float minAgentAreaX;
    [System.NonSerialized] public float maxAgentAreaX;
    [System.NonSerialized] public float minAgentAreaZ;
    [System.NonSerialized] public float maxAgentAreaZ;
    [System.NonSerialized] public float startTime = 0f;
    [System.NonSerialized] public float leftTime = 0f;

    [SerializeField, Range(0f, 1f)] public float attackProb = 0.2f;
    [SerializeField, Range(0f, 1f)] public float gotoProb = 0.2f;
    [SerializeField, Range(0f, 1f)] public float defenceProb = 0.2f;

    [System.NonSerialized] public int targetTypeInt;
    public float[] targetState = new float[6];
    public enum Targets { Free, Go, Attack, Defence, Num };// Num is use for get total target bumber
    public enum EndType { Win, Lose, Running, Num };
    [System.NonSerialized] public int targetNum = 0;
    private Dictionary<int, float[]> oneHotRarget = new Dictionary<int, float[]>();

    private int inArea = 0;
    private float freeProb;
    private float sceneSize;
    private float lastDistance;
    public Vector3 targetPosition;
    private Vector3 targetLocalPosition;
    private bool firstRewardFlag = true;
    private bool targetEnemySpawnFinish = false;
    private SceneBlockContainer blockCont;
    private EnemyContainer enemyCont;
    private EnvironmentUIControl envUICon;
    private ParameterContainer paramCon;
    private CharacterController agentCharaCon;
    private WorldUIController worldUICon;
    private HUDController hudCon;
    private RaySensors raySensors;

    // Start is called before the first frame update
    void Start()
    {
        minEnemyAreaX = edgeLeft.transform.localPosition.x + 1.0f;
        maxEnemyAreaX = edgeRight.transform.localPosition.x - 1.0f;
        minEnemyAreaZ = edgeAgent_Enemy.transform.localPosition.z + 1.0f;
        maxEnemyAreaZ = edgeUp.transform.localPosition.z - 1.0f;

        minAgentAreaX = edgeLeft.transform.localPosition.x + 1.0f;
        maxAgentAreaX = edgeRight.transform.localPosition.x - 1.0f;
        minAgentAreaZ = edgeDown.transform.localPosition.z + 1.0f;
        maxAgentAreaZ = edgeAgent_Enemy.transform.localPosition.z - 1.0f;

        blockCont = SceneBlockContainerObj.GetComponent<SceneBlockContainer>();
        envUICon = EnvironmentUIObj.GetComponent<EnvironmentUIControl>();
        enemyCont = EnemyContainerObj.GetComponent<EnemyContainer>();
        agentCharaCon = AgentObj.GetComponent<CharacterController>();
        paramCon = ParameterContainerObj.GetComponent<ParameterContainer>();
        worldUICon = WorldUIObj.GetComponent<WorldUIController>();
        hudCon = HUDObj.GetComponent<HUDController>();
        raySensors = AgentObj.GetComponent<RaySensors>();
        freeProb = 1 - attackProb - gotoProb - defenceProb;
        targetNum = (int)Targets.Num;
        if (freeProb < 0)
        {
            Debug.LogError("target percentage wrong");
        }

        // initialize a simple fake onehot encoder.
        for (int i = 0; i < targetNum; i++)
        {
            float[] onehotList = new float[targetNum];
            for (int j = 0; j < targetNum; j++)
            {
                onehotList[j] = 0;
            }
            onehotList[i] = 1;
            oneHotRarget.Add(i, onehotList);
        }
    }

    private void Update()
    {
        leftTime = paramCon.timeLimit - Time.time + startTime;
    }

    public void rollNewScene()
    {
        startTime = Time.time;// Reset StartTime as now time
        leftTime = paramCon.timeLimit - Time.time + startTime;
        float randTargetType = UnityEngine.Random.Range(0f, 1f);
        if (randTargetType <= gotoProb)
        {
            // goto target spawn
            Debug.Log("GOTO THIS TARGET!");
            targetTypeInt = (int)Targets.Go;
            int randBlockType = Random.Range(0, blockCont.goBlockPrefabs.Length);
            // get choosed scene size 
            sceneSize = blockCont.goBlockPrefabs[randBlockType].GetComponent<SceneBlock>().blockSize;
            float randX = UnityEngine.Random.Range(minEnemyAreaX + sceneSize/2 + 1f, maxEnemyAreaX - sceneSize/2 - 1f);
            float randZ = UnityEngine.Random.Range(minEnemyAreaZ + sceneSize/2 + 1f, maxEnemyAreaZ - sceneSize/2 - 1f);
            targetPosition = new Vector3(randX, 0f, randZ);
            // Init Agent position
            moveAgentToSpwanArea();
            // init scene block
            blockCont.destroyBlock();
            blockCont.createNewBlock(targetTypeInt, randBlockType, targetPosition, group1Tag, group2Tag);
            enemyCont.destroyAllEnemys();
            enemyCont.randomInitEnemysExcept(hudCon.enemyNum, targetPosition, sceneSize);
            blockCont.thisBlock.initBlock(EnvironmentObj);
            // set startDistance
            firstRewardFlag = true;
        }
        else if (randTargetType > gotoProb && randTargetType <= gotoProb + attackProb)
        {
            // attack target spawn
            Debug.Log("ATTACK!");
            targetTypeInt = (int)Targets.Attack;
            int randBlockType = Random.Range(0, blockCont.attackBlockPrefabs.Length);
            // get choosed scene size 
            sceneSize = blockCont.attackBlockPrefabs[randBlockType].GetComponent<SceneBlock>().blockSize;
            float randX = UnityEngine.Random.Range(minEnemyAreaX + sceneSize/2 + 1f, maxEnemyAreaX - sceneSize/2 - 1f);
            float randZ = UnityEngine.Random.Range(minEnemyAreaZ + sceneSize/2 + 1f, maxEnemyAreaZ - sceneSize/2 - 1f);
            targetPosition = new Vector3(randX, 0f, randZ);
            // Init Agent position
            moveAgentToSpwanArea();
            // init scene block
            blockCont.destroyBlock();
            blockCont.createNewBlock(targetTypeInt, randBlockType, targetPosition, group1Tag, group2Tag);
            enemyCont.destroyAllEnemys();
            enemyCont.randomInitEnemysExcept(hudCon.enemyNum, targetPosition, sceneSize);
            blockCont.thisBlock.initBlock(EnvironmentObj);
            // set startDistance
            firstRewardFlag = true;
            targetEnemySpawnFinish = false;
        }
        else if (randTargetType > gotoProb + attackProb && randTargetType <= gotoProb + attackProb + defenceProb)
        {
            // defence target spawn
            Debug.Log("DEFENCE!");
            targetTypeInt = (int)Targets.Defence;
            int randBlockType = Random.Range(0, blockCont.attackBlockPrefabs.Length);
            // get choosed scene size 
            sceneSize = blockCont.defencePrefabs[randBlockType].GetComponent<SceneBlock>().blockSize;
            float randX = UnityEngine.Random.Range(minEnemyAreaX + sceneSize/2 + 1f, maxEnemyAreaX - sceneSize/2 - 1f);
            float randZ = UnityEngine.Random.Range(minEnemyAreaZ + sceneSize/2 + 1f, maxEnemyAreaZ - sceneSize/2 - 1f);
            targetPosition = new Vector3(randX, 0f, randZ);
            // Init Agent position
            moveAgentTo(targetPosition);
            // init scene block
            blockCont.destroyBlock();
            blockCont.createNewBlock(targetTypeInt, randBlockType, targetPosition, group1Tag, group2Tag);
            enemyCont.destroyAllEnemys();
            enemyCont.randomInitEnemysExcept(hudCon.enemyNum, targetPosition, sceneSize);
            blockCont.thisBlock.initBlock(EnvironmentObj);
            // set startDistance
            firstRewardFlag = true;
        }
        else
        {
            //Debug.Log("Free");
            targetTypeInt = (int)Targets.Free;
            enemyCont.destroyAllEnemys();
            enemyCont.randomInitEnemys(hudCon.enemyNum);
            moveAgentToSpwanArea();
            blockCont.destroyBlock();
        }
        updateTargetStates();
        envUICon.updateTargetType(targetTypeInt);
    }

    // get target observation states
    public void updateTargetStates()
    {
        // targettype, x,y,z, firebasesAreaDiameter
        if (targetTypeInt == (int)Targets.Free)
        {
            targetState[0] = targetTypeInt;
            for(int i = 1; i < targetState.Length; i++)
                targetState[i] = 0f;
        }
        else
        {
            targetState[0] = targetTypeInt;
            /* targetState[1] = targetPosition.x / raySensors.viewDistance; // normalization
            targetState[2] = targetPosition.y / raySensors.viewDistance;
            targetState[3] = targetPosition.z / raySensors.viewDistance;*/
            targetState[1] = targetPosition.x; // normalization
            targetState[2] = targetPosition.y;
            targetState[3] = targetPosition.z;
            targetState[4] = blockCont.thisBlock.firebasesAreaDiameter / raySensors.viewDistance;
            targetState[5] = blockCont.thisBlock.belongRatio;
        }
    }

    // move Agent into Agent Spawn Area
    public void moveAgentToSpwanArea()
    {
        float randX = UnityEngine.Random.Range(minAgentAreaX, maxAgentAreaX); ;
        float randZ = 0f;
        if (paramCon.spawnAgentInAllMap)
        {
            // spawn agent in all around map
            randZ = UnityEngine.Random.Range(minAgentAreaZ, maxEnemyAreaZ);
        }
        else
        {
            // spawn agent in only agent spawn area
            randZ = UnityEngine.Random.Range(minAgentAreaZ, maxAgentAreaZ);
        }

        int Y = 1;
        Vector3 initAgentLoc = new Vector3(randX, Y, randZ);
        moveAgentTo(initAgentLoc);
    }

    // move Agent to this position
    public void moveAgentTo(Vector3 thisPosition)
    {
        // while using transform.localPosition to move character
        // u should turn off character Controller or it won't work
        agentCharaCon.enabled = false;
        AgentObj.transform.localPosition = thisPosition;
        agentCharaCon.enabled = true;
    }


    // check over and get rewards
    // 1 = success,2 = overtime,0 = notover
    public (int, float,float) checkOverAndRewards()
    {
        int endTypeInt = 0;
        float thisReward = 0;
        float endReward = 0;
        float nowDistance = 0f;
        switch (targetTypeInt)
        {
            case (int)Targets.Go:
                // goto
                (nowDistance, inArea) = blockCont.getAgentTargetDistanceAndInside(AgentObj.transform.position);
                envUICon.updateTargetGauge(blockCont.thisBlock.firebasesBelong, blockCont.thisBlock.belongMaxPoint);
                float areaTargetReward = getDistanceReward(nowDistance, inArea);
                //if(inArea != 0)
                if (blockCont.thisBlock.firebasesBelong >= blockCont.thisBlock.belongMaxPoint)
                {
                    // win
                    // let the area belongs to me
                    thisReward = areaTargetReward;
                    endReward = paramCon.goWinReward;
                    //thisReward = (paramCon.inAreaReward * inArea) + getDistanceReward(nowDistance);
                    endTypeInt = (int)EndType.Win;
                }
                else if (leftTime <= 0)
                {
                    // time out lose
                    thisReward = areaTargetReward;
                    endReward = paramCon.loseReward;
                    endTypeInt = (int)EndType.Lose;
                }
                else
                {
                    // keep on keeping on!
                    thisReward = areaTargetReward;
                    endReward = 0;
                    endTypeInt = (int)EndType.Running;
                }
                break;
            case (int)Targets.Attack:
                // attack
                (nowDistance, inArea) = blockCont.getAgentTargetDistanceAndInside(AgentObj.transform.position);
                envUICon.updateTargetGauge(blockCont.thisBlock.firebasesBelong, blockCont.thisBlock.belongMaxPoint);
                if (blockCont.thisBlock.getInAreaNumber(group2Tag) <= 0 && targetEnemySpawnFinish)
                {
                    // win
                    // let the area belongs to me and kill every enmy in this area.
                    thisReward = 0;
                    endReward = paramCon.attackWinReward;
                    //thisReward = (paramCon.inAreaReward * inArea) + getSceneReward(nowDistance);
                    endTypeInt = (int)EndType.Win;
                }
                else if (leftTime <= 0 && targetEnemySpawnFinish)
                {
                    // time out lose
                    thisReward = 0;
                    endReward = paramCon.loseReward;
                    //thisReward = (paramCon.inAreaReward * inArea) + getSceneReward(nowDistance);
                    endTypeInt = (int)EndType.Lose;
                }
                else
                {
                    // keep on keeping on!
                    // thisReward = (paramCon.inAreaReward * inArea) + getDistanceReward(nowDistance);
                    thisReward = 0;
                    endReward = 0;
                    targetEnemySpawnFinish = true;
                    endTypeInt = (int)EndType.Running;
                }
                break;
            case (int)Targets.Defence:
                //defence
                // !!! DIDN't FINISH!!!
                (nowDistance, inArea) = blockCont.getAgentTargetDistanceAndInside(AgentObj.transform.position);
                envUICon.updateTargetGauge(blockCont.thisBlock.firebasesBelong, blockCont.thisBlock.belongMaxPoint);
                if (leftTime <= 0 && blockCont.thisBlock.firebasesBelong >= 0f)
                {
                    // win
                    // time over and the area still mine
                    thisReward = paramCon.defenceWinReward;
                    //thisReward = (paramCon.inAreaReward * inArea) + getSceneReward(nowDistance);
                    endTypeInt = (int)EndType.Win;
                }
                else if (blockCont.thisBlock.firebasesBelong <= blockCont.thisBlock.belongMaxPoint)
                {
                    // lost area lose
                    thisReward = paramCon.loseReward;
                    //thisReward = (paramCon.inAreaReward * inArea) + getSceneReward(nowDistance);
                    endTypeInt = (int)EndType.Lose;
                }
                else
                {
                    // keep on keeping on!
                    // thisReward = (paramCon.inAreaReward * inArea) + getDistanceReward(nowDistance);
                    endTypeInt = (int)EndType.Running;
                }
                break;
            default:
                //free kill
                if (EnemyContainerObj.transform.childCount <= 0)
                {
                    // win
                    //thisReward = paramCon.winReward + (paramCon.timeBonusPerSecReward * leftTime);
                    thisReward = 0;
                    endReward = paramCon.freeWinReward;
                    endTypeInt = (int)EndType.Win;
                }
                else if (Time.time - startTime >= paramCon.timeLimit)
                {
                    // lose
                    //thisReward = paramCon.loseReward;
                    thisReward = 0;
                    endReward = paramCon.loseReward;
                    endTypeInt = (int)EndType.Lose;
                }
                else
                {
                    // keep on keeping on!
                    thisReward = 0;
                    endReward = 0;
                    endTypeInt = (int)EndType.Running;
                }
                break;
        }
        envUICon.showResult(endTypeInt);
        worldUICon.updateChart(targetTypeInt, endTypeInt);
        return (endTypeInt, thisReward,endReward);
    }

    // caulculate sceneReward if close to target then get great reward
    public float getDistanceReward(float nowDistance,int inarea)
    {
        if (firstRewardFlag)
        {
            // first distance record
            (lastDistance, _) = blockCont.getAgentTargetDistanceAndInside(AgentObj.transform.position);
            firstRewardFlag = false;
        }
        float thisSceneReward = 0f;
        if (inarea != 0)
        {
            // in area
            thisSceneReward = paramCon.inAreaReward;
        }
        else
        {
            // out of area
            // thisSceneReward = paramCon.distanceReward * Math.Clamp(lastDistance - nowDistance, 0, 100);
            thisSceneReward = paramCon.distanceReward * (lastDistance - nowDistance);
        }
        lastDistance = nowDistance;
        return thisSceneReward;
    }

    // calculate kill reward base on killed enemy's position
    public float killReward(Vector3 enemyPosition)
    {
        float thisKillReward = 0f;
        if (targetTypeInt == (int)Targets.Attack)
        {
            // attack mode
            (_, int isInArea) = blockCont.thisBlock.getDist_inArea(enemyPosition);
            if (isInArea == 1)
            {
                // kill in area enemy
                thisKillReward = paramCon.killTargetEnemyReward;
            }
            else
            {
                thisKillReward = paramCon.killReward;
            }
        }
        else if (targetTypeInt == (int)Targets.Free)
        {
            // free mode hit
            thisKillReward = paramCon.killTargetEnemyReward;
        }
        else
        {
            // goto & defence
            thisKillReward = paramCon.killReward;
        }
        return thisKillReward;
    }

    // calculate hit reward base on killed enemy's position and now mode
    public float hitEnemyReward(Vector3 enemyPosition)
    {
        float thisHitReward = 0f;
        if (targetTypeInt == (int)Targets.Attack)
        {
            // attack mode
            (_, int isInArea) = blockCont.thisBlock.getDist_inArea(enemyPosition);
            if (isInArea == 1)
            {
                // hit in area enemy
                thisHitReward = paramCon.hitTargetReward;
            }
            else
            {
                // hit not in area enemy
                thisHitReward = paramCon.hitReward;
            }
        }
        else if (targetTypeInt == (int)Targets.Free)
        {
            // free mode hit
            thisHitReward = paramCon.hitTargetReward;
        }
        else
        {
            // goto & defence
            thisHitReward = paramCon.hitReward;
        }
        return thisHitReward;
    }

    // get in area state
    public int getInAreaState()
    {
        if(targetTypeInt == (int)Targets.Go)
        {
            return inArea;
        }
        else
        {
            return 0;
        }
    }
}
