/*
 * MegaMek -
 * Copyright (C) 2000,2001,2002,2003,2004,2005,2006 Ben Mazur (bmazur@sev.org)
 * 
 * This file (C) 2008 Jörg Walter <j.walter@syntax-k.de>
 *
 *  This program is free software; you can redistribute it and/or modify it
 *  under the terms of the GNU General Public License as published by the Free
 *  Software Foundation; either version 2 of the License, or (at your option)
 *  any later version.
 *
 *  This program 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.
 */

package megamek.client.ui.AWT.boardview3d;

import com.sun.j3d.utils.behaviors.interpolators.*;
import com.sun.j3d.utils.geometry.Cone;
import com.sun.j3d.utils.geometry.GeometryInfo;
import com.sun.j3d.utils.geometry.NormalGenerator;
import com.sun.j3d.utils.geometry.Stripifier;
import java.awt.Color;
import java.util.Vector;
import javax.media.j3d.Alpha;
import javax.media.j3d.Appearance;
import javax.media.j3d.BranchGroup;
import javax.media.j3d.ColoringAttributes;
import javax.media.j3d.LineStripArray;
import javax.media.j3d.Material;
import javax.media.j3d.ScaleInterpolator;
import javax.media.j3d.Shape3D;
import javax.media.j3d.Transform3D;
import javax.media.j3d.TransformGroup;
import javax.media.j3d.TransparencyAttributes;
import javax.media.j3d.TransparencyInterpolator;
import javax.vecmath.*;
import megamek.client.ui.AWT.util.PlayerColors;
import megamek.common.BattleArmor;
import megamek.common.Coords;
import megamek.common.Entity;
import megamek.common.IBoard;
import megamek.common.IGame;
import megamek.common.IHex;
import megamek.common.Infantry;
import megamek.common.Mech;
import megamek.common.UnitLocation;

/**
 *
 * @author jwalt
 */
class EntityModel extends BranchGroup {
    
    Alpha alpha;
    TransformGroup facing, label, mech;

    static final Vector3d location(IHex hex, Coords c, int elevation, double height) {
        Vector3d v = new Vector3d(BoardModel.getHexLocation(c, hex.getElevation() + elevation));
        v.z += height/2 + .1;
        return v;
    }

    static final Vector3d labelLocation(IHex hex, Coords c, int elevation) {
        return new Vector3d(BoardModel.getHexLocation(c, hex.getElevation() + (elevation < 0 ? 0 : elevation)));
    }

    static final double facing(int dir) {
        return -Math.PI/3 * dir;
    }
    
    static final double pitch(Entity entity) {
        if (entity.isProne() && entity instanceof Mech) return -Math.PI/2;
        return 0.0;
    }
    
    static final Vector3d scale(Entity entity) {
        if (entity.isProne() && entity instanceof Mech) {
            return new Vector3d(BoardModel.UNIT_SIZE, height(entity), BoardModel.HEX_HEIGHT*2);
        } else {
            return new Vector3d(BoardModel.UNIT_SIZE, BoardModel.UNIT_SIZE, height(entity));
        }
    }

    static final double height(Entity entity) {
        double height = BoardModel.HEX_HEIGHT*(entity.height()+1);
        if (entity instanceof Infantry) height = BoardModel.INFANTRY_HEIGHT;
        if (entity instanceof BattleArmor) height = BoardModel.BATTLEARMOR_HEIGHT;
        if (entity.isDestroyed()) height = BoardModel.WRECK_HEIGHT;

        return height;
    }

