/*
 * MegaMek -
 * Copyright (C) 2000,2001,2002,2003,2004,2005,2006 Ben Mazur (bmazur@sev.org)
 * 
 * This file also (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.universe.*;
import com.sun.j3d.utils.pickfast.*;

import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.PopupMenu;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.image.BufferedImage;
import java.util.Enumeration;
import java.util.Vector;
import javax.media.j3d.BranchGroup;
import javax.media.j3d.Canvas3D;
import javax.media.j3d.Node;
import javax.media.j3d.PickInfo;
import javax.media.j3d.Transform3D;
import javax.media.j3d.View;
import javax.vecmath.Color3f;
import javax.vecmath.Point2d;
import javax.vecmath.Point3d;
import megamek.client.event.BoardViewEvent;
import megamek.client.event.BoardViewListener;
import megamek.client.event.MechDisplayEvent;
import megamek.client.event.MechDisplayListener;
import megamek.client.ui.IDisplayable;
import megamek.client.ui.AWT.GUIPreferences;
import megamek.client.ui.AWT.TilesetManager;
import megamek.common.Coords;
import megamek.common.Entity;
import megamek.common.IBoard;
import megamek.common.IGame;
import megamek.common.IHex;
import megamek.common.MovePath;
import megamek.common.Player;
import megamek.common.UnitLocation;
import megamek.common.actions.AttackAction;
import megamek.common.event.*;
import megamek.common.preference.IClientPreferences;
import megamek.common.preference.IPreferenceChangeListener;
import megamek.common.preference.PreferenceChangeEvent;
import megamek.common.preference.PreferenceManager;

// Optimization TODO:
// - set behaviour bounds as small as actually needed
// - explore the use of SharedGroups

/**
 * Displays the board; lets the user scroll around and select points on it.
 */
