/*
 * Copyright (c) 2009-2012 jMonkeyEngine
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 *
 * * Redistributions of source code must retain the above copyright
 *   notice, this list of conditions and the following disclaimer.
 *
 * * Redistributions in binary form must reproduce the above copyright
 *   notice, this list of conditions and the following disclaimer in the
 *   documentation and/or other materials provided with the distribution.
 *
 * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
 *   may be used to endorse or promote products derived from this software
 *   without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package com.jme3.terrain.geomipmap;

import com.jme3.bounding.BoundingBox;
import com.jme3.export.InputCapsule;
import com.jme3.export.JmeExporter;
import com.jme3.export.JmeImporter;
import com.jme3.export.OutputCapsule;
import com.jme3.material.Material;
import com.jme3.math.FastMath;
import com.jme3.math.Vector2f;
import com.jme3.math.Vector3f;
import com.jme3.scene.Spatial;
import com.jme3.scene.control.UpdateControl;
import com.jme3.terrain.Terrain;
import com.jme3.terrain.heightmap.HeightMap;
import com.jme3.terrain.heightmap.HeightMapGrid;
import java.io.IOException;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * <p>
 * TerrainGrid itself is an actual TerrainQuad. Its four children are the visible four tiles.</p>
 * </p><p>
 * The grid is indexed by cells. Each cell has an integer XZ coordinate originating at 0,0.
 * TerrainGrid will piggyback on the TerrainLodControl so it can use the camera for its
 * updates as well. It does this in the overwritten update() method.
 * </p><p>
 * It uses an LRU (Least Recently Used) cache of 16 terrain tiles (full TerrainQuadTrees). The
 * center 4 are the ones that are visible. As the camera moves, it checks what camera cell it is in
 * and will attach the now visible tiles.
 * </p><p>
 * The 'quadIndex' variable is a 4x4 array that represents the tiles. The center
 * four (index numbers: 5, 6, 9, 10) are what is visible. Each quadIndex value is an
 * offset vector. The vector contains whole numbers and represents how many tiles in offset
 * this location is from the center of the map. So for example the index 11 [Vector3f(2, 0, 1)]
 * is located 2*terrainSize in X axis and 1*terrainSize in Z axis.
 * </p><p>
 * As the camera moves, it tests what cameraCell it is in. Each camera cell covers four quad tiles
 * and is half way inside each one.
 * </p><pre>
 * +-------+-------+
 * | 1     |     3 |    Four terrainQuads that make up the grid
 * |    *..|..*    |    with the cameraCell in the middle, covering
 * |----|--|--|----|    all four quads.
 * |    *..|..*    |
 * | 2     |     4 |
 * +-------+-------+
 * </pre><p>
 * This results in the effect of when the camera gets half way across one of the sides of a quad to
 * an empty (non-loaded) area, it will trigger the system to load in the next tiles.
 * </p><p>
 * The tile loading is done on a background thread, and once the tile is loaded, then it is
 * attached to the qrid quad tree, back on the OGL thread. It will grab the terrain quad from
 * the LRU cache if it exists. If it does not exist, it will load in the new TerrainQuad tile.
 * </p><p>
 * The loading of new tiles triggers events for any TerrainGridListeners. The events are:
 * <ul>
 *  <li>tile Attached
 *  <li>tile Detached
 *  <li>grid moved.
 * </ul>
 * <p>
 * These allow physics to update, and other operation (often needed for loading the terrain) to occur
 * at the right time.
 * </p>
 * @author Anthyon
 */
public class TerrainGrid extends TerrainQuad {
    protected static final Logger log = Logger.getLogger(TerrainGrid.class.getCanonicalName());
    protected Vector3f currentCamCell = Vector3f.ZERO;
    protected int quarterSize; // half of quadSize
    protected int quadSize;
    protected HeightMapGrid heightMapGrid;
    private TerrainGridTileLoader gridTileLoader;
    protected Vector3f[] quadIndex;
    protected Set<TerrainGridListener> listeners = new HashSet<TerrainGridListener>();
    protected Material material;
    //cache  needs to be 1 row (4 cells) larger than what we care is cached
    protected LRUCache<Vector3f, TerrainQuad> cache = new LRUCache<Vector3f, TerrainQuad>(20);
    protected int cellsLoaded = 0;
    protected int[] gridOffset;
    protected boolean runOnce = false;
    protected ExecutorService cacheExecutor;

