/**
 * Efficient cache or osmnodes
 *
 * @author ab
 */
package btools.mapaccess;

import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import btools.codec.DataBuffers;
import btools.codec.MicroCache;
import btools.codec.WaypointMatcher;
import btools.expressions.BExpressionContextWay;

public final class NodesCache {
  private File segmentDir;
  private File secondarySegmentsDir = null;

  public OsmNodesMap nodesMap;
  private BExpressionContextWay expCtxWay;
  private int lookupVersion;
  private int lookupMinorVersion;
  private boolean forceSecondaryData;
  private String currentFileName;

  private Map<String, PhysicalFile> fileCache;
  private DataBuffers dataBuffers;

  private OsmFile[][] fileRows;

  public WaypointMatcher waypointMatcher;

  public boolean first_file_access_failed = false;
  public String first_file_access_name;

  private long cacheSum = 0;
  private long maxmemtiles;
  private boolean detailed; // NOPMD used in constructor

  private boolean garbageCollectionEnabled = false;
  private boolean ghostCleaningDone = false;


  private long cacheSumClean = 0;
  private long ghostSum = 0;
  private long ghostWakeup = 0;

  private boolean directWeaving = !Boolean.getBoolean("disableDirectWeaving");

  public String formatStatus() {
    return "collecting=" + garbageCollectionEnabled + " noGhosts=" + ghostCleaningDone + " cacheSum=" + cacheSum + " cacheSumClean=" + cacheSumClean + " ghostSum=" + ghostSum + " ghostWakeup=" + ghostWakeup;
  }

  public NodesCache(File segmentDir, BExpressionContextWay ctxWay, boolean forceSecondaryData, long maxmem, NodesCache oldCache, boolean detailed) {
    this.maxmemtiles = maxmem / 8;
    this.segmentDir = segmentDir;
    this.nodesMap = new OsmNodesMap();
    this.nodesMap.maxmem = (2L * maxmem) / 3L;
    this.expCtxWay = ctxWay;
    this.lookupVersion = ctxWay.meta.lookupVersion;
    this.lookupMinorVersion = ctxWay.meta.lookupMinorVersion;
    this.forceSecondaryData = forceSecondaryData;
    this.detailed = detailed;

    if (ctxWay != null) {
      ctxWay.setDecodeForbidden(detailed);
    }

    first_file_access_failed = false;
    first_file_access_name = null;

    if (!this.segmentDir.isDirectory())
      throw new RuntimeException("segment directory " + segmentDir.getAbsolutePath() + " does not exist");

    if (oldCache != null) {
      fileCache = oldCache.fileCache;
      dataBuffers = oldCache.dataBuffers;
      secondarySegmentsDir = oldCache.secondarySegmentsDir;

      // re-use old, virgin caches (if same detail-mode)
      if (oldCache.detailed == detailed) {
        fileRows = oldCache.fileRows;
        for (OsmFile[] fileRow : fileRows) {
          if (fileRow == null)
            continue;
          for (OsmFile osmf : fileRow) {
            cacheSum += osmf.setGhostState();
          }
        }
      } else {
        fileRows = new OsmFile[180][];
      }
    } else {
      fileCache = new HashMap<>(4);
      fileRows = new OsmFile[180][];
      dataBuffers = new DataBuffers();
      secondarySegmentsDir = StorageConfigHelper.getSecondarySegmentDir(segmentDir);
    }
    ghostSum = cacheSum;
  }

  public void clean(boolean all) {
    for (OsmFile[] fileRow : fileRows) {
      if (fileRow == null)
        continue;
      for (OsmFile osmf : fileRow) {
        osmf.clean(all);
      }
    }
  }

  // if the cache sum exceeded a threshold,
  // clean all ghosts and enable garbage collection
  private void checkEnableCacheCleaning() {
    if (cacheSum < maxmemtiles) {
      return;
    }

    for (int i = 0; i < fileRows.length; i++) {
      OsmFile[] fileRow = fileRows[i];
      if (fileRow == null) {
        continue;
      }
      for (OsmFile osmf : fileRow) {
        if (garbageCollectionEnabled && !ghostCleaningDone) {
          cacheSum -= osmf.cleanGhosts();
        } else {
          cacheSum -= osmf.collectAll();
        }
      }
    }

    if (garbageCollectionEnabled) {
      ghostCleaningDone = true;
      maxmemtiles *= 2;
    } else {
      cacheSumClean = cacheSum;
      garbageCollectionEnabled = true;
    }
  }

