using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using Random = UnityEngine.Random;

[CreateAssetMenu(fileName = "star_map_generator", menuName = "Star map generator")]
public sealed class StarMapGenerator : ScriptableObject
{
    [SerializeField]
    private int size = 20;
    [SerializeField]
    private Vector3 scale = Vector3.one;
    [SerializeField]
    private Vector3 randomOffset = Vector3.zero;
    [SerializeField]
    private Range distanceRange = new Range();
    [SerializeField]
    private int startCredits = 1000;
    [SerializeField]
    private int startFuel = 12;
    [SerializeField]
    private int fuelCost = 2;
    [SerializeField]
    private GameParameters gameParameters = new GameParameters();
    [SerializeField]
    private TextAsset stantionNamesSource = null;
    [SerializeField]
    private TextAsset resourceCostSource = null;
    [SerializeField]
    private TextAsset stantionParamsSource = null;
    [SerializeField]
    private TextAsset modifiersSource = null;

    [SerializeField]
    private TextAsset shipsSource = null;
    [SerializeField]
    private TextAsset drivesSource = null;
    [SerializeField]
    private TextAsset tanksSource = null;
    [SerializeField]
    private TextAsset cargoModulesSource = null;
    [SerializeField]
    private TextAsset shieldsSource = null;
    [SerializeField]
    private TextAsset weaponsSource = null;

    [NonSerialized]
    private string[] stantionNames = null;
    [NonSerialized]
    private Dictionary<string, SpaceStantionParams> spaceStantionParams = null;

    [Serializable]
    private sealed class Range {
        [SerializeField] public float Min;
        [SerializeField] public float Max;

        public Range(float min = 0, float max = 1) {
            Min = min;
            Max = max;
        }
    }

    public Dictionary<string, int> ResourceCost => GetResourceCost();

    public GameParameters GameParameters => gameParameters;

    public int FuelCost => fuelCost;

    public PlayerData CreatePlayer(string name = null)
    {
        var planets = GeneratePlanets(Random.Range(8, 12));
        var navPoints = GenerateNavPoints(size, planets);
        var stantionTypes = GetStantionParams().Select(preset => new SpaceStantionTypeData() {
            Id = preset.Key,
            StockSize = preset.Value.StockSize,
            Items = preset.Value.ResourceCapacity
        }).ToArray();
        var nearestPoint = navPoints.OrderBy(pointA => pointA.Position.magnitude).First();
        var starShip = new StarShipData()
        {
            Position = new Vector3(0, 0, 50),
            Rotation = Quaternion.identity,
            HyperFuel = startFuel,
            Armor = -1, // default full armor
            Equipment = new EquipmentData(), // default equip
            Items = new Dictionary<string, int>()
        };
        return new PlayerData() {
            Name = name ?? "Misterios pilot",
            Turn = 1,
            Credits = startCredits,
            CurrentNavigationPointId = Array.IndexOf(navPoints, nearestPoint),
            StarShip = starShip,
            StarSystem = new StarSystemData() {
                NavigationPoints = navPoints,
                Planets = planets,
                StantionTypes = stantionTypes
            }
        };
    }

    private PlanetData[] GeneratePlanets(int count)
    {
        var sun = new PlanetData()
        {
            Position = Vector3.zero,
            Radius = Random.Range(0.4f, 0.5f),
            Color = Color.white
        };

        var planets = new List<PlanetData> { sun };
        const float planetDistance = 1.6f;
        for (int i = 0; i < count; i++) {
            var distance = (i + 1 + Random.Range(-0.4f, 0.4f)) * planetDistance;
            var sizeId = Random.Range(1, 5);
            var size = (sizeId + Random.Range(-0.5f, 0.5f)) * 0.04f;
            var position = Quaternion.Euler(0, Random.Range(0, 360f), 0) * (Vector3.right * distance);
            planets.Add(new PlanetData()
            {
                Position = position,
                Radius = size,
                Color = new Color(
                    Random.Range(0.5f, 1f),
                    Random.Range(0.5f, 1f),
                    Random.Range(0.5f, 1f) )
            });
            var satellitesCount = Random.Range(0, sizeId * 2);
            for (int s = 0; s < satellitesCount; s++)
            {
                planets.Add(new PlanetData()
                {
                    Position = position + Quaternion.Euler(0, Random.Range(0, 360f), 0) * (Vector3.right * ((s + 2 + Random.Range(-0.5f, 0.5f)) * 0.15f)),
                    Radius = Random.Range(1, 10) * 0.005f,
                    Color = new Color(
                        Random.Range(0.5f, 1f),
                        Random.Range(0.5f, 1f),
                        Random.Range(0.5f, 1f))
                });
            }
        }
        return planets.ToArray();
    }

