/*
 
* Copyright (c) 2016, Metron, Inc.
 
* 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 Metron, Inc. 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 METRON, INC. 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.metsci.glimpse.dnc.convert;

import static com.google.common.base.Objects.equal;
import static com.metsci.glimpse.dnc.util.DncMiscUtils.filenameToLowercase;
import static com.metsci.glimpse.dnc.util.DncMiscUtils.last;
import static com.metsci.glimpse.dnc.util.DncMiscUtils.sorted;
import static com.metsci.glimpse.dnc.util.DncMiscUtils.toArrayList;
import static com.metsci.glimpse.util.GeneralUtils.ints;
import static gov.nasa.worldwind.formats.vpf.VPFConstants.DATABASE_HEADER_TABLE;
import static gov.nasa.worldwind.formats.vpf.VPFConstants.EDGE_PRIMITIVE_TABLE;
import static java.util.Arrays.sort;
import static java.util.Collections.reverse;
import static java.util.Collections.unmodifiableList;

import java.io.File;
import java.io.FileFilter;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.EnumSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Stack;

import gov.nasa.worldwind.formats.vpf.VPFBasicPrimitiveDataFactory;
import gov.nasa.worldwind.formats.vpf.VPFBufferedRecordData;
import gov.nasa.worldwind.formats.vpf.VPFCoverage;
import gov.nasa.worldwind.formats.vpf.VPFFeature;
import gov.nasa.worldwind.formats.vpf.VPFFeatureClass;
import gov.nasa.worldwind.formats.vpf.VPFFeatureTableFilter;
import gov.nasa.worldwind.formats.vpf.VPFFeatureType;
import gov.nasa.worldwind.formats.vpf.VPFLibrary;
import gov.nasa.worldwind.formats.vpf.VPFPrimitiveData;
import gov.nasa.worldwind.formats.vpf.VPFPrimitiveData.EdgeInfo;
import gov.nasa.worldwind.formats.vpf.VPFPrimitiveData.FaceInfo;
import gov.nasa.worldwind.formats.vpf.VPFPrimitiveData.Ring;
import gov.nasa.worldwind.formats.vpf.VPFPrimitiveDataFactory;
import gov.nasa.worldwind.formats.vpf.VPFSurfaceLine;
import gov.nasa.worldwind.formats.vpf.VPFTile;
import gov.nasa.worldwind.formats.vpf.VPFUtils;
import gov.nasa.worldwind.geom.LatLon;
import gov.nasa.worldwind.util.CompoundVecBuffer;
import gov.nasa.worldwind.util.VecBuffer;
import gov.nasa.worldwind.util.VecBufferSequence;

public class Vpf
{

    
public static final Comparator<VPFLibrary> vpfLibraryNameComparator = new Comparator<VPFLibrary>()
    
{
        
public int compare(VPFLibrary a, VPFLibrary b)
        
{
            
return compareStringsCaseSecondary(a.getName(), b.getName());
        
}
    
};


    
public static final Comparator<VPFCoverage> vpfCoverageNameComparator = new Comparator<VPFCoverage>()
    
{
        
public int compare(VPFCoverage a, VPFCoverage b)
        
{
            
return compareStringsCaseSecondary(a.getName(), b.getName());
        
}
    
};


    
public static final Comparator<VPFFeatureClass> vpfFeatureClassNameComparator = new Comparator<VPFFeatureClass>()
    
{
        
public int compare(VPFFeatureClass a, VPFFeatureClass b)
        
{
            
return compareStringsCaseSecondary(a.getClassName(), b.getClassName());
        
}
    
};


    
/**
     
* Like case-insensitive comparison, but uses case-sensitive comparison as a tie-breaker.
     
*/

    
public static int compareStringsCaseSecondary(String a, String b)
    
{
        
int caseInsensitiveComparison = a.compareToIgnoreCase( b );
        
if ( caseInsensitiveComparison != 0 ) return caseInsensitiveComparison;

        
int caseSensitiveComparison = a.compareTo( b );
        
return caseSensitiveComparison;
    
}


    
public static VPFFeatureClass[] readAllFeatureClasses(VPFLibrary lib)
    