public class BoardView3D
    extends Canvas3D
    implements megamek.client.ui.AWT.IBoardView, megamek.client.ui.swing.IBoardView, BoardListener, MouseListener, MouseMotionListener, MechDisplayListener, IPreferenceChangeListener, GameListener
{
    // FIXME: this isn't actually serializable, I guess.
    static final long serialVersionUID = 475073852535962574L;

    // geometry tree / important subtrees
    private SimpleUniverse universe;
    private BoardModel board;
    private AttackGroup attacks;
    private EntityGroup entities;
    private BranchGroup cursors;
    private MoveGroup moves;
    private CursorModel cursor, selectCursor, highlightCursor, firstLOSCursor, secondLOSCursor;
    private ViewTransform currentView;
    private HoverInfo hoverInfo;
    
    // displayables
    private Vector<IDisplayable> displayables = new Vector<IDisplayable>();
    private BufferedImage bi1, bi2;
    
    // mouse event control
    private PickCanvas pickBoard, pickEntities;
    boolean dragged;

    private Coords lastCursor;
    private Coords selected;
    private Coords firstLOS;
    
    private ConnectionModel ruler;
    private static final Color3f LOS_COLOR = C.red;

    // Game data
    private IGame game;
    private TileTextureManager tileManager;
    private Player localPlayer;
    private Vector<BoardViewListener> boardListeners = new Vector<BoardViewListener>();

    static {
        System.setProperty("j3d.implicitAntialiasing", "true");
        //System.setProperty("j3d.rend", "ogl");
    }
    
    /**
     * Construct a new board view for the specified game
     * @param game
     * @throws java.io.IOException 
     */
    public BoardView3D(IGame game) throws java.io.IOException {
        super(SimpleUniverse.getPreferredConfiguration());
        this.game = game;

        universe = new SimpleUniverse(this, ViewTransform.MAX_TRANSFORMS);
        ViewingPlatform vp = universe.getViewingPlatform();
        
        vp.setNominalViewingTransform();
        View v = universe.getViewer().getView();
        v.setMinimumFrameCycleTime(40); // 40ms = 25 FPS
        v.setSceneAntialiasingEnable(false);
        v.setBackClipDistance(1000*BoardModel.HEX_DIAMETER);
        v.setBackClipPolicy(View.VIRTUAL_EYE);
        v.setFrontClipDistance(BoardModel.HEX_DIAMETER/3);
        v.setFrontClipPolicy(View.VIRTUAL_EYE);
        v.setTransparencySortingPolicy(View.TRANSPARENCY_SORT_GEOMETRY);
        
        tileManager = new TileTextureManager(this, game);
        
        cursors = new BranchGroup();
        cursors.setPickable(false);
        cursors.addChild(cursor = new CursorModel(C.cyan));
        cursors.addChild(highlightCursor = new CursorModel(C.white));
        cursors.addChild(selectCursor = new CursorModel(C.blue));
        cursors.addChild(firstLOSCursor = new CursorModel(LOS_COLOR));
        cursors.addChild(secondLOSCursor = new CursorModel(LOS_COLOR));
        cursors.setCapability(BranchGroup.ALLOW_CHILDREN_WRITE);
        cursors.setCapability(BranchGroup.ALLOW_CHILDREN_EXTEND);
        cursors.compile();
        universe.addBranchGraph(cursors);

        attacks = new AttackGroup(game, tileManager, currentView);
        universe.addBranchGraph(attacks);

        entities = new EntityGroup(game, tileManager, currentView);
        universe.addBranchGraph(entities);
        
        board = new BoardModel(tileManager, universe, game);
        universe.addBranchGraph(board);

        moves = new MoveGroup(game, currentView);
        universe.addBranchGraph(moves);

        pickBoard = new PickCanvas(this, board);
        pickBoard.setMode(PickInfo.PICK_BOUNDS);
        pickBoard.setTolerance(0.0f);
        pickBoard.setFlags(PickInfo.NODE|PickInfo.LOCAL_TO_VWORLD);

        pickEntities = new PickCanvas(this, entities);
        pickEntities.setMode(PickInfo.PICK_BOUNDS);
        pickEntities.setTolerance(0.0f);
        pickEntities.setFlags(PickInfo.NODE|PickInfo.LOCAL_TO_VWORLD);

        bi1 = new BufferedImage(1, 1, BufferedImage.TYPE_4BYTE_ABGR);
        bi2 = new BufferedImage(1, 1, BufferedImage.TYPE_4BYTE_ABGR);

        hoverInfo = new HoverInfo(game, this);
        addDisplayable(hoverInfo);

        setView(Integer.parseInt(System.getProperty("megamek.client.ui.AWT.boardview3d.view", "0")));

        game.addGameListener(this);
        game.getBoard().addBoardListener(this);
        addMouseListener(this);
        addMouseMotionListener(this);
        PreferenceManager.getClientPreferences().addPreferenceChangeListener(this);
        setBackground(Color.DARK_GRAY);
    }

    
    String setView(int index) {
        if (currentView != null) currentView.remove();
        currentView = ViewTransform.create(index, universe);
        entities.setView(currentView);
        moves.setView(currentView);
        attacks.setView(currentView);
        return currentView.getName();
    }

    public void preferenceChange(PreferenceChangeEvent e) {
        if (e.getName().equals(IClientPreferences.MAP_TILESET)) updateBoard();
        else entities.update();
    }

    /**
     * Adds the specified board listener to receive
     * board events from this board.
     *
     * @param listener the board listener.
     */
    public void addBoardViewListener(BoardViewListener listener) {
        if (!boardListeners.contains(listener)) {
            boardListeners.addElement(listener);
        }
    }
    
    /**
     * Removes the specified board listener.
     *
     * @param listener the board listener.
     */
    public void removeBoardViewListener(BoardViewListener listener) {
        boardListeners.removeElement(listener);
    }

    /**
     * Notifies attached board listeners of the event.
     *
     * @param event the board event.
     */
    public void processBoardViewEvent(BoardViewEvent event) {
        if (boardListeners == null) return;

        for(Enumeration e = boardListeners.elements(); e.hasMoreElements();) {
            BoardViewListener l = (BoardViewListener)e.nextElement();
            switch(event.getType()) {
            case BoardViewEvent.BOARD_HEX_CLICKED :
            case BoardViewEvent.BOARD_HEX_DOUBLECLICKED :
            case BoardViewEvent.BOARD_HEX_DRAGGED :
            case BoardViewEvent.BOARD_HEX_POPUP :
                l.hexMoused(event);
                break;
            case BoardViewEvent.BOARD_HEX_CURSOR :
                l.hexCursor(event);
                break;
            case BoardViewEvent.BOARD_HEX_HIGHLIGHTED :
                l.boardHexHighlighted(event);
                break;
            case BoardViewEvent.BOARD_HEX_SELECTED :
                l.hexSelected(event);
                break;
            case BoardViewEvent.BOARD_FIRST_LOS_HEX :
                l.firstLOSHex(event);
                break;
            case BoardViewEvent.BOARD_SECOND_LOS_HEX :
                l.secondLOSHex(event, firstLOS);
                break;
            case BoardViewEvent.FINISHED_MOVING_UNITS :
                l.finishedMovingUnits(event);
                break;
            case BoardViewEvent.SELECT_UNIT :
                l.unitSelected(event);
                break;
            }
        }
    }

    public void addDisplayable(IDisplayable disp) {
        displayables.addElement(disp);
        repaint();
    }
    
    @Override
    public void postRender() {
        try {
            getGraphics2D().drawAndFlushImage(bi1, 0, 0, null);
            getGraphics2D().drawAndFlushImage(bi2, getWidth()-bi2.getWidth(), 0, null);
        } catch (IllegalStateException ise) {}
    }

    public void showPopup(PopupMenu popup, Coords c) {
        if (popup.getParent() == null) add(popup);

        IHex hex = game.getBoard().getHex(c);
        int level = 0;
        try {
            level = hex.getElevation();
            Entity e = game.getEntities(c).nextElement();
            level += e.getElevation();
        } catch (Exception e) {}

        Transform3D v2i = new Transform3D();
        getVworldToImagePlate(v2i);
        Point3d p = BoardModel.getHexLocation(c, level);
        v2i.transform(p);
        Point2d pixel = new Point2d();
        getPixelLocationFromImagePlate(p, pixel);
        popup.show(this, (int)pixel.x, (int)pixel.y);
    }
    
    
    private Object pickSomething(MouseEvent me) {
        try {
            if (pickEntities == null || pickBoard == null) return null;

            PickInfo target;
            Node node;

            pickEntities.setShapeLocation(me);
            target = pickEntities.pickClosest();

            if (target == null) {
                pickBoard.setShapeLocation(me);
                target = pickBoard.pickClosest();
            }
            if (target == null) return null;

            node = target.getNode();
            while (node != null && node.getUserData() == null) node = node.getParent();
            if (node == null) return null;

            return node.getUserData();

        } catch (IllegalStateException ise) {
            // ignore early clicks
            return null;
        }
    }

    private Coords pickCoords(MouseEvent me) {
        Object target = pickSomething(me);
        if (target instanceof Coords) return (Coords)target;
        else if (target instanceof Entity) return ((Entity)target).getPosition();
        
        return null;
    }    
    
    public void hideTooltip() {}


    public boolean isMovingUnits() {
        return entities.isMoving();
    }

    public void redrawEntity(Entity entity) {
        entities.update(entity);
    }

    public void centerOnHex(Coords c) {
        if (c == null || currentView == null || !game.getBoard().contains(c)) return;

        currentView.centerOnHex(c, game.getBoard().getHex(c));
    }

    public void drawMovementData(Entity entity, MovePath md) {
        moves.set(md);
    }

    public void clearMovementData() {
        moves.clear();
    }

    public void setLocalPlayer(Player p) {
        localPlayer = p;
        hoverInfo.setSelected(null, null, p);
        refreshDisplayables();
    }

    public Player getLocalPlayer() {
        return localPlayer;
    }

    public void markDeploymentHexesFor(Player p) {
        board.showDeployment(p);
    }

    public void addAttack(AttackAction aa) {
        attacks.add(aa);
        hoverInfo.add(aa);
        refreshDisplayables();
    }

    public void removeAttacksFor(Entity entity) {
        attacks.remove(entity);
        hoverInfo.remove(entity);
        refreshDisplayables();
    }

    public void refreshAttacks() {
        attacks.update();
        hoverInfo.update();
        refreshDisplayables();
    }

    public void mousePressed(MouseEvent me) {
        Coords c = pickCoords(me);
        if (c == null) return;
        dragged = false;

        if (me.isPopupTrigger() && !me.isControlDown()) {
            processBoardViewEvent(new BoardViewEvent(this, c, null, BoardViewEvent.BOARD_HEX_POPUP, me.getModifiers()));
        }
    }
    public void mouseReleased(MouseEvent me) {
        Coords c = pickCoords(me);
        if (c == null) return;

        if (me.isPopupTrigger() && !me.isControlDown() && !dragged) {
            processBoardViewEvent(new BoardViewEvent(this, c, null, BoardViewEvent.BOARD_HEX_POPUP, me.getModifiers()));
        }
    }
    public void mouseEntered(MouseEvent me) { }
    public void mouseExited(MouseEvent me) { }

    public void mouseClicked(MouseEvent me) {

        for (int i = 0; i < displayables.size(); i++) {
            IDisplayable disp = displayables.elementAt(i);
            if (disp.isHit(me.getPoint(), getSize())) {
                disp.isReleased();
                return;
            }
        }

        Coords c = pickCoords(me);
        if (c == null) return;

        if (me.isPopupTrigger() && !me.isControlDown()) {
            processBoardViewEvent(new BoardViewEvent(this, c, null, BoardViewEvent.BOARD_HEX_POPUP, me.getModifiers()));
        } else if (me.getClickCount() == 1 && me.isControlDown()) {
            if (c.equals(hoverInfo.getLOS())) {
                firstLOSCursor.hide();
                hoverInfo.setLOS(null);
            } else {
                firstLOSCursor.move(c, game.getBoard().getHex(c));
                firstLOSCursor.setColor(LOS_COLOR);
                hoverInfo.setLOS(c);
            }
            secondLOSCursor.hide();
            hoverInfo.setPosition(c);
            refreshDisplayables();
        } else if (me.getClickCount() == 1) {
            processBoardViewEvent(new BoardViewEvent(this, c, null, BoardViewEvent.BOARD_HEX_DRAGGED, me.getModifiers()));
            processBoardViewEvent(new BoardViewEvent(this, c, null, BoardViewEvent.BOARD_HEX_CLICKED, me.getModifiers()));
        } else {
            processBoardViewEvent(new BoardViewEvent(this, c, null, BoardViewEvent.BOARD_HEX_DOUBLECLICKED, me.getModifiers()));
        }
    }

    public void drawRuler(Coords s, Coords e, Color sc, Color ec) {
        if (ruler != null) ruler.detach();
        ruler = null;
        firstLOSCursor.hide();
        secondLOSCursor.hide();
        if (s == null) return;
        
        IBoard gboard = game.getBoard();
        IHex sh = gboard.getHex(s);

        firstLOSCursor.move(s, sh);
        firstLOSCursor.setColor(new Color3f(sc));

        if (e == null) return;
        
        IHex eh = gboard.getHex(e);

        ruler = new ConnectionModel(
            s, e,
            sh.surface()+1, eh.surface()+1,
            null,
            new Color3f(ec),
            0.5f
        );
        secondLOSCursor.move(e, eh);
        secondLOSCursor.setColor(new Color3f(ec));
        cursors.addChild(ruler);
    }

    /**
     * @return Returns the lastCursor.
     */
    public Coords getLastCursor() {
        return lastCursor;
    }

    /**
     * @return Returns the selected.
     */
    public Coords getSelected() {
        return selected;
    }

    public void setFirstLOS(Coords c) {
        firstLOS = c;
    }

    /**
     * Determines if this Board contains the Coords,
     * and if so, "selects" that Coords.
     *
     * @param coords the Coords.
     */
    public void select(Coords coords) {
        if (coords != null && !game.getBoard().contains(coords)) return;

        selected = coords;
        selectCursor.move(coords, game.getBoard().getHex(coords));
        firstLOSCursor.hide();
        secondLOSCursor.hide();
        processBoardViewEvent(new BoardViewEvent(this, coords, null, BoardViewEvent.BOARD_HEX_SELECTED,0));
    }
    
    /**
     * Determines if this Board contains the Coords,
     * and if so, highlights that Coords.
     *
     * @param coords the Coords.
     */
    public void highlight(Coords coords) {
        if (coords != null && !game.getBoard().contains(coords)) return;

        highlightCursor.move(coords, game.getBoard().getHex(coords));
        firstLOSCursor.hide();
        secondLOSCursor.hide();
        processBoardViewEvent(new BoardViewEvent(this, coords, null, BoardViewEvent.BOARD_HEX_HIGHLIGHTED, 0));
    }
    
    /**
     * Determines if this Board contains the Coords,
     * and if so, "cursors" that Coords.
     *
     * @param coords the Coords.
     */
    public void cursor(Coords coords) {
        if (coords != null && !game.getBoard().contains(coords)) return;

        if (lastCursor == null || coords == null || !coords.equals(lastCursor)) {
            lastCursor = coords;
            cursor.move(coords, game.getBoard().getHex(coords));
            firstLOSCursor.hide();
            secondLOSCursor.hide();
            processBoardViewEvent(new BoardViewEvent(this, coords, null, BoardViewEvent.BOARD_HEX_CURSOR, 0));
        } else {
            lastCursor = coords;
        }
    }

    public void checkLOS(Coords coords) {
        if (coords != null && !game.getBoard().contains(coords)) return;

        if (hoverInfo.getLOS() == null) {
            hoverInfo.setLOS(coords);
            firstLOSCursor.move(coords, game.getBoard().getHex(coords));
            firstLOSCursor.setColor(LOS_COLOR);
            secondLOSCursor.hide();
        } else {
            secondLOSCursor.move(coords, game.getBoard().getHex(coords));
            secondLOSCursor.setColor(LOS_COLOR);
            hoverInfo.setPosition(coords);
            refreshDisplayables();
        }
    }

    /* (non-Javadoc)
     * @see megamek.common.BoardListener#boardNewBoard(megamek.common.BoardEvent)
     */
    public void boardNewBoard(BoardEvent b) {
        updateBoard();
    }

    /* (non-Javadoc)
     * @see megamek.common.BoardListener#boardChangedHex(megamek.common.BoardEvent)
     */
    public void boardChangedHex(BoardEvent b) {
        IHex hex = game.getBoard().getHex(b.getCoords());
        tileManager.hexChanged(hex);
        board.update(b.getCoords(), hex, localPlayer);
    }

    public void gameEntityNew(GameEntityNewEvent e) {
        for (Entity en : e.GetEntities()) redrawEntity(en);
        refreshDisplayables();
    }

    public void gameEntityRemove(GameEntityRemoveEvent e) {
        entities.remove(e.getEntity());
        refreshDisplayables();
    }

    public void gameEntityChange(GameEntityChangeEvent e) {
        Vector<UnitLocation> mp = e.getMovePath();
        if (mp != null && mp.size() > 0 && GUIPreferences.getInstance().getShowMoveStep()) {
            entities.move(e.getEntity(), mp);
        } else {
            entities.update(e.getEntity());
        }
        refreshDisplayables();
    }

    public void gameNewAction(GameNewActionEvent e) {}

    public void gameBoardNew(GameBoardNewEvent e) {
        IBoard b = e.getOldBoard();
        if (b != null) b.removeBoardListener(BoardView3D.this);
        b = e.getNewBoard();
        if (b != null) b.addBoardListener(BoardView3D.this);
        updateBoard();
        if (b != null) centerOnHex(new Coords(0,0));
    }        

    public void gameBoardChanged(GameBoardChangeEvent e) {
        updateBoard();
    }

    public void gamePhaseChange(GamePhaseChangeEvent e) {
        refreshAttacks();
        switch (e.getNewPhase()) {
        case PHASE_MOVEMENT :
        case PHASE_FIRING :
        case PHASE_PHYSICAL :
            refreshAttacks();
            break;
        case PHASE_INITIATIVE :
            attacks.clear();
            hoverInfo.clear();
            break;
        case PHASE_END:
        case PHASE_VICTORY:
            attacks.clear();
            hoverInfo.clear();
            clearMovementData();
        }
        refreshDisplayables();
    }

    private void updateBoard() {
        board.update(localPlayer);
        entities.update();
        refreshDisplayables();
    }

    public void WeaponSelected(MechDisplayEvent b) {
        attacks.setSelected(b.getEntity(), b.getEquip(), localPlayer);
        hoverInfo.setSelected(b.getEntity(), b.getEquip(), localPlayer);
        refreshDisplayables();
    }

    public void zoomIn() {
        currentView.zoom(1);
    }

    public void zoomOut() {
        currentView.zoom(-1);
    }

    public Component getComponent() {
        return this;
    }

    public TilesetManager getTilesetManager() {
        return tileManager.getTilesetManager();
    }

    public void refreshMinefields() {
        updateBoard();
    }

    public void refreshDisplayables() {
        Dimension size = getSize();
        if (size.width <= 0 || size.height <= 0) return;

        // common hardware limitation
        if (size.width > 2048) size.width = 2048;
        if (size.height > 1024) size.height = 1024;

        BufferedImage b = new BufferedImage(size.width, size.height, BufferedImage.TYPE_4BYTE_ABGR);
        Graphics gr = b.getGraphics();
        for (int i = 0; i < displayables.size(); i++) {
            displayables.elementAt(i).draw(gr, size);
        }
        BufferedImage b1 = new BufferedImage(size.width/2, size.height, BufferedImage.TYPE_4BYTE_ABGR);
        b1.getGraphics().drawImage(b, 0, 0, null);
        BufferedImage b2 = new BufferedImage(size.width/2, size.height, BufferedImage.TYPE_4BYTE_ABGR);
        b2.getGraphics().drawImage(b, -size.width/2, 0, null);
        bi1 = b1;
        bi2 = b2;
    }

    // Unused GameListener methods
    public void gamePlayerConnected(GamePlayerConnectedEvent e) {}
    public void gamePlayerDisconnected(GamePlayerDisconnectedEvent e) {}
    public void gamePlayerChange(GamePlayerChangeEvent e) {}
    public void gamePlayerChat(GamePlayerChatEvent e) {}
    public void gameTurnChange(GameTurnChangeEvent e) {}
    public void gameReport(GameReportEvent e) {}
    public void gameEnd(GameEndEvent e) {}
    public void gameSettingsChange(GameSettingsChangeEvent e) {}
    public void gameMapQuery(GameMapQueryEvent e) {}
    public void gameEntityNewOffboard(GameEntityNewOffboardEvent e) {}

    public void mouseDragged(MouseEvent me) {
        dragged = true;
    }

    public void mouseMoved(MouseEvent me) {
        Coords c = pickCoords(me);
        if ((c == null? c != hoverInfo.coords : !c.equals(hoverInfo.coords))) {
            if (hoverInfo.getLOS() != null) {
                secondLOSCursor.move(c, game.getBoard().getHex(c));
                secondLOSCursor.setColor(LOS_COLOR);
            }
            hoverInfo.setPosition(c);
            refreshDisplayables();
        }
    }

}
