/*
 * Aphelion
 * Copyright (c) 2013  Joris van der Wel
 * 
 * This file is part of Aphelion
 * 
 * Aphelion is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, version 3 of the License.
 * 
 * Aphelion is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU Affero General Public License
 * along with Aphelion.  If not, see <http://www.gnu.org/licenses/>.
 * 
 * In addition, the following supplemental terms apply, based on section 7 of
 * the GNU Affero General Public License (version 3):
 * a) Preservation of all legal notices and author attributions
 * b) Prohibition of misrepresentation of the origin of this material, and
 * modified versions are required to be marked in reasonable ways as
 * different from the original version (for example by appending a copyright notice).
 * 
 * Linking this library statically or dynamically with other modules is making a
 * combined work based on this library. Thus, the terms and conditions of the
 * GNU Affero General Public License cover the whole combination.
 * 
 * As a special exception, the copyright holders of this library give you 
 * permission to link this library with independent modules to produce an 
 * executable, regardless of the license terms of these independent modules,
 * and to copy and distribute the resulting executable under terms of your 
 * choice, provided that you also meet, for each linked independent module,
 * the terms and conditions of the license of that module. An independent
 * module is a module which is not derived from or based on this library.
 */

package aphelion.shared.physics.entities;

import aphelion.shared.swissarmyknife.EntityGridEntity;
import aphelion.shared.swissarmyknife.EntityGrid;
import aphelion.shared.gameconfig.GCInteger;
import aphelion.shared.physics.PhysicsMap;
import aphelion.shared.physics.State;
import aphelion.shared.physics.valueobjects.*;
import aphelion.shared.swissarmyknife.LinkedListEntry;
import aphelion.shared.swissarmyknife.SwissArmyKnife;

/**
 *
 * @author Joris
 */
public abstract class MapEntity implements EntityGridEntity
{
        public long createdAt_tick;
        private boolean removed = false;
        /** valid if removed = true */
        public long removedAt_tick;
        
        public final LinkedListEntry<MapEntity> dirtyPositionPathLink_state = new LinkedListEntry<>(this);
        
        /** Tracks for which ticks the position path needs reexecuting.
         * Any dirty tick will need to reexecute dead reckon.
         * This is consistent as long as everything is received in-order (otherwise a timewarp is needed)
         */
        public final SequentialDirtyTracker dirtyPositionPathTracker = new SequentialDirtyTracker();
        
        /** Set if this entity was removed during State.resetTo(). 
         * It is very likely this entity will be reused if this attribute is set.
         * This attribute is used for assertions and should not be modified in MapEntity.resetTo
         */
        public boolean removedDuringReset = false;
        
        public final State state;
        public final MapEntity[] crossStateList;
        /** This attribute must only be accessed by EntityGrid. */
        public final LinkedListEntry<MapEntity> entityGridEntry = new LinkedListEntry<>(this);
        
        public final int HISTORY_LENGTH;
        /** The most recent position. 
         * Most methods work on this value, after which it is added to the posHistory.
         */
        public final PhysicsPositionVector pos = new PhysicsPositionVector();
        /** The history of all positions of this entity. */
        public final PhysicsPointHistoryDetailed posHistory;
        /** The history of all velocities of this entity. */
        public final PhysicsPointHistory velHistory;
        
        /** The radius in pixels * 1024 (not diameter) of the object. The radius may be 0 if it has no
         * diameter, like a bullet.
         */
        public GCInteger radius;

        //public long debug_id;
        //public long debug_id_with_reset;
        MapEntity(State state, MapEntity[] crossStateList, long createdAt_tick, int historyLength)
        {
                //this.debug_id = ++state.env.debug_entities;
                //this.debug_id_with_reset = debug_id;
                this.state = state;
                this.crossStateList = crossStateList;
                crossStateList[state.id] = this;
                this.createdAt_tick = createdAt_tick;
                this.HISTORY_LENGTH = historyLength;
                this.posHistory = new PhysicsPointHistoryDetailed(createdAt_tick, HISTORY_LENGTH);
                assert posHistory.HISTORY_LENGTH == HISTORY_LENGTH;
                this.velHistory = new PhysicsPointHistoryDetailed(createdAt_tick, HISTORY_LENGTH);
                assert velHistory.HISTORY_LENGTH == HISTORY_LENGTH;
                dirtyPositionPathTracker.setFirstDirtyTick(createdAt_tick+1);
        }