  public int loadSegmentFor(int ilon, int ilat) {
    MicroCache mc = getSegmentFor(ilon, ilat);
    return mc == null ? 0 : mc.getSize();
  }

  public MicroCache getSegmentFor(int ilon, int ilat) {
    try {
      int lonDegree = ilon / 1000000;
      int latDegree = ilat / 1000000;
      OsmFile osmf = null;
      OsmFile[] fileRow = fileRows[latDegree];
      int ndegrees = fileRow == null ? 0 : fileRow.length;
      for (int i = 0; i < ndegrees; i++) {
        if (fileRow[i].lonDegree == lonDegree) {
          osmf = fileRow[i];
          break;
        }
      }
      if (osmf == null) {
        osmf = fileForSegment(lonDegree, latDegree);
        OsmFile[] newFileRow = new OsmFile[ndegrees + 1];
        for (int i = 0; i < ndegrees; i++) {
          newFileRow[i] = fileRow[i];
        }
        newFileRow[ndegrees] = osmf;
        fileRows[latDegree] = newFileRow;
      }
      currentFileName = osmf.filename;

      if (!osmf.hasData()) {
        return null;
      }

      MicroCache segment = osmf.getMicroCache(ilon, ilat);
      if (segment == null) {
        checkEnableCacheCleaning();
        segment = osmf.createMicroCache(ilon, ilat, dataBuffers, expCtxWay, waypointMatcher, directWeaving ? nodesMap : null);

        cacheSum += segment.getDataSize();
      } else if (segment.ghost) {
        segment.unGhost();
        ghostWakeup += segment.getDataSize();
      }
      return segment;
    } catch (IOException re) {
      throw new RuntimeException(re.getMessage());
    } catch (RuntimeException re) {
      throw re;
    } catch (Exception e) {
      throw new RuntimeException("error reading datafile " + currentFileName + ": " + e, e);
    }
  }

  /**
   * make sure the given node is non-hollow,
   * which means it contains not just the id,
   * but also the actual data
   *
   * @return true if successfull, false if node is still hollow
   */
  public boolean obtainNonHollowNode(OsmNode node) {
    if (!node.isHollow())
      return true;

    MicroCache segment = getSegmentFor(node.ilon, node.ilat);
    if (segment == null) {
      return false;
    }
    if (!node.isHollow()) {
      return true; // direct weaving...
    }

    long id = node.getIdFromPos();
    if (segment.getAndClear(id)) {
      node.parseNodeBody(segment, nodesMap, expCtxWay);
    }

    if (garbageCollectionEnabled) { // garbage collection
      cacheSum -= segment.collect(segment.getSize() >> 1); // threshold = 1/2 of size is deleted
    }

    return !node.isHollow();
  }


  /**
   * make sure all link targets of the given node are non-hollow
   */
  public void expandHollowLinkTargets(OsmNode n) {
    for (OsmLink link = n.firstlink; link != null; link = link.getNext(n)) {
      obtainNonHollowNode(link.getTarget(n));
    }
  }

  /**
   * make sure all link targets of the given node are non-hollow
   */
  public boolean hasHollowLinkTargets(OsmNode n) {
    for (OsmLink link = n.firstlink; link != null; link = link.getNext(n)) {
      if (link.getTarget(n).isHollow()) {
        return true;
      }
    }
    return false;
  }

  /**
   * get a node for the given id with all link-targets also non-hollow
   * <p>
   * It is required that an instance of the start-node does not yet
   * exist, not even a hollow instance, so getStartNode should only
   * be called once right after resetting the cache
   *
   * @param id the id of the node to load
   * @return the fully expanded node for id, or null if it was not found
   */
  public OsmNode getStartNode(long id) {
    // initialize the start-node
    OsmNode n = new OsmNode(id);
    n.setHollow();
    nodesMap.put(n);
    if (!obtainNonHollowNode(n)) {
      return null;
    }
    expandHollowLinkTargets(n);
    return n;
  }

