
package app.crossword.yourealwaysbe.forkyz.util.files;

import java.time.LocalDate;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import android.content.Context;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.room.ColumnInfo;
import androidx.room.Dao;
import androidx.room.Database;
import androidx.room.Delete;
import androidx.room.Entity;
import androidx.room.Insert;
import androidx.room.OnConflictStrategy;
import androidx.room.PrimaryKey;
import androidx.room.Query;
import androidx.room.Room;
import androidx.room.RoomDatabase;
import androidx.room.Transaction;
import androidx.room.TypeConverter;
import androidx.room.TypeConverters;
import androidx.room.migration.Migration;
import androidx.sqlite.db.SupportSQLiteDatabase;

import app.crossword.yourealwaysbe.forkyz.util.AppPuzzleUtils;
import app.crossword.yourealwaysbe.puz.Puzzle;

public class MetaCache {

    public static class Converters {
        // need nullable not just non-null now
        // https://stackoverflow.com/a/77888612

        @TypeConverter
        @Nullable
        public static LocalDate fromEpochDay(@Nullable Long value) {
            return value == null ? null : LocalDate.ofEpochDay(value);
        }

        @TypeConverter
        @Nullable
        public static Long dateToEpochDay(@Nullable LocalDate date) {
            return date == null ? null : date.toEpochDay();
        }

        @TypeConverter
        @Nullable
        public static Uri fromUriString(@Nullable String value) {
            return value == null ? null : Uri.parse(value);
        }

        @TypeConverter
        @Nullable
        public static String uriToString(@Nullable Uri uri) {
            return uri == null ? null : uri.toString();
        }
    }

    @Entity(tableName = "cachedMeta")
    @TypeConverters({Converters.class})
    public static class CachedMeta {
        @PrimaryKey
        @NonNull
        public Uri mainFileUri;

        // currently unused but may be used to speed up dir listing in
        // browse activity
        @ColumnInfo
        public Uri metaFileUri;

        @ColumnInfo
        @NonNull
        public Uri directoryUri;

        // this field is now defunct, remove on next schema change
        @ColumnInfo
        public boolean isUpdatable;

        @ColumnInfo
        @Nullable
        public LocalDate date;

        @ColumnInfo
        public int percentComplete;

        @ColumnInfo
        public int percentFilled;

        @ColumnInfo
        @Nullable
        public String source;

        @ColumnInfo
        @Nullable
        public String title;

        // from db version 2
        @ColumnInfo
        @Nullable
        public String author;

        // from db version 3
        @ColumnInfo
        public char rating;

        // from db version 4
        @ColumnInfo
        public boolean hasSolution;
    }

    @Dao
    @TypeConverters({Converters.class})
    public static abstract class CachedMetaDao {
        @Query("SELECT * FROM cachedMeta WHERE directoryUri = :directory")
        public abstract List<CachedMeta> getDirCache(Uri directory);

        @Query("SELECT * FROM cachedMeta WHERE mainFileUri = :mainFileUri")
        public abstract CachedMeta getCache(Uri mainFileUri);

        @Query("SELECT mainFileUri FROM cachedMeta WHERE directoryUri = :directory")
        public abstract List<Uri> getDirFileUris(Uri directory);

        @Insert(onConflict = OnConflictStrategy.REPLACE)
        public abstract void insertAll(CachedMeta... metas);

        @Query("DELETE FROM cachedMeta WHERE mainFileUri = :mainFileUri")
        public abstract void delete(Uri mainFileUri);

        @Delete
        public abstract void delete(CachedMeta cm);

        /**
         * Delete files in dir not in puzMetaFiles
         */
        @Transaction
        public void deleteOutside(
            Uri directory, Collection<PuzMetaFile> puzMetaFiles
        ) {
            Set<Uri> keepUris = new HashSet<>(puzMetaFiles.size());
            for (PuzMetaFile pm : puzMetaFiles) {
                keepUris.add(pm.getPuzHandle().getMainFileHandle().getUri());
            }

            for (CachedMeta cm : getDirCache(directory)) {
                if (!keepUris.contains(cm.mainFileUri)) {
                    delete(cm);
                }
            }
        }
    }

    @Database(entities = {CachedMeta.class}, version = 4)
    public static abstract class CachedMetaDB extends RoomDatabase {
        private static CachedMetaDB instance = null;

        /**
         * Version 2 adds an author column
         *
         * Thanks to
         * https://medium.com/androiddevelopers/understanding-migrations-with-room-f01e04b07929
         */
        private static final Migration MIGRATION_l_2 = new Migration(1, 2) {
            @Override
            public void migrate(SupportSQLiteDatabase database) {
                database.execSQL(
                    "ALTER TABLE cachedMeta ADD COLUMN author TEXT"
                );
            }
        };

        private static final Migration MIGRATION_2_3 = new Migration(2, 3) {
            @Override
            public void migrate(SupportSQLiteDatabase database) {
                database.execSQL(
                    "ALTER TABLE cachedMeta "
                    + "ADD COLUMN rating INTEGER NOT NULL DEFAULT 0"
                );
            }
        };

