/*
 * This file is part of JGrasstools (http://www.jgrasstools.org)
 * (C) HydroloGIS - www.hydrologis.com 
 * 
 * JGrasstools 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 3 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.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.jgrasstools.gears.io.las;

import java.io.File;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;

import org.geotools.coverage.grid.GridCoverage2D;
import org.geotools.data.simple.SimpleFeatureCollection;
import org.geotools.feature.DefaultFeatureCollection;
import org.geotools.feature.simple.SimpleFeatureBuilder;
import org.geotools.feature.simple.SimpleFeatureTypeBuilder;
import org.geotools.geometry.jts.ReferencedEnvelope;
import org.geotools.geometry.jts.ReferencedEnvelope3D;
import org.geotools.util.WeakValueHashMap;
import org.jgrasstools.gears.io.las.core.ALasReader;
import org.jgrasstools.gears.io.las.core.ILasHeader;
import org.jgrasstools.gears.io.las.core.LasRecord;
import org.jgrasstools.gears.io.las.index.OmsLasIndexReader;
import org.jgrasstools.gears.io.las.index.LasIndexer;
import org.jgrasstools.gears.io.las.index.strtree.STRtreeJGT;
import org.jgrasstools.gears.libs.modules.JGTConstants;
import org.jgrasstools.gears.utils.CrsUtilities;
import org.jgrasstools.gears.utils.coverage.CoverageUtilities;
import org.jgrasstools.gears.utils.files.FileUtilities;
import org.opengis.feature.simple.SimpleFeature;
import org.opengis.feature.simple.SimpleFeatureType;
import org.opengis.referencing.crs.CoordinateReferenceSystem;

import com.vividsolutions.jts.geom.Coordinate;
import com.vividsolutions.jts.geom.Envelope;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.geom.Polygon;
import com.vividsolutions.jts.geom.prep.PreparedGeometry;
import com.vividsolutions.jts.geom.prep.PreparedGeometryFactory;
import com.vividsolutions.jts.index.strtree.AbstractSTRtree;
import com.vividsolutions.jts.index.strtree.ItemBoundable;

/**
 * A class that manages las folder data.
 * 
 * @author Andrea Antonello (www.hydrologis.com)
 */
class LasFolderIndexDataManager extends ALasDataManager implements AutoCloseable {
    private WeakValueHashMap<String, Pair> fileName2LasReaderMap;
    private WeakValueHashMap<String, STRtreeJGT> fileName2IndexMap;
    private List<String> fileName4LasReaderMapSupport;
    // private int READERCACHE = 5;
    private File lasFolderIndexFile;
    private File lasFolder;
    private STRtreeJGT mainLasFolderIndex;
    private GridCoverage2D inDem;
    private double elevThreshold;

    private SimpleFeatureCollection overviewFeatures;
    private ReferencedEnvelope referencedEnvelope2D;
    private List<ReferencedEnvelope> referencedEnvelope2DList = new ArrayList<ReferencedEnvelope>();
    private List<String> fileNamesList = new ArrayList<String>();
    private ReferencedEnvelope3D referencedEnvelope3D;

    /**
     * Constructor.
     * 
     * @param lasFolderIndexFile the las folder index file.
     * @param inDem a dem to normalize the elevation. If <code>null</code>, the original las elevation is used.
     * @param elevThreshold a threshold to use for the elevation normalization.
     * @param inCrs the data {@link org.opengis.referencing.crs.CoordinateReferenceSystem}. if null, the one of the dem is read, if available.
     */
    LasFolderIndexDataManager( File lasFolderIndexFile, GridCoverage2D inDem, double elevThreshold,
            CoordinateReferenceSystem inCrs ) {
        this.lasFolderIndexFile = lasFolderIndexFile;
        this.inDem = inDem;
        this.elevThreshold = elevThreshold;
        lasFolder = lasFolderIndexFile.getParentFile();

        try {
            // prj file rules if available
            inCrs = CrsUtilities.readProjectionFile(lasFolderIndexFile.getAbsolutePath(), "lasfolder");
        } catch (Exception e) {
            // ignore and try to read
        }
        if (inCrs != null) {
            crs = inCrs;
        } else if (inDem != null) {
            crs = inDem.getCoordinateReferenceSystem();
        } else {
            throw new IllegalArgumentException("The Crs can't be null.");
        }
        // indexMap = new LinkedHashMap<String, Pair>() {
        // @Override
        // protected boolean removeEldestEntry(Entry<String, Pair> eldest) {
        // return size() > READERCACHE;
        // }
        // };
        fileName2LasReaderMap = new WeakValueHashMap<String, Pair>();
        fileName2IndexMap = new WeakValueHashMap<String, STRtreeJGT>();
        fileName4LasReaderMapSupport = new ArrayList<String>();
    }

