using System;
using System.Collections;
using System.Collections.Generic;
using Orbiter.Interfaces;

namespace Orbiter.Multiplayer
{
    public class VisibilityCluster<T> where T: class
    {
        private class Arguments<T>
        {
            private SpaceTime spaceTime;
            private uint scale;
            private Coordinates position;
            private T payload;

            public Arguments(T payload, uint scale, SpaceTime spaceTime, Coordinates position)
            {
                this.spaceTime = spaceTime;
                this.scale = scale;
                this.position = position;
                this.payload = payload;
            }

            public SpaceTime SpaceTime
            {
                get { return spaceTime; }
            }

            public uint Scale
            {
                get { return scale; }
            }

            public Coordinates Position
            {
                get { return position; }
            }

            public T Payload
            {
                get { return payload; }
            }
        }

        private class Coordinates
        {
            private long x;
            private long y;
            private long z;

            public Coordinates(long x, long y, long z)
            {
                this.x = x;
                this.y = y;
                this.z = z;
            }

            public Coordinates(Vector3 vector)
            {
                x = (long)vector.X;
                y = (long)vector.Y;
                z = (long)vector.Z;
            }

            public override int GetHashCode()
            {
                return x.GetHashCode() ^ y.GetHashCode() ^ z.GetHashCode();
            }

            public override bool Equals(object obj)
            {
                Coordinates other = obj as Coordinates;
                if (other == null) return false;
                if (x != other.x) return false;
                if (y != other.y) return false;
                if (z != other.z) return false;
                return true;
            }

            public static Coordinates operator >>(Coordinates left, int right)
            {
                return new Coordinates(left.x >> right, left.y >> right, left.z >> right);
            }

            public static Coordinates operator +(Coordinates left, Coordinates right)
            {
                return new Coordinates(left.x + right.x, left.y + right.y, left.z + right.z);
            }
        }
        
        private class Iteration
        {
            public int Scale=0;
            private int i=-1;
            private int j=-1;
            private int k=-1;
            public int Neighbour=0;
            
            public int I
            {
                get{return i;}
                set{if (value > 1) i = -1;}
            }

            public int J
            {
                get { return j; }
                set { if (value > 1) j = -1; }
            }

            public int K
            {
                get { return k; }
                set { if (value > 1) k = -1; }
            }
        }
        
        Dictionary<SpaceTime, Dictionary<uint, Dictionary<Coordinates, List<T>>>> toWhomImInRange=new Dictionary<SpaceTime, Dictionary<uint, Dictionary<Coordinates, List<T>>>>();
        Dictionary<SpaceTime, Dictionary<uint, Dictionary<Coordinates, List<T>>>> whoIsInRange= new Dictionary<SpaceTime, Dictionary<uint, Dictionary<Coordinates, List<T>>>>();
        Dictionary<T, Arguments<T>> members= new Dictionary<T, Arguments<T>>();
        Dictionary<T, Iteration> iterations=new Dictionary<T, Iteration>();
        private double subVisualDistance;
        
        public VisibilityCluster(uint verticalPixels, double fieldOfView)
        {
            subVisualDistance = 0.25*verticalPixels/Math.Tan(Math.PI*fieldOfView/360);
        }

        public VisibilityCluster(double subVisualDistance)
        {
            this.subVisualDistance = subVisualDistance;
        }
        
        public void Set(T payload, uint logarithmicScale, SpaceTime spaceTime, Vector3 position)
        {
            //Create visibility arguments and check state
            Vector3 svdPosition = position / subVisualDistance;
            Arguments<T> args = new Arguments<T>(payload, logarithmicScale, spaceTime, new Coordinates(svdPosition));
            if (members.ContainsKey(payload))
            {
                if (members[payload].Position.Equals(args.Position)) return; //No need for update
                InternalRemove(payload);
            }
            Coordinates coord = args.Position >> (int)logarithmicScale;
            
            //Add to visible-neighbours list
            Dictionary<uint, Dictionary<Coordinates, List<T>>> gBodyList=null;
            if (whoIsInRange.ContainsKey(spaceTime)) gBodyList=whoIsInRange[spaceTime];
            if (gBodyList==null) whoIsInRange.Add(spaceTime, gBodyList = new Dictionary<uint, Dictionary<Coordinates, List<T>>>());
            Dictionary<Coordinates, List<T>> scaleList = null;
            if (gBodyList.ContainsKey(logarithmicScale)) scaleList = gBodyList[logarithmicScale];
            if (scaleList == null) gBodyList.Add(logarithmicScale, scaleList = new Dictionary<Coordinates, List<T>>());
            List<T> coordinatesList = null;
            if (scaleList.ContainsKey(coord)) coordinatesList = scaleList[coord];
            if (coordinatesList==null) scaleList.Add(coord, coordinatesList = new List<T>());
            coordinatesList.Add(payload);

            //Add to observing-neighbours list
            gBodyList = null;
            if (toWhomImInRange.ContainsKey(spaceTime)) gBodyList = toWhomImInRange[spaceTime];
            if (gBodyList == null) toWhomImInRange.Add(spaceTime, gBodyList = new Dictionary<uint, Dictionary<Coordinates, List<T>>>());
            scaleList = null;
            if (gBodyList.ContainsKey(logarithmicScale)) scaleList = gBodyList[logarithmicScale];
            //Add scale level if necessary
            if (scaleList == null)
            {
                scaleList = new Dictionary<Coordinates, List<T>>();
                foreach (KeyValuePair<T, Arguments<T>> pair in members)
                {
                    coord = pair.Value.Position >> (int)logarithmicScale;
                    coordinatesList = null;
                    if (scaleList.ContainsKey(coord)) coordinatesList = scaleList[coord];
                    if (coordinatesList == null) scaleList.Add(coord, coordinatesList = new List<T>());
                    coordinatesList.Add(pair.Key);
                }
                gBodyList.Add(logarithmicScale, scaleList);
            }
            //Add payload to each scale level
            foreach (KeyValuePair<uint, Dictionary<Coordinates, List<T>>> pair in gBodyList)
            {
                coord = args.Position >> (int)pair.Key;
                coordinatesList = null;
                if (pair.Value.ContainsKey(coord)) coordinatesList = pair.Value[coord];
                if (coordinatesList == null) pair.Value.Add(coord, coordinatesList = new List<T>());
                coordinatesList.Add(payload);
            }
            
            //Add to member list
            members.Add(payload, args);
            
            //Add to iteration list
            if (!iterations.ContainsKey(payload)) iterations.Add(payload, new Iteration());
        }
        
