/*
 * 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.Point;
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.Vector;
import javax.media.j3d.BranchGroup;
import javax.media.j3d.Canvas3D;
import javax.media.j3d.Group;
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.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(Group.ALLOW_CHILDREN_WRITE);
        cursors.setCapability(Group.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 (Object name2 : boardListeners) {
            BoardViewListener l = (BoardViewListener) name2;
            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(Object popup, Coords c) {
        if (((PopupMenu)popup).getParent() == null) {
            add((PopupMenu)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);
        ((PopupMenu)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);
    }

    /*
     * (non-Javadoc)
     *
     * @see megamek.common.BoardListener#boardChangedHex(megamek.common.BoardEvent)
     */
    public void boardChangedAllHexes(BoardEvent b) {
        tileManager.getTilesetManager().loadAllHexes();
        board.update(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();
        default:
        }
        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, new Point(size.width, size.height), 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();
        }
    }

}