    protected class UpdateQuadCache implements Runnable {

        protected final Vector3f location;

        public UpdateQuadCache(Vector3f location) {
            this.location = location;
        }

        /**
         * This is executed if the camera has moved into a new CameraCell and will load in
         * the new TerrainQuad tiles to be children of this TerrainGrid parent.
         * It will first check the LRU cache to see if the terrain tile is already there,
         * if it is not there, it will load it in and then cache that tile.
         * The terrain tiles get added to the quad tree back on the OGL thread using the
         * attachQuadAt() method. It also resets any cached values in TerrainQuad (such as
         * neighbours).
         */
        public void run() {
            for (int i = 0; i < 4; i++) {
                for (int j = 0; j < 4; j++) {
                    int quadIdx = i * 4 + j;
                    final Vector3f quadCell = location.add(quadIndex[quadIdx]);
                    TerrainQuad q = cache.get(quadCell);
                    if (q == null) {
                        if (heightMapGrid != null) {
                            // create the new Quad since it doesn't exist
                            HeightMap heightMapAt = heightMapGrid.getHeightMapAt(quadCell);
                            q = new TerrainQuad(getName() + "Quad" + quadCell, patchSize, quadSize, heightMapAt == null ? null : heightMapAt.getHeightMap());
                            q.setMaterial(material.clone());
                            log.log(Level.FINE, "Loaded TerrainQuad {0} from HeightMapGrid", q.getName());
                        } else if (gridTileLoader != null) {
                            q = gridTileLoader.getTerrainQuadAt(quadCell);
                            // only clone the material to the quad if it doesn't have a material of its own
                            if(q.getMaterial()==null) q.setMaterial(material.clone());
                            log.log(Level.FINE, "Loaded TerrainQuad {0} from TerrainQuadGrid", q.getName());
                        }
                    }
                    cache.put(quadCell, q);

                    
                    final int quadrant = getQuadrant(quadIdx);
                    final TerrainQuad newQuad = q;
                    
                    if (isCenter(quadIdx)) {
                        // if it should be attached as a child right now, attach it
                        getControl(UpdateControl.class).enqueue(new Callable() {
                            // back on the OpenGL thread:
                            public Object call() throws Exception {
                                if (newQuad.getParent() != null) {
                                    attachQuadAt(newQuad, quadrant, quadCell, true);
                                }
                                else {
                                    attachQuadAt(newQuad, quadrant, quadCell, false);
                                }
                                return null;
                            }
                        });
                    } else {
                        getControl(UpdateControl.class).enqueue(new Callable() {
                            public Object call() throws Exception {
                                removeQuad(newQuad);
                                return null;
                            }
                        });
                    }
                }
            }

            getControl(UpdateControl.class).enqueue(new Callable() {
                    // back on the OpenGL thread:
                    public Object call() throws Exception {
                        for (Spatial s : getChildren()) {
                            if (s instanceof TerrainQuad) {
                                TerrainQuad tq = (TerrainQuad)s;
                                tq.resetCachedNeighbours();
                            }
                        }
                        System.out.println("fixed normals "+location.clone().mult(size));
                        setNeedToRecalculateNormals();
                        return null;
                    }
            });
        }
    }

    protected boolean isCenter(int quadIndex) {
        return quadIndex == 9 || quadIndex == 5 || quadIndex == 10 || quadIndex == 6;
    }