        /**
         * Remove the entity from all lists. This is called during the ticking loop after as the result of a soft remove.
         * @param tick
         */
        public void hardRemove(long tick)
        {
                this.softRemove(tick);
                
                crossStateList[state.id] = null;
                dirtyPositionPathLink_state.remove();
        }

        /**
         * Mark the entity for removal, but do not really remove it from any list yet.
         * The ticking loop will execute a hardRemove() when this entity has been removed in all states.
         * This ensures timewarps will work properly
         * @param tick
         */
        public void softRemove(long tick)
        {
                if (removed)
                {
                        if (removedAt_tick > tick)
                        {
                                removedAt_tick = tick;
                        }
                }
                else
                {
                        removedAt_tick = tick;
                }
                
                removed = true;
        }

        /**
         * Has the entity been removed at the given tick?.
         * This method does not check createdAt_tick, use isNonExistent() for that
         * @param tick The tick to check by
         * @return true if the entity has been removed
         */
        public boolean isRemoved(long tick)
        {
                if (this.removed)
                {
                        return this.removedAt_tick <= tick;
                }

                return false;
        }

        /**
         * Has this entity been scheduled for removal (regardless of the tick)
         * @return true if the entity has been scheduled for removal
         */
        public boolean isRemovedSet()
        {
                return this.removed;
        }

        /** Does this entity not yet, or no longer exist at the tick of the state this entity belongs to?.
         * An entity is considered nonexistent before its creation (this.createdAt_tick) and
         * after its (soft) removal (this.removedAt_tick)
         * @return true if the entity is nonexistent
         */
        public boolean isNonExistent()
        {
                return this.isNonExistent(this.state.tick_now);
        }

        /** Does this entity not yet, or no longer exist at the specified tick?.
         * An entity is considered nonexistent before its creation (this.createdAt_tick) and
         * after its (soft) removal (this.removedAt_tick)
         * @param tick The tick to check by
         * @return true if the entity is nonexistent
         */
        public boolean isNonExistent(long tick)
        {
                if (this.isRemoved(tick))
                {
                        return true;
                }
                
                return tick < this.createdAt_tick;
        }

        /**
         * Mark that the historic position at the given tick is "dirty".
         * The position will be rolled back to the tick before this tick and resimulated, but only for this entity.
         * The effect is not instant, it is deferred to the next ticked loop.
         * @param dirtyTick The tick that is dirty
         */
        public void markDirtyPositionPath(long dirtyTick)
        {
                dirtyTick = Math.max(this.createdAt_tick, dirtyTick);
                dirtyPositionPathTracker.markDirty(dirtyTick);

                if (dirtyPositionPathLink_state.head == null)
                {
                        state.dirtyPositionPathList.append(dirtyPositionPathLink_state);
                }
        }
        
        public MapEntity getEntityAt(long tick, boolean ignoreSoftDelete, boolean lookAtOtherStates)
        {
                MapEntity en;
                if (lookAtOtherStates)
                {
                        int ticks_ago = (int) (state.tick_now - tick);

                        int states_ago = SwissArmyKnife.divideFloor(ticks_ago, state.econfig.TRAILING_STATE_DELAY);

                        int state_id = state.id + states_ago;
                        if (state_id < 0 || state_id >= crossStateList.length)
                        {
                                return null;
                        }
                        
                        en = crossStateList[state_id];
                }
                else
                {
                        en = this;
                }
                
                if (en == null)
                {
                        return null;
                }

                if (en.createdAt_tick > tick)
                {
                        return null;
                }
                
                if (!ignoreSoftDelete)
                {
                        if (en.isNonExistent(tick))
                        {
                                return null;
                        }
                }
                
                return en;
        }

        /** Gets the most current position, velocity for the entity.
         * @param pos The object to fill with position, velocity, and rotation
         * @return true if the pos has been filled
         */
        public void getPosition(PhysicsPositionVector pos)
        {
                pos.set(this.pos);
        }
        
        /** Get the historic position of this entity.
         * @param pos The position is filled in this object
         * @param tick 
         * @param lookAtOtherStates If set, also look at entities in older states. 
         *        If the history of this actor overlaps with the older one, the position 
         *        from the older one is used.
         * @return true if a position was found (the entity existed at this point in time)
         */
        public boolean getHistoricPosition(PhysicsPositionVector pos, long tick, boolean lookAtOtherStates)
        {
                MapEntity en = getEntityAt(tick, false, lookAtOtherStates);

                if (en == null)
                {
                        pos.unset();
                        return false; // deleted or too far in the past
                }

                en.posHistory.get(pos.pos, tick);
                en.velHistory.get(pos.vel, tick);
                assert pos.pos.set == pos.vel.set;
                return pos.pos.set;
        }
        