    public EntityModel(Entity entity, TileTextureManager tilesetManager, ViewTransform view, IGame game) {
        Coords c = entity.getPosition();
        IHex hex = game.getBoard().getHex(c);

        int elevation = entity.getElevation();
        int dir = entity.getFacing();
        if (dir == -1) dir = 0;
        int sdir = entity.getSecondaryFacing();
        if (sdir == -1) sdir = dir;

        Color tint = PlayerColors.getColor(entity.getOwner().getColorIndex());
        Color3f c50 = new Color3f(tint);
        c50.scale(0.5f);
        Color3f c30 = new Color3f(tint);
        c30.scale(0.3f);

        Vector3d scale = scale(entity);
        // TODO: use this as fallback, but load real 3D models if available
        if (true) {
            Shape3D sh = tilesetManager.getModel(entity);

            Transform3D t = new Transform3D();

            t.rotX(pitch(entity));
            t.setScale(scale);

            mech = new TransformGroup(t);
            mech.addChild(sh);
        }
        mech.setCapability(TransformGroup.ALLOW_TRANSFORM_WRITE);

        Transform3D ftrans = new Transform3D();
        ftrans.rotZ(facing(dir));
        ftrans.setTranslation(location(hex, c, elevation, height(entity)));
        facing = new TransformGroup(ftrans);
        facing.setCapability(TransformGroup.ALLOW_TRANSFORM_WRITE);
        facing.addChild(mech);

        if (entity.getECMRange() != Entity.NONE) {
            int range = entity.getECMRange();
            Appearance eapp = new Appearance();
            eapp.setColoringAttributes(new ColoringAttributes(c50, ColoringAttributes.SHADE_FLAT));
            TransparencyAttributes ta = new TransparencyAttributes(TransparencyAttributes.BLENDED, 0.90f);
            ta.setCapability(TransparencyAttributes.ALLOW_VALUE_WRITE);
            eapp.setTransparencyAttributes(ta);
            TransparencyInterpolator ti = new TransparencyInterpolator(C.halfAlpha, ta, .90f, .97f);
            ti.setSchedulingBounds(BoardModel.bounds);
            addChild(ti);
            eapp.setPolygonAttributes(C.noCull);
            Shape3D ecm = makeECMArea(range);
            ecm.setAppearance(eapp);
            ecm.setPickable(false);
            facing.addChild(ecm);
            ecm = makeECMBorder(range);
            ecm.setAppearance(eapp);
            ecm.setPickable(false);
            facing.addChild(ecm);
            eapp = new Appearance();
            eapp.setColoringAttributes(new ColoringAttributes(c50, ColoringAttributes.SHADE_FLAT));
            eapp.setLineAttributes(C.defLine);
            ecm = makeECMOutline(range);
            ecm.setAppearance(eapp);
            ecm.setPickable(false);
            facing.addChild(ecm);
        }
        
        Appearance sapp = new Appearance();
        sapp.setMaterial(new Material(new Color3f(tint), C.black, c50, C.white, 64.0f));

        Cone co = new Cone();
        co.setAppearance(sapp);

        if (dir != sdir) {
            TransformGroup fscale = new TransformGroup();
            fscale.setCapability(TransformGroup.ALLOW_TRANSFORM_WRITE);
            ScaleInterpolator si = new ScaleInterpolator(C.dblAlpha, fscale);
            si.setSchedulingBounds(BoardModel.bounds);
            si.setMinimumScale(.8f);
            si.setMaximumScale(1.25f);
            fscale.addChild(co);
            Transform3D sftrans = new Transform3D();
            sftrans.rotZ(facing(sdir-dir));
            Vector3d sv = new Vector3d(0.0, 5*BoardModel.HEX_DIAMETER/12, 0.0);
            sftrans.transform(sv);
            sftrans.setTranslation(sv);
            TransformGroup sfacing = new TransformGroup(sftrans);
            sfacing.addChild(fscale);
            sfacing.addChild(si);
            facing.addChild(sfacing);
        }

        label = new TransformGroup(new Transform3D(C.nullRot, labelLocation(hex, c, elevation), 1.0));
        label.addChild(view.makeViewRelative(new LabelModel(entity.getShortName(), C.white, c50, LabelModel.BOLD), BoardModel.HEX_DIAMETER*.45));
        label.setPickable(false);
        label.setCapability(TransformGroup.ALLOW_TRANSFORM_WRITE);

        addChild(facing);
        addChild(label);
        setCapability(BranchGroup.ALLOW_DETACH);
        setCapability(BranchGroup.ALLOW_CHILDREN_READ);
        setCapability(BranchGroup.ALLOW_CHILDREN_WRITE);
        setCapability(BranchGroup.ALLOW_CHILDREN_EXTEND);
        setUserData(entity);
    }