    protected int getQuadrant(int quadIndex) {
        if (quadIndex == 5) {
            return 1;
        } else if (quadIndex == 9) {
            return 2;
        } else if (quadIndex == 6) {
            return 3;
        } else if (quadIndex == 10) {
            return 4;
        }
        return 0; // error
    }

    public TerrainGrid(String name, int patchSize, int maxVisibleSize, Vector3f scale, TerrainGridTileLoader terrainQuadGrid,
            Vector2f offset, float offsetAmount) {
        this.name = name;
        this.patchSize = patchSize;
        this.size = maxVisibleSize;
        this.stepScale = scale;
        this.offset = offset;
        this.offsetAmount = offsetAmount;
        initData();
        this.gridTileLoader = terrainQuadGrid;
        terrainQuadGrid.setPatchSize(this.patchSize);
        terrainQuadGrid.setQuadSize(this.quadSize);
        addControl(new UpdateControl());
        
        fixNormalEdges(new BoundingBox(new Vector3f(0,0,0), size*2, Float.MAX_VALUE, size*2));
        addControl(new NormalRecalcControl(this));
    }

    public TerrainGrid(String name, int patchSize, int maxVisibleSize, Vector3f scale, TerrainGridTileLoader terrainQuadGrid) {
        this(name, patchSize, maxVisibleSize, scale, terrainQuadGrid, new Vector2f(), 0);
    }

    public TerrainGrid(String name, int patchSize, int maxVisibleSize, TerrainGridTileLoader terrainQuadGrid) {
        this(name, patchSize, maxVisibleSize, Vector3f.UNIT_XYZ, terrainQuadGrid);
    }

    public TerrainGrid() {
    }

    private void initData() {
        int maxVisibleSize = size;
        this.quarterSize = maxVisibleSize >> 2;
        this.quadSize = (maxVisibleSize + 1) >> 1;
        this.totalSize = maxVisibleSize;
        this.gridOffset = new int[]{0, 0};

        /*
         *        -z
         *         | 
         *        1|3 
         *  -x ----+---- x
         *        2|4
         *         |
         *         z
         */
        this.quadIndex = new Vector3f[]{
            new Vector3f(-1, 0, -1), new Vector3f(0, 0, -1), new Vector3f(1, 0, -1), new Vector3f(2, 0, -1),
            new Vector3f(-1, 0, 0), new Vector3f(0, 0, 0), new Vector3f(1, 0, 0), new Vector3f(2, 0, 0),
            new Vector3f(-1, 0, 1), new Vector3f(0, 0, 1), new Vector3f(1, 0, 1), new Vector3f(2, 0, 1),
            new Vector3f(-1, 0, 2), new Vector3f(0, 0, 2), new Vector3f(1, 0, 2), new Vector3f(2, 0, 2)};

    }

    /**
     * Get the location in cell-coordinates of the specified location.
     * Cell coordinates are integer corrdinates, usually with y=0, each 
     * representing a cell in the world.
     * For example, moving right in the +X direction:
     * (0,0,0) (1,0,0) (2,0,0), (3,0,0)
     * and then down the -Z direction:
     * (3,0,-1) (3,0,-2) (3,0,-3)
     */
    public Vector3f getCamCell(Vector3f location) {
        Vector3f tile = getTileCell(location);
        Vector3f offsetHalf = new Vector3f(-0.5f, 0, -0.5f);
        Vector3f shifted = tile.subtract(offsetHalf);
        return new Vector3f(FastMath.floor(shifted.x), 0, FastMath.floor(shifted.z));
    }

    /**
     * Centered at 0,0.
     * Get the tile index location in integer form:
     * @param location world coordinate
     */
    public Vector3f getTileCell(Vector3f location) {
        Vector3f tileLoc = location.divide(this.getWorldScale().mult(this.quadSize));
        return tileLoc;
    }

    public TerrainGridTileLoader getGridTileLoader() {
        return gridTileLoader;
    }
    
