/*
 * 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.client.graphics.world.event;

import aphelion.client.RENDER_LAYER;
import aphelion.client.graphics.world.ActorShip;
import aphelion.client.graphics.world.GCImageAnimation;
import aphelion.client.graphics.world.MapEntities;
import aphelion.client.graphics.world.Projectile;
import aphelion.shared.gameconfig.GCImage;
import aphelion.shared.physics.EnvironmentConf;
import aphelion.shared.physics.PhysicsEnvironment;
import aphelion.shared.physics.PhysicsMap;
import aphelion.shared.physics.entities.ProjectilePublic;
import aphelion.shared.physics.events.pub.ProjectileExplosionPublic;
import aphelion.shared.physics.events.pub.ProjectileExplosionPublic.EXPLODE_REASON;
import static aphelion.shared.physics.events.pub.ProjectileExplosionPublic.EXPLODE_REASON.EXPIRATION;
import static aphelion.shared.physics.events.pub.ProjectileExplosionPublic.EXPLODE_REASON.HIT_SHIP;
import static aphelion.shared.physics.events.pub.ProjectileExplosionPublic.EXPLODE_REASON.HIT_TILE;
import static aphelion.shared.physics.events.pub.ProjectileExplosionPublic.EXPLODE_REASON.PROX_DELAY;
import static aphelion.shared.physics.events.pub.ProjectileExplosionPublic.EXPLODE_REASON.PROX_DIST;
import aphelion.shared.physics.valueobjects.PhysicsPoint;
import aphelion.shared.physics.valueobjects.PhysicsPositionVector;
import aphelion.shared.resource.ResourceDB;
import java.util.ArrayList;
import javax.annotation.Nonnull;

/**
 *
 * @author Joris
 */
public class ProjectileExplosionTracker implements EventTracker
{
        private final ResourceDB resourceDB;
        private final PhysicsEnvironment physicsEnv;
        private final MapEntities mapEntities;
        
        private boolean firstRun = true;
        private ProjectileExplosionPublic event;
        
        private long renderDelay;
        
        private ArrayList<GCImageAnimation> projectileAnimations;
        private final PhysicsPoint lastEventPosition = new PhysicsPoint();
        /** Used to track old animations incase a new animation is spawned somewhere else. */
        private int spawnID = 0;
        
        private ActorShip hitActor;
        
        // Todo: setting?:
        private static final long TIMEWARP_RESPAWN_ANIM_DISTSQ = 5 * PhysicsMap.TILE_PIXELS;
        private static final long TIMEWARP_EXTRA_ANIM_DISTSQ = 5 * PhysicsMap.TILE_PIXELS;
        private static final float TIMEWARP_ALPHA_VELOCITY = 0.025f;

        public ProjectileExplosionTracker(@Nonnull ResourceDB resourceDB, @Nonnull PhysicsEnvironment physicsEnv, @Nonnull MapEntities mapEntities)
        {
                this.resourceDB = resourceDB;
                this.physicsEnv = physicsEnv;
                this.mapEntities = mapEntities;
        }
        
        public void update(@Nonnull ProjectileExplosionPublic event)
        {
                if (this.event == null)
                {
                        this.event = event;
                }
                
                assert this.event == event;
                
                // might be null
                ProjectilePublic physicsProjectile_state0 = event.getProjectile(0);
                
                if (firstRun)
                {
                        if (physicsProjectile_state0 == null)
                        {
                                return;
                        }
                        
                        Projectile projectile = mapEntities.physicsProjectileToGraphics(physicsProjectile_state0);
                        
                        // do not update the render delay after it has been set
                        renderDelay = projectile.renderDelay_current;
                        if (renderDelay >= physicsEnv.getConfig().HIGHEST_DELAY)
                        {
                                return; // too old
                        }
                }
                
                hitActor = mapEntities.getActorShip(event.getHitActor(0));
                
                firstRun = false;
                
                if (projectileAnimations != null && lastEventPosition.set)
                {
                        PhysicsPoint pos = new PhysicsPoint();
                        event.getPosition(0, pos);
                        
                        if (lastEventPosition.distanceSquared(pos) >= TIMEWARP_RESPAWN_ANIM_DISTSQ)
                        {
                                // Spawn new animations, but do not remove the old ones (they are fade out)
                                projectileAnimations = null;
                        }
                }
                
                if (projectileAnimations == null)
                {
                        // using the render delay of the projectile

                        event.getPosition(0, lastEventPosition);
                
                        if (physicsProjectile_state0 != null &&
                            event.hasOccurred(0) && 
                            event.getOccurredAt(0) <= physicsEnv.getTick() - renderDelay)
                        {
                                spawnAnimations();       
                        }
                }
        }