    @Override
    public File getFile() {
        return lasFolderIndexFile;
    }

    /**
     * Open the main folder file and read the main index.
     *
     * @throws Exception
     */
    @Override
    public void open() throws Exception {
        mainLasFolderIndex = OmsLasIndexReader.readIndex(lasFolderIndexFile.getAbsolutePath());
    }

    /**
     * Get points inside a given geometry boundary.
     *
     * @param checkGeom the {@link com.vividsolutions.jts.geom.Geometry} to use to check.
     * @param doOnlyEnvelope check for the geom envelope instead of a intersection with it.
     * @return the list of points contained in the supplied geometry.
     * @throws Exception
     */
    @Override
    @SuppressWarnings("rawtypes")
    public synchronized List<LasRecord> getPointsInGeometry( Geometry checkGeom, boolean doOnlyEnvelope ) throws Exception {
        checkOpen();
        ArrayList<LasRecord> pointsListForTile = new ArrayList<LasRecord>();

        Envelope env = checkGeom.getEnvelopeInternal();
        PreparedGeometry preparedGeometry = null;
        if (!doOnlyEnvelope) {
            preparedGeometry = PreparedGeometryFactory.prepare(checkGeom);
        }

        List filesList = mainLasFolderIndex.query(env);
        for( Object fileName : filesList ) {
            if (fileName instanceof String) {
                String name = (String) fileName;

                Pair pair = fileName2LasReaderMap.get(name);
                if (pair == null) {
                    File lasFile = new File(lasFolder, name);
                    File lasIndexFile = FileUtilities.substituteExtention(lasFile, "lasfix");

                    if (lasIndexFile.exists()) {
                        pair = getIndexPair(lasFile);
                        if (pair != null) {
                            fileName2LasReaderMap.put(name, pair);
                            fileName4LasReaderMapSupport.add(name);
                        }
                    } else {
                        continue;
                    }
                }

                List addressesList = pair.strTree.query(env);
                for( Object obj : addressesList ) {
                    if (obj instanceof double[]) {
                        double[] addresses = (double[]) obj;
                        long from = (long) addresses[0];
                        long to = (long) addresses[1];
                        for( long pointNum = from; pointNum < to; pointNum++ ) {
                            LasRecord lasDot = pair.reader.getPointAt(pointNum);
                            if (!doAccept(lasDot)) {
                                continue;
                            }
                            if (inDem != null) {
                                Coordinate c = new Coordinate(lasDot.x, lasDot.y);
                                if (env.contains(c)) {
                                    // check geom instead of only envelope?
                                    if (!doOnlyEnvelope && !preparedGeometry.contains(gf.createPoint(c))) {
                                        continue;
                                    }
                                    double value = CoverageUtilities.getValue(inDem, lasDot.x, lasDot.y);
                                    if (JGTConstants.isNovalue(value)) {
                                        continue;
                                    }
                                    double height = lasDot.z - value;
                                    if (height > elevThreshold) {
                                        // lasDot.z = height;
                                        lasDot.groundElevation = height;
                                        pointsListForTile.add(lasDot);
                                    }
                                }
                            } else {
                                Coordinate c = new Coordinate(lasDot.x, lasDot.y);
                                if (env.contains(c)) {
                                    // check geom instead of only envelope?
                                    if (!doOnlyEnvelope && !preparedGeometry.contains(gf.createPoint(c))) {
                                        continue;
                                    }
                                    pointsListForTile.add(lasDot);
                                }
                            }

                        }
                    }
                }
            }
        }
        return pointsListForTile;
    }