    /**
     * Get the terrain tile at the specified world location, in XZ coordinates.
     */
    public Terrain getTerrainAt(Vector3f worldLocation) {
        if (worldLocation == null)
            return null;
        Vector3f tileCell = getTileCell(worldLocation.setY(0));
        tileCell = new Vector3f(Math.round(tileCell.x), tileCell.y, Math.round(tileCell.z));
        return cache.get(tileCell);
    }
    
    /**
     * Get the terrain tile at the specified XZ cell coordinate (not world coordinate).
     * @param cellCoordinate integer cell coordinates
     * @return the terrain tile at that location
     */
    public Terrain getTerrainAtCell(Vector3f cellCoordinate) {
        return cache.get(cellCoordinate);
    }
    
    /**
     * Convert the world location into a cell location (integer coordinates)
     */
    public Vector3f toCellSpace(Vector3f worldLocation) {
        Vector3f tileCell = getTileCell(worldLocation);
        tileCell = new Vector3f(Math.round(tileCell.x), tileCell.y, Math.round(tileCell.z));
        return tileCell;
    }
    
    /**
     * Convert the cell coordinate (integer coordinates) into world coordinates.
     */
    public Vector3f toWorldSpace(Vector3f cellLocation) {
        return cellLocation.mult(getLocalScale()).multLocal(quadSize - 1);
    }
    
    protected void removeQuad(TerrainQuad q) {
        if (q != null && ( (q.getQuadrant() > 0 && q.getQuadrant()<5) || q.getParent() != null) ) {
            for (TerrainGridListener l : listeners) {
                l.tileDetached(getTileCell(q.getWorldTranslation()), q);
            }
            q.setQuadrant((short)0);
            this.detachChild(q);
            cellsLoaded++; // For gridoffset calc., maybe the run() method is a better location for this.
        }
    }

    /**
     * Runs on the rendering thread
     * @param shifted quads are still attached to the parent and don't need to re-load
     */
    protected void attachQuadAt(TerrainQuad q, int quadrant, Vector3f quadCell, boolean shifted) {
        
        q.setQuadrant((short) quadrant);
        if (!shifted)
            this.attachChild(q);

        Vector3f loc = quadCell.mult(this.quadSize - 1).subtract(quarterSize, 0, quarterSize);// quadrant location handled TerrainQuad automatically now
        q.setLocalTranslation(loc);

        if (!shifted) {
            for (TerrainGridListener l : listeners) {
                l.tileAttached(quadCell, q);
            }
        }
        updateModelBound();
        
    }

    
    /**
     * Called when the camera has moved into a new cell. We need to
     * update what quads are in the scene now.
     * 
     * Step 1: touch cache
     * LRU cache is used, so elements that need to remain
     * should be touched.
     *
     * Step 2: load new quads in background thread
     * if the camera has moved into a new cell, we load in new quads
     * @param camCell the cell the camera is in
     */
    protected void updateChildren(Vector3f camCell) {

        int dx = 0;
        int dy = 0;
        if (currentCamCell != null) {
            dx = (int) (camCell.x - currentCamCell.x);
            dy = (int) (camCell.z - currentCamCell.z);
        }

        int xMin = 0;
        int xMax = 4;
        int yMin = 0;
        int yMax = 4;
        if (dx == -1) { // camera moved to -X direction
            xMax = 3;
        } else if (dx == 1) { // camera moved to +X direction
            xMin = 1;
        }

        if (dy == -1) { // camera moved to -Y direction
            yMax = 3;
        } else if (dy == 1) { // camera moved to +Y direction
            yMin = 1;
        }

        // Touch the items in the cache that we are and will be interested in.
        // We activate cells in the direction we are moving. If we didn't move 
        // either way in one of the axes (say X or Y axis) then they are all touched.
        for (int i = yMin; i < yMax; i++) {
            for (int j = xMin; j < xMax; j++) {
                cache.get(camCell.add(quadIndex[i * 4 + j]));
            }
        }
        
        // ---------------------------------------------------
        // ---------------------------------------------------

        if (cacheExecutor == null) {
            // use the same executor as the LODControl
            cacheExecutor = createExecutorService();
        }

        cacheExecutor.submit(new UpdateQuadCache(camCell));

        this.currentCamCell = camCell;
    }