    void move(final Entity entity, Vector<UnitLocation> movePath, IBoard gboard) {
        final int knots = movePath.size();
        KBKeyFrame[] keyframes1 = new KBKeyFrame[knots+1];
        KBKeyFrame[] keyframes2 = new KBKeyFrame[knots+1];
        KBKeyFrame[] keyframes3 = new KBKeyFrame[knots+1];

        int pos = 0;
        Point3f scale = new Point3f(1.0f, 1.0f, 1.0f);
        Point3f zero = new Point3f();
        final Entity prev = (Entity)getUserData();
        final int mpos = (prev.isProne()?1:knots);
        final float pitchA = (float)-pitch(prev), pitchB = (float)-pitch(entity);
        final Point3f scaleA = new Point3f(scale(prev)), scaleB = new Point3f(scale(entity));
        IHex hex = gboard.getHex(prev.getPosition());
        int dir = prev.getFacing();
        if (dir == -1) dir = 0;
        keyframes1[pos] = new KBKeyFrame(
            pos*1.0f/knots,
            0,
            new Point3f(location(hex, prev.getPosition(), prev.getElevation(), height(pos < mpos?prev:entity))),
            0.0f, 0.0f, (float)facing(dir),
            scale,
            -1.0f, -0.7f, 0.2f
        );
        keyframes2[pos] = new KBKeyFrame(
            pos*1.0f/knots, 0,
            new Point3f(labelLocation(hex, prev.getPosition(), prev.getElevation())),
            0.0f, 0.0f, 0.0f, scale, -1.0f, -0.7f, 0.2f
        );
        keyframes3[pos] = new KBKeyFrame(
            pos*1.0f/knots, 0,
            zero,
            0.0f, (pos < mpos?pitchA:pitchB), 0.0f,
            (pos < mpos?scaleA:scaleB), -1.0f, -0.7f, 0.2f
        );
        pos++;
        for (UnitLocation step : movePath) {
            hex = gboard.getHex(step.getCoords());
            dir = step.getFacing();
            if (dir == -1) dir = 0;
            keyframes1[pos] = new KBKeyFrame(
                pos*1.0f/knots,
                0,
                new Point3f(location(hex, step.getCoords(), step.getElevation(), height(pos < mpos?prev:entity))),
                0.0f, 0.0f, (float)facing(dir),
                scale,
                -1.0f, -0.7f, 0.2f
            );
            keyframes2[pos] = new KBKeyFrame(
                pos*1.0f/knots, 0,
                new Point3f(labelLocation(hex, step.getCoords(), step.getElevation())),
                0.0f, 0.0f, 0.0f, scale, -1.0f, -0.7f, 0.2f
            );
            keyframes3[pos] = new KBKeyFrame(
                pos*1.0f/knots, 0,
                zero,
                0.0f, (pos < mpos?pitchA:pitchB), 0.0f,
                (pos < mpos?scaleA:scaleB), -1.0f, -0.7f, 0.2f
            );
            pos++;
        }
        
        setUserData(entity);

        alpha = new Alpha(1, 500*movePath.size());
        KBRotPosScaleSplinePathInterpolator interpolator1 = 
            new KBRotPosScaleSplinePathInterpolator(alpha, facing, new Transform3D(), keyframes1) {
                @Override
                public void computeTransform(float alphaValue, Transform3D transform) {
                    super.computeTransform(alphaValue, transform);
                    Vector3d trans = new Vector3d();
                    transform.get(trans);
                    Point3d pos = new Point3d(trans);
                    double h = height(((int)(knots*alphaValue)) < mpos?prev:entity);
                    pos.z = pos.z - h/2 + (h-BoardModel.HEX_HEIGHT) + BoardModel.HEX_HEIGHT/2;
                    EntityGroup g = ((EntityGroup)getParent().getParent().getParent());
                    if (alphaValue < 0.999f) {
                        g.removeC3LinksFor(entity);
                        g.addC3LinksFor(entity, pos);
                    } else {
                        g.update(entity);
                    }
                }
            };

        KBRotPosScaleSplinePathInterpolator interpolator2 = 
            new KBRotPosScaleSplinePathInterpolator(alpha, label, new Transform3D(), keyframes2);
        
        // as of 2008-06-01, this does not handle scaling because KBRotPosScale is lacking non-uniform scale support
        // too bad, but visually acceptable
        // since the scale value at alpha==1.0 could be wrong, override it.
        KBRotPosScaleSplinePathInterpolator interpolator3 = 
            new KBRotPosScaleSplinePathInterpolator(alpha, mech, new Transform3D(), keyframes3) {
                @Override
                public void computeTransform(float alphaValue, Transform3D transform) {
                    if (alphaValue < 0.999f) {
                        super.computeTransform(alphaValue, transform);
                    } else {
                        transform.rotX(pitch(entity));
                        transform.setScale(new Vector3d(scaleB));
                    }
                }
            };

        interpolator1.setSchedulingBounds(BoardModel.bounds);
        interpolator2.setSchedulingBounds(BoardModel.bounds);
        interpolator3.setSchedulingBounds(BoardModel.bounds);

        BranchGroup bg = new BranchGroup();
        bg.addChild(interpolator1);
        bg.addChild(interpolator2);
        bg.addChild(interpolator3);
        addChild(bg);

        alpha.setStartTime(System.currentTimeMillis()+1000);
    }
    