    /**
     * Retrieve all the trees envelopes that intersect the geometry.
     *
     * @param checkGeom the {@link com.vividsolutions.jts.geom.Geometry} to use to check.
     * @param doOnlyEnvelope check for the geom envelope instead of a intersection with it.
     * @param minMaxZ an array to be filled with the min and max z to be used as style.
     * @return the list of envelopes contained in the supplied geometry.
     * @throws Exception
     */
    @Override
    public synchronized List<Geometry> getEnvelopesInGeometry( Geometry checkGeom, boolean doOnlyEnvelope, double[] minMaxZ )
            throws Exception {
        checkOpen();
        ArrayList<Geometry> envelopeListForTile = new ArrayList<Geometry>();

        Envelope env = checkGeom.getEnvelopeInternal();
        PreparedGeometry preparedGeometry = null;
        if (!doOnlyEnvelope) {
            preparedGeometry = PreparedGeometryFactory.prepare(checkGeom);
        }
        double min = Double.POSITIVE_INFINITY;
        double max = Double.NEGATIVE_INFINITY;
        List< ? > filesList = mainLasFolderIndex.query(env);
        for( Object fileName : filesList ) {
            if (fileName instanceof String) {
                String name = (String) fileName;
                File lasFile = new File(lasFolder, name);
                File lasIndexFile = FileUtilities.substituteExtention(lasFile, "lasfix");

                String absolutePath = lasIndexFile.getAbsolutePath();
                STRtreeJGT lasIndex = fileName2IndexMap.get(absolutePath);
                if (lasIndex == null) {
                    lasIndex = OmsLasIndexReader.readIndex(absolutePath);
                    fileName2IndexMap.put(absolutePath, lasIndex);
                }
                List< ? > queryBoundables = lasIndex.queryBoundables(env);
                for( Object object : queryBoundables ) {
                    if (object instanceof ItemBoundable) {
                        ItemBoundable itemBoundable = (ItemBoundable) object;
                        double[] item = (double[]) itemBoundable.getItem();
                        if (item.length > 0) {
                            Envelope bounds = (Envelope) itemBoundable.getBounds();
                            Polygon envelopePolygon = LasIndexer.envelopeToPolygon(bounds);
                            envelopePolygon.setUserData(new double[]{item[2], item[3]});
                            if (minMaxZ != null) {
                                min = Math.min(min, item[2]);
                                max = Math.max(max, item[2]);
                            }
                            if (doOnlyEnvelope) {
                                envelopeListForTile.add(envelopePolygon);
                            } else {
                                if (preparedGeometry.intersects(envelopePolygon)) {
                                    envelopeListForTile.add(envelopePolygon);
                                }
                            }
                        }
                    }
                }
            }
        }
        if (minMaxZ != null) {
            minMaxZ[0] = min;
            minMaxZ[1] = max;
        }
        return envelopeListForTile;
    }

    @Override
    @SuppressWarnings("rawtypes")
    public synchronized ReferencedEnvelope getOverallEnvelope() throws Exception {
        if (referencedEnvelope2D == null) {
            checkOpen();
            Class<AbstractSTRtree> class1 = AbstractSTRtree.class;
            Field f1 = class1.getDeclaredField("itemBoundables");
            f1.setAccessible(true);
            ArrayList boundablesList = (ArrayList) f1.get(mainLasFolderIndex);
            Envelope env = null;
            for( Object item : boundablesList ) {
                if (item instanceof ItemBoundable) {
                    ItemBoundable boundable = (ItemBoundable) item;
                    Envelope envelope = (Envelope) boundable.getBounds();
                    ReferencedEnvelope tmp = new ReferencedEnvelope(envelope, crs);
                    referencedEnvelope2DList.add(tmp);
                    String name = (String) boundable.getItem();
                    fileNamesList.add(name);

                    if (env == null) {
                        env = envelope;
                    } else {
                        env.expandToInclude(envelope.getMinX(), envelope.getMinY());
                        env.expandToInclude(envelope.getMaxX(), envelope.getMaxY());
                    }
                }
            }
            referencedEnvelope2D = new ReferencedEnvelope(env, crs);
        }
        return referencedEnvelope2D;
    }

    @Override
    public List<ReferencedEnvelope> getEnvelopeList() throws Exception {
        getOverallEnvelope();
        return referencedEnvelope2DList;
    }