{
        
return readFeatureClasses(lib, VPFFeatureType.values());
    
}


    
public static VPFFeatureClass[] readFeatureClasses(VPFLibrary lib, VPFFeatureType... types)
    
{
        
EnumSet<VPFFeatureType> typeSet = EnumSet.noneOf(VPFFeatureType.class);
        
for (VPFFeatureType t : types) typeSet.add(t);

        
FileFilter featureTableFilter = new VPFFeatureTableFilter()
        
{
            
public boolean accept(File file)
            
{
                
// On a case-insensitive filesystem, java.io.File equality is case-insensitive,
                
// so the file-equality test here always returns true.
                
//
                
// On a case-sensitive filesystem, Worldwind's VPF reader only works right with
                
// lowercase filenames. In such a case, lowercase symlinks have been created for
                
// any files with names containing uppercase chars. Consequently:
                
//
                
//
   
1. Every file is accessible using the lowercase version of its filename

                
//
   
2. Files with uppercase chars should be ignored, to avoid duplicates

                
//
                
// Therefore, only accept files whose names have no uppercase chars.
                
//
                
return ( super.accept(file) && equal(file, filenameToLowercase(file)) );
            
}
        
};

        
Set<VPFFeatureClass> featureClasses = new LinkedHashSet<>();
        
for (VPFCoverage cov : lib.getCoverages())
        
{
            
if (cov.isReferenceCoverage()) continue;

            
VPFFeatureClass[] fcs = VPFUtils.readFeatureClasses(cov, featureTableFilter);
            
for (VPFFeatureClass fc : fcs)
            
{
                
VPFFeatureType type = fc.getType();
                
if (typeSet.contains(type)) featureClasses.add(fc);
            
}
        
}

        
VPFFeatureClass[] featureClassesArray = featureClasses.toArray(new VPFFeatureClass[0]);
        
sort( featureClassesArray, vpfFeatureClassNameComparator);
        
return featureClassesArray;
    
}


    
public static Map<VPFCoverage,VPFPrimitiveData> createPrimitiveDatas(VPFLibrary lib, VPFTile tile)
    
{
        
VPFPrimitiveDataFactory factory = new VPFBasicPrimitiveDataFactory(tile);

        
Map<VPFCoverage,VPFPrimitiveData> primitiveDatas = new LinkedHashMap<>();
        
for (VPFCoverage cov : sorted(lib.getCoverages(), vpfCoverageNameComparator))
        
{
            
primitiveDatas.put(cov, factory.createPrimitiveData(cov));
        
}
        
return primitiveDatas;
    
}


    
/**
     
* Returns a map from database name to database dir
     
*/
    
public static Map<String,File> vpfDatabaseDirsByName(File parentDir)
    
{
        
Map<String,File> dbDirs = new LinkedHashMap<>();

        
File[] children = parentDir.listFiles();
        
if (children != null)
        
{
            
sort(children);
            
for (File dbDir : children)
            
{
                
File dhtFile = findDhtFile(dbDir);
                
if (dhtFile != null)
                
{
                    
String dbName = readDatabaseName(dhtFile);
                    
if (dbName != null && !dbDirs.containsKey(dbName))
                    
{
                        
dbDirs.put(dbName, dhtFile.getParentFile().getAbsoluteFile());
                    
}
                
}
            
}
        
}

        
return dbDirs;
    
}


    
public static File findDhtFile(File dbDir)
    
{
        
File fileOrSymlink = null;
        
File fileActual = null;

        
File[] children = dbDir.listFiles();
        
if (children != null)
        
{
            
sort(children);
            
for (File f : children)
            
{
                
if (f.getName().equalsIgnoreCase(DATABASE_HEADER_TABLE) && f.isFile())
                
{
                    
fileOrSymlink = f;
                    
if (!Files.isSymbolicLink(f.toPath()))
                    
{
                        
fileActual = f;
                    
}
                
}
            
}
        
}

        
return (fileActual == null ? fileOrSymlink : fileActual);
    
}


    
public static String readDatabaseName(File dhtFile)
    