    private static final double[] makeECMCoords(int range) {
        double[] top = new double[3*6*(2*range+1)+3];
        int coord = 0;
        double[] pos = new double[]{
            -range*1.5*BoardModel.HEX_SIDE_LENGTH - BoardModel.HEX_SIDE_LENGTH,
            range*BoardModel.HEX_DIAMETER/2,
            10.0*BoardModel.HEX_HEIGHT
        };
        coord = makeECMPart(range, coord, top, pos,
            -BoardModel.HEX_SIDE_LENGTH/2, BoardModel.HEX_SIDE_LENGTH/2,
            -BoardModel.HEX_DIAMETER/2, -BoardModel.HEX_DIAMETER/2);
        coord = makeECMPart(range, coord, top, pos,
            BoardModel.HEX_SIDE_LENGTH/2, BoardModel.HEX_SIDE_LENGTH,
            -BoardModel.HEX_DIAMETER/2, 0);
        coord = makeECMPart(range, coord, top, pos,
            BoardModel.HEX_SIDE_LENGTH, BoardModel.HEX_SIDE_LENGTH/2,
            0, BoardModel.HEX_DIAMETER/2);
        coord = makeECMPart(range, coord, top, pos,
            BoardModel.HEX_SIDE_LENGTH/2, -BoardModel.HEX_SIDE_LENGTH/2,
            BoardModel.HEX_DIAMETER/2, BoardModel.HEX_DIAMETER/2);
        coord = makeECMPart(range, coord, top, pos,
            -BoardModel.HEX_SIDE_LENGTH/2, -BoardModel.HEX_SIDE_LENGTH,
            BoardModel.HEX_DIAMETER/2, 0);
        coord = makeECMPart(range, coord, top, pos,
            -BoardModel.HEX_SIDE_LENGTH, -BoardModel.HEX_SIDE_LENGTH/2,
            0, -BoardModel.HEX_DIAMETER/2);
        top[coord++] = top[0];
        top[coord++] = top[1];
        top[coord++] = top[2];
        
        return top;
    }

    private static final Shape3D makeECMArea(int range) {
        double[] top = makeECMCoords(range);

        GeometryInfo gi = new GeometryInfo(GeometryInfo.POLYGON_ARRAY);
        gi.setCoordinates(top);
        gi.setStripCounts(new int[] { top.length/3 });
        gi.setContourCounts(new int[] { 1 });

        NormalGenerator ng = new NormalGenerator();
        ng.generateNormals(gi);

        Stripifier st = new Stripifier();
        st.stripify(gi);
        
        return new Shape3D(gi.getGeometryArray());
    }
    
    private static final Shape3D makeECMBorder(int range) {
        double[] top = makeECMCoords(range);
        double[] border = new double[top.length*2];
        
        int j = 0;
        for (int i = 0; i < top.length; i += 3) {
            border[j++] = top[i];
            border[j++] = top[i+1];
            border[j++] = top[i+2];

            border[j++] = top[i];
            border[j++] = top[i+1];
            border[j++] = -top[i+2];
        }

        GeometryInfo gi = new GeometryInfo(GeometryInfo.TRIANGLE_STRIP_ARRAY);
        gi.setCoordinates(border);
        gi.setStripCounts(new int[] { border.length/3 });

        NormalGenerator ng = new NormalGenerator();
        ng.generateNormals(gi);

        Stripifier st = new Stripifier();
        st.stripify(gi);
        
        return new Shape3D(gi.getGeometryArray());
    }
    
    private static final Shape3D makeECMOutline(int range) {
        double[] outline = makeECMCoords(range);

        LineStripArray l = new LineStripArray(outline.length, LineStripArray.COORDINATES, new int[] { outline.length/3 });
        l.setCoordinates(0, outline);

        return new Shape3D(l);
    }
    
    private static final int makeECMPart(int range, int coord, double[] dest, double[] pos, double dx1, double dx2, double dy1, double dy2) {
        dest[coord++] = pos[0];
        pos[0] += dx2;
        dest[coord++] = pos[1];
        pos[1] += dy2;
        dest[coord++] = pos[2];

        for (int i = 0; i < range; i++) {
            dest[coord++] = pos[0];
            pos[0] += dx1;
            dest[coord++] = pos[1];
            pos[1] += dy1;
            dest[coord++] = pos[2];

            dest[coord++] = pos[0];
            pos[0] += dx2;
            dest[coord++] = pos[1];
            pos[1] += dy2;
            dest[coord++] = pos[2];
        }
        return coord;
    }

    boolean isMoving() {
        return alpha != null && !alpha.finished();
    }

}