        private void InternalRemove(T payload)
        {
            if (!members.ContainsKey(payload))
                throw new VisibilityClusterException("Can't remove payload \"" + payload.ToString() +
                                                     "\" from visibility cluster. No member!");
            Arguments<T> args = members[payload];
            if (!whoIsInRange[args.SpaceTime][args.Scale][args.Position >> (int)args.Scale].Remove(payload))
                throw new VisibilityClusterException("Can't remove payload \"" + payload.ToString() +
                                                     "\" from visibility cluster. Not found in visible-neighbours list!");
            foreach (KeyValuePair<uint, Dictionary<Coordinates, List<T>>> pair in toWhomImInRange[args.SpaceTime])
            {
                if (!pair.Value[args.Position >> (int)pair.Key].Remove(payload))
                    throw new VisibilityClusterException("Can't remove payload \"" + payload.ToString() +
                                                     "\" from visibility cluster. Not found in observing-neighbours list at index "+pair.Key+"!");
            }
            members.Remove(payload);
        }
        
        public void Remove(T payload)
        {
            if (iterations.ContainsKey(payload)) iterations.Remove(payload);
            InternalRemove(payload);
        }
        
        public T NextVisibleNeighbour(T payload)
        {
            if (!members.ContainsKey(payload)) return null;
            Arguments<T> args = members[payload];
            Iteration it = iterations[payload];
            Dictionary<uint, Dictionary<Coordinates, List<T>>> gBodyList = whoIsInRange[args.SpaceTime];
            if (it.Scale >= gBodyList.Count) it.Scale = 0;
            uint scale = 0;
            foreach (KeyValuePair<uint, Dictionary<Coordinates, List<T>>> pair in gBodyList)
            {
                if (scale++ < it.Scale) continue;
                Coordinates pos = args.Position >> (int)pair.Key;
                List<T> list = pair.Value[pos + new Coordinates(it.I++, it.J++, it.K++)];
                if (it.Neighbour >= list.Count) it.Neighbour = 0;
                if (list.Count > 0) return list[it.Neighbour++];
                break;
            }
            return null;
        }

        public T NextObservingNeighbour(T payload)
        {
            if (!members.ContainsKey(payload)) return null;
            Arguments<T> args = members[payload];
            Iteration iteration = iterations[payload];
            iteration.Scale = 0;
            Coordinates pos = args.Position >> (int)args.Scale;
            List<T> list = toWhomImInRange[args.SpaceTime][args.Scale][pos + new Coordinates(iteration.I++, iteration.J++, iteration.K++)];
            if (iteration.Neighbour >= list.Count) iteration.Neighbour = 0;
            if (list.Count > 0) return list[iteration.Neighbour++];
            return null;
        }
        
        public IEnumerable<T> VisibleNeighbours(T payload)
        {
            if (!members.ContainsKey(payload)) yield break;
            Arguments<T> args = members[payload];
            foreach (KeyValuePair<uint, Dictionary<Coordinates, List<T>>> pair in whoIsInRange[args.SpaceTime])
            {
                Coordinates pos = args.Position >> (int)pair.Key;
                for (int i = -1; i < 2; i++)
                    for (int j = -1; j < 2; j++)
                        for (int k = -1; k < 2; k++)
                        {
                            Coordinates neighbourhood = pos + new Coordinates(i, j, k);
                            if (pair.Value.ContainsKey(neighbourhood))
                                foreach (T t in pair.Value[neighbourhood])
                                    if (t != payload) yield return t;
                        }
            }
            yield break;
        }
        
        public IEnumerable<T> ObservingNeighbours(T payload)
        {
            if (!members.ContainsKey(payload)) yield break;
            var args = members[payload];
            var pos = args.Position >> (int) args.Scale;
            var scaleList = toWhomImInRange[args.SpaceTime][args.Scale];
            for (var i = -1; i < 2; i++) for (var j = -1; j < 2; j++) for (var k = -1; k < 2; k++)
            {
                var neighbourhood = pos + new Coordinates(i, j, k);
                if (!scaleList.ContainsKey(neighbourhood)) continue;
                foreach (var t in scaleList[neighbourhood]) if (t != payload) yield return t;
            }
        }
    }
}