        private void spawnAnimations()
        {
                ++spawnID;
                
                ProjectilePublic physicsProjectile_state0 = event.getProjectile(0);
                
                projectileAnimations = new ArrayList<>(1 + event.getCoupledProjectilesSize(0));

                PhysicsPoint tileHit = new PhysicsPoint();
                event.getHitTile(0, tileHit);

                GCImage hitImage;
                EXPLODE_REASON reason = event.getReason(0);
                switch (reason)
                {
                        case EXPIRATION:
                                hitImage = physicsProjectile_state0.getWeaponConfigImage(
                                        "projectile-expiration-animation", 
                                        resourceDB);
                                break;

                        case PROX_DELAY:
                                hitImage = physicsProjectile_state0.getWeaponConfigImage(
                                        "projectile-prox-animation", 
                                        resourceDB);
                                break;

                        case PROX_DIST:
                                hitImage = physicsProjectile_state0.getWeaponConfigImage(
                                        "projectile-prox-animation", 
                                        resourceDB);
                                break;

                        case HIT_TILE:
                                hitImage = physicsProjectile_state0.getWeaponConfigImage(
                                        "projectile-hit-tile-animation", 
                                        resourceDB);
                                break;

                        case HIT_SHIP:
                                hitImage = physicsProjectile_state0.getWeaponConfigImage(
                                        "projectile-hit-ship-animation", 
                                        resourceDB);
                                break;

                        default:
                                assert false;
                                return;
                }


                if (hitImage == null)
                {
                        return;
                }
                
                spawnAnimation(hitImage, physicsProjectile_state0);
                
                for (ProjectilePublic coupledProjectile : event.getCoupledProjectiles(0))
                {
                        spawnAnimation(hitImage, coupledProjectile);
                }
        }
        
        private void spawnAnimation(@Nonnull GCImage hitImage, @Nonnull ProjectilePublic physicsProj)
        {
                Projectile proj = mapEntities.physicsProjectileToGraphics(physicsProj);
                
                PhysicsPoint pos = new PhysicsPoint();
                event.getPosition(0, pos);
                
                boolean hitLocal = hitActor != null && hitActor.isLocalPlayer();
                
                if (pos.set)
                {
                        GCImageAnimation anim = new MyAnimation(spawnID, resourceDB, hitImage);
                        
                        if (!hitLocal) // inherit the alpha of the projectile, unless it hit the local player
                        {
                                anim.setAlpha(proj.getAlpha());
                        }

                        anim.setPositionFromPhysics(pos);
                        mapEntities.addAnimation(RENDER_LAYER.AFTER_LOCAL_SHIP, anim, null);
                        projectileAnimations.add(anim);
                }
                
                // If the last rendered position of the projectile is very different,
                // display 2 animations.
                if (proj.physicsPos.set && 
                    (!pos.set || pos.distanceSquared(proj.physicsPos) > TIMEWARP_EXTRA_ANIM_DISTSQ)
                   )
                {
                        GCImageAnimation anim = new MyAnimation(spawnID, resourceDB, hitImage);
                        if (!hitLocal) // inherit the alpha of the projectile
                        {
                                anim.setAlpha(proj.getAlpha());
                        }
                        
                        anim.setPositionFromPhysics(proj.physicsPos);
                        mapEntities.addAnimation(RENDER_LAYER.AFTER_LOCAL_SHIP, anim, null);
                        projectileAnimations.add(anim);
                }
        }
        
        
        private class MyAnimation extends GCImageAnimation
        {
                private final int mySpawnID;
                MyAnimation(int spawnID, ResourceDB db, GCImage image)
                {
                        super(db, image);
                        this.mySpawnID = spawnID;
                }

                @Override
                public void tick(long tick)
                {
                        super.tick(tick);

                        // TODO: perhaps limit the alpha to the alpha of the projectile?
                        // (or inherit the velocity?)
                        if (spawnID == this.mySpawnID && event.hasOccurred(0))
                        {
                                this.setAlphaVelocity(TIMEWARP_ALPHA_VELOCITY);
                        }
                        else
                        {
                                // fade out if the event was timewarped (or: fade it back in)
                                this.setAlphaVelocity(-TIMEWARP_ALPHA_VELOCITY);
                        }
                }
        }
}