        /** Get the historic smooth position of this entity.
         * @param pos The position is filled in this object
         * @param tick 
         * @param lookAtOtherStates If set, also look at entities in older states. 
         *        If the history of this actor overlaps with the older one, the position 
         *        from the older one is used.
         * @return true if a position was found (the entity existed at this point in time)
         */
        public boolean getHistoricSmoothPosition(PhysicsPoint pos, long tick, boolean lookAtOtherStates)
        {
                // Smooth history is not tracked for all map entities
                pos.unset();
                return false;
        }

        /**
         * Simulate the given amount of ticks of dead reckoning, collision, force emitters, etc
         * @param map Map
         * @param tick_now The tick this calculation belongs to. The last tick that will be calculated is
         *                 (tick_now + reckon_ticks - 1)
         * @param reckon_ticks The amount of ticks to perform
         * @param applyForceEmitters When this method is used to correct late operations, force emitters
         *                           must also be parsed. However when this dead reckon step is not for a late tick,
         *                           forces are calculated using a much more efficient method (using the entity grid,
         *                           see State.tickForceEmitters). This second method is considered the consistent one
         *                           and is thus used during timewarps.
         */
        public abstract void performDeadReckoning(PhysicsMap map, long tick_now, long reckon_ticks, boolean applyForceEmitters);
        
        /** Add (or update) the current position to the history
         *
         * @param tick The tick the current position (this.pos) belongs to
         */
        public void updatedPosition(long tick)
        {
                posHistory.setHistory(tick, pos.pos);
                velHistory.setHistory(tick, pos.vel);
                dirtyPositionPathTracker.resolved(tick);
                
                if (tick == state.tick_now)
                {
                        if (this.isRemoved(tick))
                        {
                                state.entityGrid.updateLocation(this, null);
                        }
                        else
                        {
                                state.entityGrid.updateLocation(this, pos.pos);
                        }
                }
        }
        
        public PhysicsPointHistoryDetailed getAndSeekHistoryDetail(long tick, boolean lookAtOtherStates)
        {
                MapEntity en = getEntityAt(tick, false, lookAtOtherStates);

                if (en == null)
                {
                        return null; // deleted or too far in the past
                }
                
                en.posHistory.seekDetail(tick);
                return en.posHistory;
        }
        
        public void resetTo(MapEntity other)
        {
                if (!this.state.isForeign(other))
                {
                        assert other.crossStateList == this.crossStateList;

                        if (other.crossStateList[this.state.id] != null)
                        {
                                assert other.crossStateList[this.state.id] == this;
                        }

                        if (this.crossStateList[other.state.id] != null)
                        {
                                assert this.crossStateList[other.state.id] == other;
                        }
                }
                
                //this.debug_id_with_reset = other.debug_id_with_reset;
                createdAt_tick = other.createdAt_tick;
                this.removed = other.removed;
                removedAt_tick = other.removedAt_tick;
                
                pos.set(other.pos);
                posHistory.set(other.posHistory);
                velHistory.set(other.velHistory);
                
                if (state.isForeign(other) && posHistory.HISTORY_LENGTH != other.posHistory.HISTORY_LENGTH)
                {
                        // history length might be different between foreign states.
                        // projectile history within a single state is usually not complete.
                        // (unlike actors, which store full (overlapping) histories up to in each state)
                        // the posHistory.set above make sure the tick range is correct.
                        // Using overwrite the missing values will be filled in properly.
                        
                        for (int s = other.state.env.econfig.TRAILING_STATES - 1;
                             s >= other.state.id;
                             --s)
                        {
                                if (other.crossStateList[s] != null)
                                {
                                        posHistory.overwrite(other.crossStateList[s].posHistory);
                                        velHistory.overwrite(other.crossStateList[s].velHistory);
                                }
                        }
                }
                
                markDirtyPositionPath(other.dirtyPositionPathTracker.getFirstDirtyTick());
        }

        @Override
        public LinkedListEntry getEntityGridEntry(EntityGrid grid)
        {
                return this.entityGridEntry;
        }
}