    public void addListener(TerrainGridListener listener) {
        this.listeners.add(listener);
    }

    public Vector3f getCurrentCell() {
        return this.currentCamCell;
    }

    public void removeListener(TerrainGridListener listener) {
        this.listeners.remove(listener);
    }

    @Override
    public void setMaterial(Material mat) {
        this.material = mat;
        super.setMaterial(mat);
    }

    public void setQuadSize(int quadSize) {
        this.quadSize = quadSize;
    }

    @Override
    public void adjustHeight(List<Vector2f> xz, List<Float> height) {
        Vector3f currentGridLocation = getCurrentCell().mult(getLocalScale()).multLocal(quadSize - 1);
        for (Vector2f vect : xz) {
            vect.x -= currentGridLocation.x;
            vect.y -= currentGridLocation.z;
        }
        super.adjustHeight(xz, height);
    }

    @Override
    protected float getHeightmapHeight(int x, int z) {
        return super.getHeightmapHeight(x - gridOffset[0], z - gridOffset[1]);
    }
    
    @Override
    public int getNumMajorSubdivisions() {
        return 2;
    }
    
    @Override
    public Material getMaterial(Vector3f worldLocation) {
        if (worldLocation == null)
            return null;
        Vector3f tileCell = getTileCell(worldLocation);
        Terrain terrain = cache.get(tileCell);
        if (terrain == null)
            return null; // terrain not loaded for that cell yet!
        return terrain.getMaterial(worldLocation);
    }

    /**
     * This will print out any exceptions from the thread
     */
    protected ExecutorService createExecutorService() {
        final ThreadFactory threadFactory = new ThreadFactory() {
            public Thread newThread(Runnable r) {
                Thread th = new Thread(r);
                th.setName("jME TerrainGrid Thread");
                th.setDaemon(true);
                return th;
            }
        };
        ThreadPoolExecutor ex = new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>(), 
                                    threadFactory) {
            protected void afterExecute(Runnable r, Throwable t) {
                super.afterExecute(r, t);
                if (t == null && r instanceof Future<?>) {
                    try {
                        Future<?> future = (Future<?>) r;
                        if (future.isDone())
                            future.get();
                    } catch (CancellationException ce) {
                        t = ce;
                    } catch (ExecutionException ee) {
                        t = ee.getCause();
                    } catch (InterruptedException ie) {
                        Thread.currentThread().interrupt(); // ignore/reset
                    }
                }
                if (t != null)
                    t.printStackTrace();
            }
        };
        return ex;
    }
    
    @Override
    public void read(JmeImporter im) throws IOException {
        super.read(im);
        InputCapsule c = im.getCapsule(this);
        name = c.readString("name", null);
        size = c.readInt("size", 0);
        patchSize = c.readInt("patchSize", 0);
        stepScale = (Vector3f) c.readSavable("stepScale", null);
        offset = (Vector2f) c.readSavable("offset", null);
        offsetAmount = c.readFloat("offsetAmount", 0);
        gridTileLoader = (TerrainGridTileLoader) c.readSavable("terrainQuadGrid", null);
        material = (Material) c.readSavable("material", null);
        initData();
        if (gridTileLoader != null) {
            gridTileLoader.setPatchSize(this.patchSize);
            gridTileLoader.setQuadSize(this.quadSize);
        }
    }

    @Override
    public void write(JmeExporter ex) throws IOException {
        super.write(ex);
        OutputCapsule c = ex.getCapsule(this);
        c.write(gridTileLoader, "terrainQuadGrid", null);
        c.write(size, "size", 0);
        c.write(patchSize, "patchSize", 0);
        c.write(stepScale, "stepScale", null);
        c.write(offset, "offset", null);
        c.write(offsetAmount, "offsetAmount", 0);
        c.write(material, "material", null);
    }
}