  public OsmNode getGraphNode(OsmNode template) {
    OsmNode graphNode = new OsmNode(template.ilon, template.ilat);
    graphNode.setHollow();
    OsmNode existing = nodesMap.put(graphNode);
    if (existing == null) {
      return graphNode;
    }
    nodesMap.put(existing);
    return existing;
  }

  public void matchWaypointsToNodes(List<MatchedWaypoint> unmatchedWaypoints, double maxDistance, OsmNodePairSet islandNodePairs) {
    waypointMatcher = new WaypointMatcherImpl(unmatchedWaypoints, maxDistance, islandNodePairs);
    for (MatchedWaypoint mwp : unmatchedWaypoints) {
      int cellsize = 12500;
      preloadPosition(mwp.waypoint, cellsize);
      // get a second chance
      if (mwp.crosspoint == null) {
        cellsize = 1000000 / 32;
        preloadPosition(mwp.waypoint, cellsize);
      }
    }

    if (first_file_access_failed) {
      throw new IllegalArgumentException("datafile " + first_file_access_name + " not found");
    }
    int len = unmatchedWaypoints.size();
    for (int i = 0; i < len; i++) {
      MatchedWaypoint mwp = unmatchedWaypoints.get(i);
      if (mwp.crosspoint == null) {
        if (unmatchedWaypoints.size() > 1 && i == unmatchedWaypoints.size() - 1 && unmatchedWaypoints.get(i - 1).direct) {
          mwp.crosspoint = new OsmNode(mwp.waypoint.ilon, mwp.waypoint.ilat);
          mwp.direct = true;
        } else {
          throw new IllegalArgumentException(mwp.name + "-position not mapped in existing datafile");
        }
      }
      if (unmatchedWaypoints.size() > 1 && i == unmatchedWaypoints.size() - 1 && unmatchedWaypoints.get(i - 1).direct) {
        mwp.crosspoint = new OsmNode(mwp.waypoint.ilon, mwp.waypoint.ilat);
        mwp.direct = true;
      }
    }
  }

  private void preloadPosition(OsmNode n, int d) {
    first_file_access_failed = false;
    first_file_access_name = null;
    loadSegmentFor(n.ilon, n.ilat);
    if (first_file_access_failed) {
      throw new IllegalArgumentException("datafile " + first_file_access_name + " not found");
    }
    for (int idxLat = -1; idxLat <= 1; idxLat++)
      for (int idxLon = -1; idxLon <= 1; idxLon++) {
        if (idxLon != 0 || idxLat != 0) {
          loadSegmentFor(n.ilon + d * idxLon, n.ilat + d * idxLat);
        }
      }
  }

  private OsmFile fileForSegment(int lonDegree, int latDegree) throws Exception {
    int lonMod5 = lonDegree % 5;
    int latMod5 = latDegree % 5;

    int lon = lonDegree - 180 - lonMod5;
    String slon = lon < 0 ? "W" + (-lon) : "E" + lon;
    int lat = latDegree - 90 - latMod5;

    String slat = lat < 0 ? "S" + (-lat) : "N" + lat;
    String filenameBase = slon + "_" + slat;

    currentFileName = filenameBase + ".rd5";

    PhysicalFile ra = null;
    if (!fileCache.containsKey(filenameBase)) {
      File f = null;
      if (!forceSecondaryData) {
        File primary = new File(segmentDir, filenameBase + ".rd5");
        if (primary.exists()) {
          f = primary;
        }
      }
      if (f == null) {
        File secondary = new File(secondarySegmentsDir, filenameBase + ".rd5");
        if (secondary.exists()) {
          f = secondary;
        }
      }
      if (f != null) {
        currentFileName = f.getName();
        ra = new PhysicalFile(f, dataBuffers, lookupVersion, lookupMinorVersion);
      }
      fileCache.put(filenameBase, ra);
    }
    ra = fileCache.get(filenameBase);
    OsmFile osmf = new OsmFile(ra, lonDegree, latDegree, dataBuffers);

    if (first_file_access_name == null) {
      first_file_access_name = currentFileName;
      first_file_access_failed = osmf.filename == null;
    }

    return osmf;
  }

  public void close() {
    for (PhysicalFile f : fileCache.values()) {
      try {
        if (f != null)
          f.ra.close();
      } catch (IOException ioe) {
        // ignore
      }
    }
  }
}