    private NavigationPointData[] GenerateNavPoints(int size, PlanetData[] planets)
    {
        var navigationPoints = new List<NavigationPointData>();
        var factionCenters = GenerateRandomCenters(5, size * scale);
        var half = (size - 1) * 0.5f;
        for (int i = 0; i < size; i++)
        {
            for (int j = 0; j < size; j++)
            {
                var cellPosition = new Vector3(i - half, 0, j - half);
                if (cellPosition.magnitude > size * 0.51f)
                    continue;
                var position = cellPosition + Vector3.Scale(UnityEngine.Random.insideUnitSphere, randomOffset);
                position = Vector3.Scale(position, scale);
                // too close to planet
                if (planets.Any(planet => Vector3.Distance(planet.Position, position) <= planet.Radius)) 
                    continue; 
                navigationPoints.Add(
                    CreateNavigationPoint(
                        position,
                        FindFaction(position, factionCenters))
                    );
            }
        }
        return navigationPoints.ToArray();
    }

    private NavigationPointData CreateNavigationPoint(Vector3 position, int factionId)
    {
        // var stantionsCount = UnityEngine.Random.Range(1, 3) * UnityEngine.Random.Range(1, 2);
        var stantionsCount = 1;
        return new NavigationPointData()
        {
            FactionId = factionId,
            Position = position,
            SpaceStantions = CreateRandomStantions(stantionsCount)
        };
    }

    private SpaceStantionData[] CreateRandomStantions(int count)
    {
        var stantions = new SpaceStantionData[count];
        var stantionParams = GetStantionParams();
        var keys = stantionParams.Keys.ToArray(); // TODO replace single
        for (int i = 0; i < count; i++) {
            var position = (i == 0) ? Vector3.zero : UnityEngine.Random.onUnitSphere * UnityEngine.Random.Range(distanceRange.Min, distanceRange.Max);
            var stantionId = keys[UnityEngine.Random.Range(0, keys.Length)];
            var preset = stantionParams[stantionId];
            stantions[i] = new SpaceStantionData()
            {
                Position = position,
                Rotation = UnityEngine.Random.rotationUniform,
                Id = stantionId,
                Name = GetRandomName()
            };
        }
        return stantions;
    }

    private string GetRandomName()
    {
        if (stantionNames == null)
        {
            stantionNames = JsonConvert.DeserializeObject<string[]>(stantionNamesSource.text) ?? new string[] { "stantion" };
        }
        return stantionNames[UnityEngine.Random.Range(0, stantionNames.Length)];
    }

    public Dictionary<string, int> GetResourceCost() => JsonConvert.DeserializeObject<Dictionary<string, int>>(resourceCostSource.text) ?? new Dictionary<string, int>();

    private Dictionary<string, SpaceStantionParams> GetStantionParams()
    {
        if (spaceStantionParams == null)
        {
            Debug.Log(stantionParamsSource.text);
            spaceStantionParams = JsonConvert.DeserializeObject<Dictionary<string, SpaceStantionParams>>(stantionParamsSource.text);
        }
        return spaceStantionParams;
    }

    public Dictionary<string, ModifierParam> GetModifiersParams() => JsonConvert.DeserializeObject<Dictionary<string, ModifierParam>>(modifiersSource.text);

    public EquipmentBlueptints GetEquipmentList () {
        var ships = JsonConvert.DeserializeObject<EquipmentBlueprintParams[]>(shipsSource.text);
        var drives = JsonConvert.DeserializeObject<EquipmentBlueprintParams[]>(drivesSource.text);
        var tanks = JsonConvert.DeserializeObject<EquipmentBlueprintParams[]>(tanksSource.text);
        var cargoModules = JsonConvert.DeserializeObject<EquipmentBlueprintParams[]>(cargoModulesSource.text);
        var shields = JsonConvert.DeserializeObject<EquipmentBlueprintParams[]>(shieldsSource.text);
        var weapons = JsonConvert.DeserializeObject<EquipmentBlueprintParams[]>(weaponsSource.text);
        return new EquipmentBlueptints(ships, drives, tanks, cargoModules, shields, weapons);
    }

    private int FindFaction(Vector3 position, Vector3[] factionCenters)
    {
        var nearestIndex = -1;
        var nearestDistance = float.MaxValue;
        for (int i = 0; i < factionCenters.Length; i++) {
            var distance = Vector3.Distance(factionCenters[i], position);
            if (nearestDistance > distance) {
                nearestDistance = distance;
                nearestIndex = i;
            }
        }
        return nearestIndex;
    }

    private Vector3[] GenerateRandomCenters(int count, Vector3 maxOffset)
    {
        var factionCenters = new Vector3[count];
        for (var i = 0; i < factionCenters.Length; i++) {
            factionCenters[i] = Vector3.Scale(maxOffset, UnityEngine.Random.insideUnitSphere);
        }
        return factionCenters;
    }
}
