/*
   * Adds a feature layer to the geopackage.
   */
  void addFeatureLayer(
      GeoPackage geopkg, FeatureLayer layer, MapLayerInfo mapLayer, WMSMapContent map)
      throws IOException {

    FeatureEntry e = new FeatureEntry();
    initEntry(e, layer, mapLayer, map);

    Filter filter = layer.getQuery().getFilter();
    GeometryDescriptor gd = mapLayer.getFeature().getFeatureType().getGeometryDescriptor();
    if (gd != null) {
      Envelope bnds = bounds(map);
      BBOX bboxFilter =
          filterFactory.bbox(
              gd.getLocalName(),
              bnds.getMinX(),
              bnds.getMinY(),
              bnds.getMaxX(),
              bnds.getMaxY(),
              map.getRequest().getSRS());
      filter = filterFactory.and(filter, bboxFilter);
    }

    LOGGER.fine("Creating feature entry" + e.getTableName());
    geopkg.add(e, layer.getSimpleFeatureSource(), filter);
  }
  void addCoverageLayer(
      GeoPackage geopkg, GridCoverageLayer layer, MapLayerInfo mapLayer, WMSMapContent map)
      throws IOException {

    RasterEntry e = new RasterEntry();
    initEntry(e, layer, mapLayer, map);

    // TODO: ensure this is one of the supported formats
    AbstractGridFormat format = mapLayer.getCoverage().getStore().getFormat();

    LOGGER.fine("Creating raster entry" + e.getTableName());
    geopkg.add(e, layer.getCoverage(), format);
  }
  void addTileLayers(GeoPackage geopkg, List<MapLayerInfo> mapLayers, WMSMapContent map)
      throws IOException {

    if (mapLayers.isEmpty()) {
      return;
    }

    // figure out a name for the file entry
    String tileEntryName = null;
    Map formatOpts = map.getRequest().getFormatOptions();
    if (formatOpts.containsKey("tileset_name")) {
      tileEntryName = (String) formatOpts.get("tileset_name");
    }
    if (tileEntryName == null) {
      tileEntryName = map.getTitle();
    }
    if (tileEntryName == null && mapLayers.size() == 1) {
      Iterator<MapLayerInfo> it = mapLayers.iterator();
      tileEntryName = it.next().getLayerInfo().getName();
    }

    GridSubset gridSubset = findBestGridSubset(map);
    int[] minmax = findMinMaxZoom(gridSubset, map);

    BoundingBox bbox = bbox(map);

    TileEntry e = new TileEntry();
    e.setTableName(tileEntryName);

    if (mapLayers.size() == 1) {
      ResourceInfo r = mapLayers.get(0).getResource();
      e.setIdentifier(r.getTitle());
      e.setDescription(r.getAbstract());
    }
    e.setBounds(
        new ReferencedEnvelope(
            findTileBounds(gridSubset, bbox, minmax[0]), map.getCoordinateReferenceSystem()));
    e.setSrid(srid(map));

    GridSet gridSet = gridSubset.getGridSet();
    for (int z = minmax[0]; z < minmax[1]; z++) {
      Grid g = gridSet.getGrid(z);

      TileMatrix m = new TileMatrix();
      m.setZoomLevel(z);
      m.setMatrixWidth((int) g.getNumTilesWide());
      m.setMatrixHeight((int) g.getNumTilesHigh());
      m.setTileWidth(gridSubset.getTileWidth());
      m.setTileHeight(gridSubset.getTileHeight());

      // TODO: not sure about this
      m.setXPixelSize(g.getResolution());
      m.setYPixelSize(g.getResolution());
      // m.setXPixelSize(gridSet.getPixelSize());
      // m.setYPixelSize(gridSet.getPixelSize());

      e.getTileMatricies().add(m);
    }

    // figure out the actual bounds of the tiles to be renderered
    LOGGER.fine("Creating tile entry" + e.getTableName());
    geopkg.create(e);

    // create a prototype getmap request
    GetMapRequest req = new GetMapRequest();
    OwsUtils.copy(map.getRequest(), req, GetMapRequest.class);
    req.setLayers(mapLayers);

    String imageFormat =
        formatOpts.containsKey("format") ? parseFormatFromOpts(formatOpts) : findBestFormat(map);

    req.setFormat(imageFormat);
    req.setWidth(gridSubset.getTileWidth());
    req.setHeight(gridSubset.getTileHeight());

    // count tiles as we generate them
    int ntiles = 0;

    // flag determining if tile row indexes we store in database should be inverted
    boolean flipy = Boolean.valueOf((String) formatOpts.get("flipy"));
    for (int z = minmax[0]; z < minmax[1]; z++) {
      long[] intersect = gridSubset.getCoverageIntersection(z, bbox);
      for (long x = intersect[0]; x <= intersect[2]; x++) {
        for (long y = intersect[1]; y <= intersect[3]; y++) {
          BoundingBox box = gridSubset.boundsFromIndex(new long[] {x, y, z});
          req.setBbox(new Envelope(box.getMinX(), box.getMaxX(), box.getMinY(), box.getMaxY()));

          Tile t = new Tile();
          t.setZoom(z);
          t.setColumn((int) x);
          t.setRow((int) (flipy ? gridSubset.getNumTilesHigh(z) - (y + 1) : y));

          WebMap result = webMapService.getMap(req);
          t.setData(toBytes(result));

          geopkg.add(e, t);

          // images we encode are actually kept around, we need to clean them up
          if (ntiles++ == TILE_CLEANUP_INTERVAL) {
            cleanUpImages();
            ntiles = 0;
          }
        }
      }
    }
  }
  @Override
  public WebMap produceMap(WMSMapContent map) throws ServiceException, IOException {
    GeoPackage geopkg = new GeoPackage();
    geopkg.init();

    GetMapRequest req = map.getRequest();

    List<Layer> layers = map.layers();
    List<MapLayerInfo> mapLayers = req.getLayers();

    Preconditions.checkState(
        layers.size() == mapLayers.size(),
        "Number of map layers not same as number of rendered layers");

    // list of layers to render directly and include as tiles
    List<MapLayerInfo> tileLayers = new ArrayList();

    // check mode, one of:
    // vector - render vector layers as feature entries and all else as tiles (default)
    // hybrid - render vector layers as feature entries, raster layers as raster entries, all
    //          others as tile entries
    // tiled - all layers as a single tile set
    Map formatOpts = req.getFormatOptions();
    Mode mode =
        formatOpts.containsKey("mode")
            ? Mode.valueOf(((String) formatOpts.get("mode")).toUpperCase())
            : Mode.VECTOR;

    if (mode == Mode.TILED) {
      // tiled mode means render all as map tile layer
      tileLayers.addAll(mapLayers);
    } else {

      // hybrid mode, dump as raw vector or raster unless the request specifically asks for a
      // layer to be rendered as tiles
      for (int i = 0; i < layers.size(); i++) {
        Layer layer = layers.get(i);
        MapLayerInfo mapLayer = mapLayers.get(i);

        if (layer instanceof FeatureLayer) {
          addFeatureLayer(geopkg, (FeatureLayer) layer, mapLayer, map);
        } else if (layer instanceof GridCoverageLayer) {
          if (mode == Mode.HYBRID) {
            addCoverageLayer(geopkg, (GridCoverageLayer) layer, mapLayer, map);
          } else {
            tileLayers.add(mapLayer);
          }
        } else {
          tileLayers.add(mapLayer);
        }
      }
    }

    addTileLayers(geopkg, tileLayers, map);

    geopkg.close();

    final File dbFile = geopkg.getFile();
    final BufferedInputStream bin = new BufferedInputStream(new FileInputStream(dbFile));

    RawMap result =
        new RawMap(map, bin, MIME_TYPE) {
          @Override
          public void writeTo(OutputStream out) throws IOException {
            String dbFilename = getAttachmentFileName();
            if (dbFilename != null) {
              dbFilename = dbFilename.substring(0, dbFilename.length() - 4) + ".gpkg";
            } else {
              // this shouldn't really ever happen, but fallback anyways
              dbFilename = "geoserver.gpkg";
            }

            IOUtils.copy(bin, out);
            out.flush();

            //               JD: disabling zip compression for now
            //                ZipOutputStream zout = new ZipOutputStream(out);
            //                zout.putNextEntry(new ZipEntry(dbFilename));
            //
            //                super.writeTo(zout);
            //                zout.closeEntry();
            //                zout.close();

            bin.close();
            try {
              dbFile.delete();
            } catch (Exception e) {
              LOGGER.log(Level.WARNING, "Error deleting file: " + dbFile.getAbsolutePath(), e);
            }
          }
        };

    result.setContentDispositionHeader(map, ".gpkg", true);
    return result;
  }