    @Override
    public synchronized ReferencedEnvelope3D getEnvelope3D() throws Exception {
        if (referencedEnvelope3D == null) {
            checkReadersMap();

            for( String key : fileName4LasReaderMapSupport ) {
                Pair pair = fileName2LasReaderMap.get(key);
                if (pair == null) {
                    File lasFile = new File(lasFolder, key);
                    pair = getIndexPair(lasFile);
                    if (pair == null) {
                        System.err.println("Null reader pair: " + lasFile);
                        continue;
                    }
                }
                ILasHeader header = pair.reader.getHeader();
                ReferencedEnvelope3D envelope = header.getDataEnvelope();
                if (referencedEnvelope3D == null) {
                    referencedEnvelope3D = envelope;
                } else {
                    referencedEnvelope3D.expandToInclude(envelope.getMinX(), envelope.getMinY(), envelope.getMinZ());
                    referencedEnvelope3D.expandToInclude(envelope.getMaxX(), envelope.getMaxY(), envelope.getMaxZ());
                }
            }
        }
        return referencedEnvelope3D;
    }

    @Override
    public synchronized SimpleFeatureCollection getOverviewFeatures() throws Exception {
        if (overviewFeatures == null) {
            List<ReferencedEnvelope> envelopeList = getEnvelopeList();

            SimpleFeatureTypeBuilder b = new SimpleFeatureTypeBuilder();
            b.setName("overview");
            b.setCRS(crs);
            b.add("the_geom", Polygon.class);
            b.add("name", String.class);
            SimpleFeatureType type = b.buildFeatureType();
            SimpleFeatureBuilder builder = new SimpleFeatureBuilder(type);

            overviewFeatures = new DefaultFeatureCollection();

            for( int i = 0; i < envelopeList.size(); i++ ) {
                String name = fileNamesList.get(i);
                ReferencedEnvelope envelope = envelopeList.get(i);
                Polygon polygon = OmsLasIndexReader.envelopeToPolygon(envelope);
                Object[] objs = new Object[]{polygon, name};
                builder.addAll(objs);
                SimpleFeature feature = builder.buildFeature(null);
                ((DefaultFeatureCollection) overviewFeatures).add(feature);
            }
        }
        return overviewFeatures;
    }

    @SuppressWarnings("rawtypes")
    private void checkReadersMap() throws Exception {
        checkOpen();
        if (fileName2LasReaderMap.size() == 0) {
            List filesList = mainLasFolderIndex.itemsTree();
            for( Object fileName : filesList ) {
                if (fileName instanceof String) {
                    String name = (String) fileName;
                    getReader(name);
                } else if (fileName instanceof List) {
                    List filesList2 = (List) fileName;
                    for( Object fileName2 : filesList2 ) {
                        if (fileName2 instanceof String) {
                            String name2 = (String) fileName2;
                            getReader(name2);
                        }
                    }
                } else {
                    throw new RuntimeException();
                }
            }
        }
    }

    private void getReader( String name ) throws Exception {
        Pair pair = fileName2LasReaderMap.get(name);
        if (pair == null) {
            File lasFile = new File(lasFolder, name);
            pair = getIndexPair(lasFile);
            if (pair != null) {
                fileName2LasReaderMap.put(name, pair);
                fileName4LasReaderMapSupport.add(name);
            }
            // System.out.println("Added: " + lasIndexFile);
            // } else {

        }
    }

    private Pair getIndexPair( File lasFile ) throws Exception {
        File lasIndexFile = FileUtilities.substituteExtention(lasFile, "lasfix");
        if (lasIndexFile.exists()) {
            ALasReader reader = ALasReader.getReader(lasFile, crs);
            reader.open();
            reader.getHeader();
            STRtreeJGT lasIndex = OmsLasIndexReader.readIndex(lasIndexFile.getAbsolutePath());
            Pair pair = new Pair();
            pair.reader = reader;
            pair.strTree = lasIndex;
            return pair;
        } else {
            System.err.println("Doesn't exist: " + lasIndexFile);
        }
        return null;
    }

    private void checkOpen() throws Exception {
        if (mainLasFolderIndex == null) {
            open();
        }
    }

    @Override
    public void close() throws Exception {
        for( String key : fileName4LasReaderMapSupport ) {
            Pair pair = fileName2LasReaderMap.get(key);
            if (pair != null)
                pair.close();
        }
        fileName4LasReaderMapSupport.clear();
        fileName2LasReaderMap.clear();
        fileName2LasReaderMap = null;
    }

    private class Pair {
        ALasReader reader;
        STRtreeJGT strTree;
        public void close() {
            if (reader != null)
                try {
                    reader.close();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            reader = null;
            strTree = null;
        }
    }

}