        private static final Migration MIGRATION_3_4 = new Migration(3, 4) {
            @Override
            public void migrate(SupportSQLiteDatabase database) {
                database.execSQL(
                    "ALTER TABLE cachedMeta "
                    + "ADD COLUMN hasSolution INTEGER NOT NULL DEFAULT 0"
                );
            }
        };

        public static CachedMetaDB getInstance(Context applicationContext) {
            if (instance == null) {
                instance = Room.databaseBuilder(
                    applicationContext, CachedMetaDB.class, "meta-cache-db"
                ).addMigrations(MIGRATION_l_2, MIGRATION_2_3, MIGRATION_3_4)
                .build();
            }
            return instance;
        }

        public abstract CachedMetaDao cachedMetaDao();
    }

    public class MetaRecord {
        private CachedMeta dbRow;

        public MetaRecord(CachedMeta dbRow) {
            this.dbRow = dbRow;
        }

        // updatable field now defunct
        public boolean isUpdatable() { return dbRow.isUpdatable; }
        public String getCaption() { return dbRow.title; }
        public LocalDate getDate() { return dbRow.date; }
        public int getPercentComplete() { return dbRow.percentComplete; }
        public int getPercentFilled() { return dbRow.percentFilled; }
        public String getSource() { return dbRow.source; }
        public String getTitle() { return dbRow.title; }
        public String getAuthor() { return dbRow.author; }
        public char getRating() { return dbRow.rating; }
        public boolean hasSolution() {
            // because old puzzles have false as the default infer from percent
            // complete
            return dbRow.hasSolution || dbRow.percentComplete > 0;
        }
    }

    private Context applicationContext;

    public MetaCache(Context applicationContext) {
        this.applicationContext = applicationContext;
    }

    /**
     * Returns all cached meta data records for the given directory
     */
    public Map<Uri, MetaRecord> getDirCache(
        FileHandler fileHandler,
        DirHandle dirHandle
    ) {
        Uri dirUri = fileHandler.getUri(dirHandle);
        Map<Uri, MetaRecord> cache = new HashMap<>();
        for (CachedMeta cm : getDao().getDirCache(dirUri)) {
            cache.put(cm.mainFileUri, new MetaRecord(cm));
        }
        return  cache;
    }

    /**
     * Return cached meta for given handle
     *
     * @return null if no entry in cache
     */
    public MetaRecord getCache(Uri puzFileUri) {
        CachedMeta cm = getDao().getCache(puzFileUri);
        return (cm == null) ? null : new MetaRecord(cm);
    }

    /**
     * Cache meta for a file URI, returns new record
     *
     * Uses derived date if puz has no date.
     */
    public MetaRecord addRecord(
        FileHandler fileHandler,
        PuzHandle puzHandle,
        Puzzle puz
    ) {
        FileHandle mainFileHandle = puzHandle.getMainFileHandle();
        FileHandle metaHandle
            = puzHandle.accept(new PuzHandle.Visitor<FileHandle>() {
                public FileHandle visit(PuzHandle.Puz puzPH) {
                    return puzPH.getMetaFileHandle();
                }
                public FileHandle visit(PuzHandle.IPuz ipuzPH) {
                    return null;
                }
            });
        String mainFileName = fileHandler.getName(mainFileHandle);
        LocalDate date = AppPuzzleUtils.deriveDate(puz.getDate(), mainFileName);
        if (date == null)
            date = fileHandler.getModifiedDate(mainFileHandle);

        CachedMeta cm = new CachedMeta();
        cm.mainFileUri = fileHandler.getUri(mainFileHandle);
        cm.metaFileUri = (metaHandle == null)
            ? null
            : fileHandler.getUri(metaHandle);
        cm.directoryUri = fileHandler.getUri(puzHandle.getDirHandle());
        cm.isUpdatable = true; // used to be a puzzle field
        cm.date = date;
        cm.percentComplete = puz.getPercentComplete();
        cm.percentFilled = puz.getPercentFilled();
        cm.source = puz.getSource();
        cm.title = puz.getTitle();
        cm.author = puz.getAuthor();
        cm.rating = puz.getRating();
        cm.hasSolution = puz.hasSolution();

        getDao().insertAll(cm);

        return new MetaRecord(cm);
    }

    /**
     * Remove a record from the cache
     */
    public void deleteRecord(FileHandler fileHandler, PuzHandle puzHandle) {
        getDao().delete(fileHandler.getUri(puzHandle.getMainFileHandle()));
    }

    /**
     * Remove all records from directory that are not in the handles
     */
    public void cleanupCache(
        FileHandler fileHandler, DirHandle dir, List<PuzMetaFile> puzMetaFiles
    ) {
        Uri dirUri = fileHandler.getUri(dir);
        getDao().deleteOutside(dirUri, puzMetaFiles);
    }

    private CachedMetaDao getDao() {
        return CachedMetaDB.getInstance(applicationContext).cachedMetaDao();
    }
}