{
        
if (dhtFile != null)
        
{
            
VPFBufferedRecordData dht = VPFUtils.readTable(dhtFile);
            
if (dht != null && dht.getNumRecords() >= 1)
            
{
                
Object o = dht.getRecord(1).getValue("database_name");
                
if (o instanceof String)
                
{
                    
return ((String) o);
                
}
            
}
        
}
        
return null;
    
}


    
public static class Edge
    
{
        
public final int start;
        
public final int end;
        
public final List<LatLon> locations;

        
Edge(int start, int end, List<LatLon> locations)
        
{
            
this.start = start;
            
this.end = end;
            
this.locations = unmodifiableList(new ArrayList<LatLon>(locations));
        
}

        
public Edge flipped()
        
{
            
List<LatLon> locations2 = new ArrayList<LatLon>(locations);
            
reverse(locations2);
            
return new Edge(end, start, locations2);
        
}
    
}


    
public static boolean edgeIsReversed(int edgeOrientation)
    
{
        
switch (edgeOrientation)
        
{
            
case 1: return false;
            
case -1: return true;
            
default: throw new RuntimeException("Unrecognized edge-orientation code: " + edgeOrientation);
        
}
    
}


    
public static List<Edge> extractRingEdges(VPFPrimitiveData primitiveData, VecBufferSequence edgeTableData, Ring ring)
    
{
        
List<Edge> edges = new ArrayList<>();
        
for (int i = 0; i < ring.getNumEdges(); i++)
        
{
            
int id = ring.getEdgeId(i);

            
EdgeInfo info = (EdgeInfo) primitiveData.getPrimitiveInfo(EDGE_PRIMITIVE_TABLE, id);
            
List<LatLon> locations = toArrayList( edgeTableData.slice(ints(id), 0, 1).getLocations() );
            
Edge edge = new Edge(info.getStartNode(), info.getEndNode(), locations);

            
boolean reverse = edgeIsReversed(ring.getEdgeOrientation(i));
            
edges.add( reverse ? edge.flipped() : edge );
        
}
        
return edges;
    
}


    
public static class RingEdgesBacktrackException extends RuntimeException
    
{
        
public final List<Edge> edgesExtracted;
        
public final List<Edge> edgesRegularizedSoFar;
        
public final Edge currentEdge;

        
public RingEdgesBacktrackException(List<Edge> edgesExtracted, List<Edge> edgesRegularizedSoFar)
        
{
            
this(edgesExtracted, edgesRegularizedSoFar, null);
        
}

        
public RingEdgesBacktrackException(List<Edge> edgesExtracted, List<Edge> edgesRegularizedSoFar, Edge currentEdge)
        
{
            
super("Ring-edges backtrack failed");
            
this.edgesExtracted = unmodifiableList(new ArrayList<Edge>(edgesExtracted));
            
this.edgesRegularizedSoFar = unmodifiableList(new ArrayList<Edge>(edgesRegularizedSoFar));
            
this.currentEdge = currentEdge;
        
}

        
@Override
        
public String toString()
        
{
            
StringBuilder s = new StringBuilder(super.toString());

            
s.append(" ... edges:");
            
for (Edge edge : edgesExtracted) s.append(edge.start).append("-").append(edge.end).append(",");
            
s.setLength(s.length()-1);

            
s.append(" ... regularized:");
            
for (Edge edge : edgesRegularizedSoFar) s.append(edge.start).append("-").append(edge.end).append(",");
            
s.setLength(s.length()-1);

            
if (currentEdge != null)
            
{
                
s.append(" ... current:");
                
s.append(currentEdge.start).append("-").append(currentEdge.end);
            
}

            
return s.toString();
        
}
    
}


    
public static List<Edge> regularizeRingEdges(List<Edge> extractedRingEdges)
    
