content: /*
   * Copyright 2013 Hannes Janetzek
   * Copyright 2016 Stephan Leuschner
   * Copyright 2016 devemux86
   *
   * This file is part of the OpenScienceMap project (http://www.opensciencemap.org).
   *
   * This program is free software: you can redistribute it and/or modify it under the
   * terms of the GNU Lesser 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 Lesser General Public License for more details.
   *
   * You should have received a copy of the GNU Lesser General Public License along with
   * this program. If not, see <http://www.gnu.org/licenses/>.
   */
  package org.oscim.android.cache;
  
  import android.annotation.TargetApi;
  import android.content.Context;
  import android.database.Cursor;
  import android.database.sqlite.SQLiteDatabase;
  import android.database.sqlite.SQLiteDoneException;
  import android.database.sqlite.SQLiteOpenHelper;
  import android.database.sqlite.SQLiteStatement;
  import android.os.Build;
  import android.os.ParcelFileDescriptor;
  
  import org.oscim.core.Tile;
  import org.oscim.tiling.ITileCache;
  import org.slf4j.LoggerFactory;
  
  import java.io.ByteArrayInputStream;
  import java.io.ByteArrayOutputStream;
  import java.io.File;
  import java.io.FileInputStream;
  import java.io.InputStream;
  import java.io.OutputStream;
  import java.util.ArrayList;
  
  public class TileCache implements ITileCache {
  
      static final org.slf4j.Logger log = LoggerFactory.getLogger(TileCache.class);
      static final boolean dbg = false;
  
      class CacheTileReader implements TileReader {
          final InputStream mInputStream;
          final Tile mTile;
  
          public CacheTileReader(Tile tile, InputStream is) {
              mTile = tile;
              mInputStream = is;
          }
  
          @Override
          public Tile getTile() {
              return mTile;
          }
  
          @Override
          public InputStream getInputStream() {
              return mInputStream;
          }
      }
  
      class CacheTileWriter implements TileWriter {
          final ByteArrayOutputStream mOutputStream;
          final Tile mTile;
  
          CacheTileWriter(Tile tile, ByteArrayOutputStream os) {
              mTile = tile;
              mOutputStream = os;
          }
  
          @Override
          public Tile getTile() {
              return mTile;
          }
  
          @Override
          public OutputStream getOutputStream() {
              return mOutputStream;
          }
  
          @Override
          public void complete(boolean success) {
              saveTile(mTile, mOutputStream, success);
          }
      }
  
      private final ArrayList<ByteArrayOutputStream> mCacheBuffers;
      private final SQLiteHelper dbHelper;
      private final SQLiteDatabase mDatabase;
      private final SQLiteStatement mStmtGetTile;
      private final SQLiteStatement mStmtPutTile;
  
      //private final SQLiteStatement mStmtUpdateTile;
  
      public void dispose() {
          if (mDatabase.isOpen())
              mDatabase.close();
      }
  
      @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
      public TileCache(Context context, String cacheDirectory, String dbName) {
          if (dbg)
              log.debug("open cache {}, {}", cacheDirectory, dbName);
  
          if (cacheDirectory != null)
              dbName = new File(cacheDirectory, dbName).getAbsolutePath();
          dbHelper = new SQLiteHelper(context, dbName);
  
          if (Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN)
              dbHelper.setWriteAheadLoggingEnabled(true);
  
          mDatabase = dbHelper.getWritableDatabase();
  
          mStmtGetTile = mDatabase.compileStatement("" +
                  "SELECT " + COLUMN_DATA +
                  " FROM " + TABLE_NAME +
                  " WHERE x=? AND y=? AND z = ?");
  
          mStmtPutTile = mDatabase.compileStatement("" +
                  "INSERT INTO " + TABLE_NAME +
                  " (x, y, z, time, last_access, data)" +
                  " VALUES(?,?,?,?,?,?)");
  
          //mStmtUpdateTile = mDatabase.compileStatement("" +
          //        "UPDATE " + TABLE_NAME +
          //        "  SET last_access=?" +
          //        "  WHERE x=? AND y=? AND z=?");
  
          mCacheBuffers = new ArrayList<ByteArrayOutputStream>();
      }
  
      @Override
      public TileWriter writeTile(Tile tile) {
          ByteArrayOutputStream os;
  
          synchronized (mCacheBuffers) {
              if (mCacheBuffers.size() == 0)
                  os = new ByteArrayOutputStream(32 * 1024);
              else
                  os = mCacheBuffers.remove(mCacheBuffers.size() - 1);
          }
          return new CacheTileWriter(tile, os);
      }
  
      static final String TABLE_NAME = "tiles";
      static final String COLUMN_TIME = "time";
      static final String COLUMN_ACCESS = "last_access";
      static final String COLUMN_DATA = "data";
  
      //static final String COLUMN_SIZE = "size";
  
      class SQLiteHelper extends SQLiteOpenHelper {
  
          //private static final String DATABASE_NAME = "tile.db";
          private static final int DATABASE_VERSION = 1;
  
          private static final String TILE_SCHEMA =
                  "CREATE TABLE "
                          + TABLE_NAME + "("
                          + "x INTEGER NOT NULL,"
                          + "y INTEGER NOT NULL,"
                          + "z INTEGER NOT NULL,"
                          + COLUMN_TIME + " LONG NOT NULL,"
                          //+ COLUMN_SIZE + " LONG NOT NULL,"
                          + COLUMN_ACCESS + " LONG NOT NULL,"
                          + COLUMN_DATA + " BLOB,"
                          + "PRIMARY KEY(x,y,z));";
  
          public SQLiteHelper(Context context, String dbName) {
              super(context, dbName, null, DATABASE_VERSION);
          }
  
          @Override
          public void onCreate(SQLiteDatabase db) {
              log.debug("create table");
              db.execSQL(TILE_SCHEMA);
          }
  
          @Override
          public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
              log.debug("drop table");
              db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME);
              onCreate(db);
          }
  
          @Override
          public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
              onUpgrade(db, oldVersion, newVersion);
          }
      }
  
      public void saveTile(Tile tile, ByteArrayOutputStream data, boolean success) {
          byte[] bytes = null;
  
          if (success)
              bytes = data.toByteArray();
  
          synchronized (mCacheBuffers) {
              data.reset();
              mCacheBuffers.add(data);
          }
  
          if (dbg)
              log.debug("store tile {} {}", tile, Boolean.valueOf(success));
  
          if (!success)
              return;
  
          synchronized (mStmtPutTile) {
              mStmtPutTile.bindLong(1, tile.tileX);
              mStmtPutTile.bindLong(2, tile.tileY);
              mStmtPutTile.bindLong(3, tile.zoomLevel);
              mStmtPutTile.bindLong(4, 0);
              mStmtPutTile.bindLong(5, 0);
              mStmtPutTile.bindBlob(6, bytes);
  
              mStmtPutTile.execute();
              mStmtPutTile.clearBindings();
          }
      }
  
      @TargetApi(Build.VERSION_CODES.HONEYCOMB)
      public TileReader getTileApi11(Tile tile) {
          InputStream in = null;
  
          mStmtGetTile.bindLong(1, tile.tileX);
          mStmtGetTile.bindLong(2, tile.tileY);
          mStmtGetTile.bindLong(3, tile.zoomLevel);
  
          try {
              ParcelFileDescriptor result = mStmtGetTile.simpleQueryForBlobFileDescriptor();
              in = new FileInputStream(result.getFileDescriptor());
          } catch (SQLiteDoneException e) {
              log.debug("not in cache {}", tile);
              return null;
          } finally {
              mStmtGetTile.clearBindings();
          }
  
          if (dbg)
              log.debug("load tile {}", tile);
  
          return new CacheTileReader(tile, in);
      }
  
      private final String[] mQueryVals = new String[3];
  
      @Override
      public synchronized TileReader getTile(Tile tile) {
  
          //if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.HONEYCOMB)
          //    return getTileApi11(tile);
  
          mQueryVals[0] = String.valueOf(tile.zoomLevel);
          mQueryVals[1] = String.valueOf(tile.tileX);
          mQueryVals[2] = String.valueOf(tile.tileY);
  
          Cursor cursor = mDatabase.rawQuery("SELECT " + COLUMN_DATA +
                  " FROM " + TABLE_NAME +
                  " WHERE z=? AND x=? AND y=?", mQueryVals);
  
          if (!cursor.moveToFirst()) {
              if (dbg)
                  log.debug("not in cache {}", tile);
  
              cursor.close();
              return null;
          }
  
          InputStream in = new ByteArrayInputStream(cursor.getBlob(0));
          cursor.close();
  
          if (dbg)
              log.debug("load tile {}", tile);
  
          return new CacheTileReader(tile, in);
      }
  
      @Override
      public void setCacheSize(long size) {
      }
  }
  