{
        
List<Edge> regularized = new ArrayList<>();
        
Stack<Edge> stack = new Stack<Edge>();

        
for (int i = 0; i < extractedRingEdges.size(); i++)
        
{
            
Edge current = extractedRingEdges.get(i);
            
regularized.add(current);
            
stack.push(current);

            
// When an edge is completely contained in the polygon, WWJ only gives us the edge once,
            
// making sort of a T intersection (... AB, BC, BD, DE, ...). To make a well-behaved
            
// polygon, we insert "backtrack" edges (... AB, BC, *CB*, BD, DE, ...).
            
//
            
// Note that we may have to backtrack after the last edge, to get back to the start of
            
// the first edge -- hence the "(i+1) % size" here.
            
//
            
Edge next = extractedRingEdges.get((i + 1) % extractedRingEdges.size());
            
while (last(regularized).end != next.start)
            
{
                
// If the stack is empty, it means next.start is a location the polygon hasn't visited
                
// before ... which means that these edges just don't make a well-behaved polygon.
                
//
                
if (stack.isEmpty()) throw new RingEdgesBacktrackException(extractedRingEdges, regularized, current);
                
regularized.add( stack.pop().flipped() );
            
}
        
}

        
return regularized;
    
}


    
public static List<LatLon> locations(List<Edge> edges)
    
{
        
List<LatLon> locations = new ArrayList<>();
        
for (Edge edge : edges) locations.addAll(edge.locations);
        
return locations;
    
}


    
public static List<LatLon> ringLocations(VPFPrimitiveData primitiveData, VecBufferSequence edgeTableData, Ring ring)
    
{
        
return locations( regularizeRingEdges( extractRingEdges(primitiveData, edgeTableData, ring) ) );
    
}


    
public static List<List<LatLon>> vpfAreaRings(VPFFeature areaFeature, VPFPrimitiveData primitiveData)
    
{
        
List<List<LatLon>> vertices = new ArrayList<>();
        
VecBufferSequence edgeTableData = primitiveData.getPrimitiveCoords(EDGE_PRIMITIVE_TABLE);
        
String primitiveName = areaFeature.getFeatureClass().getPrimitiveTableName();
        
for (int id : areaFeature.getPrimitiveIds())
        
{
            
FaceInfo faceInfo = (FaceInfo) primitiveData.getPrimitiveInfo(primitiveName, id);

            
vertices.add( ringLocations(primitiveData, edgeTableData, faceInfo.getOuterRing()) );

            
for (Ring innerRing : faceInfo.getInnerRings())
            
{
                
vertices.add( ringLocations(primitiveData, edgeTableData, innerRing) );
            
}
        
}
        
return vertices;
    
}


    
public static List<LatLon> vpfLineVertices(VPFFeature lineFeature, VPFPrimitiveData primitiveData)
    
{
        
VPFSurfaceLine line = new VPFSurfaceLine(lineFeature, primitiveData);

        
List<LatLon> lines = new ArrayList<>();

        
LatLon vStart = null;
        
for (LatLon vEnd : line.getLocations())
        
{
            
if (vStart != null)
            
{
                
lines.add(vStart);
                
lines.add(vEnd);
            
}
            
vStart = vEnd;
        
}

        
return lines;
    
}


    
public static LatLon vpfPointVertex(VPFFeature pointFeature, VPFPrimitiveData primitiveData)
    
{
        
List<LatLon> points = new ArrayList<>();

        
String primitiveName = pointFeature.getFeatureClass().getPrimitiveTableName();
        
CompoundVecBuffer combinedCoords = primitiveData.getPrimitiveCoords(primitiveName);
        
if (combinedCoords != null)
        
{
            
for (int id : pointFeature.getPrimitiveIds())
            
{
                
VecBuffer coords = combinedCoords.subBuffer(id);
                
if (coords == null || coords.getSize() == 0) continue;

                
for (LatLon location : coords.getLocations()) points.add(location);
            
}
        
}
        
if (points.size() != 1) throw new RuntimeException("Point feature has " + points.size() + " vertices");

        
return points.get(0);
    
}

}