/**
 * SqlJetOptions.java
 * Copyright (C) 2009-2013 TMate Software Ltd
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; version 2 of the License.
 *
 * 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 General Public License for more details.
 *
 * For information on how to redistribute this software under
 * the terms of a license other than GNU General Public License
 * contact TMate Software at support@sqljet.com
 */
package org.tmatesoft.sqljet.core.internal.table;

import org.tmatesoft.sqljet.core.SqlJetEncoding;
import org.tmatesoft.sqljet.core.SqlJetErrorCode;
import org.tmatesoft.sqljet.core.SqlJetException;
import org.tmatesoft.sqljet.core.SqlJetTransactionMode;
import org.tmatesoft.sqljet.core.internal.ISqlJetBtree;
import org.tmatesoft.sqljet.core.internal.ISqlJetDbHandle;
import org.tmatesoft.sqljet.core.internal.ISqlJetLimits;
import org.tmatesoft.sqljet.core.internal.SqlJetAutoVacuumMode;
import org.tmatesoft.sqljet.core.internal.pager.SqlJetPageCache;
import org.tmatesoft.sqljet.core.table.ISqlJetOptions;

/**
 * @author TMate Software Ltd.
 * @author Sergey Scherbina (sergey.scherbina@gmail.com)
 *
 */
public class SqlJetOptions implements ISqlJetOptions {

    private static final int SCHEMA_COOKIE = 1;

    private static final int FILE_FORMAT = 2;

    private static final int PAGE_CACHE_SIZE = 3;

    private static final int AUTOVACUUM = 4;

    private static final int ENCODING = 5;

    private static final int USER_COOKIE = 6;

    private static final int INCREMENTAL_VACUUM = 7;

    private final ISqlJetBtree btree;
    private final ISqlJetDbHandle dbHandle;

    /**
     * Schema cookie. Changes with each schema change.
     */
    private int schemaCookie;

    /**
     * File format of schema layer.
     */
    private int fileFormat = SQLJET_DEFAULT_FILE_FORMAT;

    /**
     * Size of the page cache.
     */
    private int pageCacheSize = SqlJetPageCache.PAGE_CACHE_SIZE_DEFAULT;

    /**
     * Use freelist if false. Autovacuum if true.
     */
    private boolean autovacuum = ISqlJetBtree.SQLJET_DEFAULT_AUTOVACUUM != SqlJetAutoVacuumMode.NONE;

    /**
     * Db text encoding.
     */
    private SqlJetEncoding encoding = SQLJET_DEFAULT_ENCODING;

    /**
     * The user cookie. Used by the application.
     */
    private int userCookie;

    /**
     * Incremental-vacuum flag.
     */
    private boolean incrementalVacuum = ISqlJetBtree.SQLJET_DEFAULT_AUTOVACUUM == SqlJetAutoVacuumMode.INCR;

    public SqlJetOptions(ISqlJetBtree btree, ISqlJetDbHandle dbHandle) throws SqlJetException {
        this.btree = btree;
        this.dbHandle = dbHandle;
        if (readSchemaCookie() == 0) {
            try{
                initMeta();
            } catch(SqlJetException e) {
                if(SqlJetErrorCode.READONLY!=e.getErrorCode()) {
                    throw e;
                }
                // we can't init meta on read-only file ...
                // but if you want, we could just don't worry.
            }
        } else {
            readMeta();
        }
    }

    /**
     * Get the database meta information.
     *
     * Meta values are as follows:
     *
     * <table border="1">
     * <tr>
     * <td>meta[1]</td>
     * <td>Schema cookie. Changes with each schema change.</td>
     * </tr>
     * <tr>
     * <td>meta[2]</td>
     * <td>File format of schema layer.</td>
     * </tr>
     * <tr>
     * <td>meta[3]</td>
     * <td>Size of the page cache.</td>
     * </tr>
     * <tr>
     * <td>meta[4]</td>
     * <td>Use freelist if 0. Autovacuum if greater than zero.</td>
     * </tr>
     * <tr>
     * <td>meta[5]</td>
     * <td>Db text encoding. 1:UTF-8 2:UTF-16LE 3:UTF-16BE</td>
     * </tr>
     * <tr>
     * <td>meta[6]</td>
     * <td>The user cookie. Used by the application.</td>
     * </tr>
     * <tr>
     * <td>meta[7]</td>
     * <td>Incremental-vacuum flag.</td>
     * </tr>
     * </table>
     *
     * @throws SqlJetException
     *
     */
    private void readMeta() throws SqlJetException {
        schemaCookie = readSchemaCookie();
        autovacuum = readAutoVacuum();
        fileFormat = readFileFormat();
        incrementalVacuum = readIncrementalVacuum();
        userCookie = readUserCookie();
        pageCacheSize = readPageCacheSize();
        encoding = readEncoding();
    }

    public String toString() {
        StringBuffer sb = new StringBuffer();
        sb.append("ENCODING: " + encoding + "\n");
        sb.append("SCHEMA VERSION: " + schemaCookie + "\n");
        sb.append("USER VERSION: " + userCookie + "\n");
        sb.append("FILE FORMAT: " + fileFormat + "\n");
        sb.append("AUTOVACUUM: " + autovacuum + "\n");
        sb.append("CACHE SIZE: " + pageCacheSize);
        return sb.toString();
    }

    private SqlJetEncoding readEncoding() throws SqlJetException {
        switch (btree.getMeta(ENCODING)) {
        case 0:
            if (readSchemaCookie() != 0)
                throw new SqlJetException(SqlJetErrorCode.CORRUPT);
        case 1:
            return SqlJetEncoding.UTF8;
        case 2:
            return SqlJetEncoding.UTF16LE;
        case 3:
            return SqlJetEncoding.UTF16BE;
        default:
            throw new SqlJetException(SqlJetErrorCode.CORRUPT);
        }
    }

    private boolean readIncrementalVacuum() throws SqlJetException {
        return btree.getMeta(INCREMENTAL_VACUUM) != 0;
    }

    private int readUserCookie() throws SqlJetException {
        return btree.getMeta(USER_COOKIE);
    }

    private boolean readAutoVacuum() throws SqlJetException {
        return btree.getMeta(AUTOVACUUM) != 0;
    }

    private int readPageCacheSize() throws SqlJetException {
        int meta = btree.getMeta(PAGE_CACHE_SIZE);
        return meta > 0 ? meta : ISqlJetLimits.SQLJET_DEFAULT_CACHE_SIZE;
    }

    private int readFileFormat() throws SqlJetException {
        final int fileFormat = btree.getMeta(FILE_FORMAT);
        checkFileFormat(fileFormat);
        return fileFormat;
    }

    private void checkFileFormat(final int fileFormat) throws SqlJetException {
        if (fileFormat < ISqlJetLimits.SQLJET_MIN_FILE_FORMAT || fileFormat > ISqlJetLimits.SQLJET_MAX_FILE_FORMAT)
            throw new SqlJetException(SqlJetErrorCode.CORRUPT);
    }

    private int readSchemaCookie() throws SqlJetException {
        return btree.getMeta(SCHEMA_COOKIE);
    }

    public int getSchemaVersion() throws SqlJetException {
        return schemaCookie;
    }

    public int getFileFormat() throws SqlJetException {
        return fileFormat;
    }

    public int getCacheSize() throws SqlJetException {
        return pageCacheSize;
    }

    public boolean isAutovacuum() throws SqlJetException {
        return autovacuum;
    }

    public SqlJetEncoding getEncoding() throws SqlJetException {
        return encoding;
    }

    public boolean isLegacyFileFormat() throws SqlJetException {
        return fileFormat==ISqlJetLimits.SQLJET_MIN_FILE_FORMAT;
    }

    public void setLegacyFileFormat(boolean flag) throws SqlJetException {
        fileFormat=flag?ISqlJetLimits.SQLJET_MIN_FILE_FORMAT:ISqlJetLimits.SQLJET_MAX_FILE_FORMAT;
    }

    public int getUserVersion() throws SqlJetException {
        return userCookie;
    }

    public boolean isIncrementalVacuum() throws SqlJetException {
        return incrementalVacuum;
    }

    public void setSchemaVersion(int version) throws SqlJetException {
        dbHandle.getMutex().enter();
        try {
            if (!btree.isInTrans())
                throw new SqlJetException("It can be performed only in active transaction");
            verifySchemaVersion(true);
            writeSchemaCookie(this.schemaCookie = version);
        } finally {
            dbHandle.getMutex().leave();
        }
    }

    public boolean verifySchemaVersion(boolean throwIfStale) throws SqlJetException {
        dbHandle.getMutex().enter();
        try {
            final boolean stale = (schemaCookie != btree.getMeta(1));
            if (stale && throwIfStale) {
                throw new SqlJetException(SqlJetErrorCode.SCHEMA);
            }
            return !stale;
        } finally {
            dbHandle.getMutex().leave();
        }
    }

    public void changeSchemaVersion() throws SqlJetException {
        dbHandle.getMutex().enter();
        try {
            if (!btree.isInTrans())
                throw new SqlJetException("It can be performed only in active transaction");
            verifySchemaVersion(true);
            schemaCookie++;
            writeSchemaCookie(schemaCookie);
        } finally {
            dbHandle.getMutex().leave();
        }
    }

    private void initMeta() throws SqlJetException {
        final boolean inTrans = btree.isInTrans();
        final SqlJetTransactionMode transMode = btree.getTransMode();
        try{
        if(!inTrans || transMode!=SqlJetTransactionMode.EXCLUSIVE) {
            btree.beginTrans(SqlJetTransactionMode.EXCLUSIVE);
        }
        try {
            schemaCookie = 1;
            writeSchemaCookie(schemaCookie);
            writeFileFormat(fileFormat);
            writePageCacheSize(pageCacheSize);
            writeEncoding(encoding);
            final SqlJetAutoVacuumMode btreeAutoVacuum = btree.getAutoVacuum();
            autovacuum = SqlJetAutoVacuumMode.NONE != btreeAutoVacuum;
            incrementalVacuum = SqlJetAutoVacuumMode.INCR == btreeAutoVacuum;
            writeAutoVacuum(autovacuum);
            writeIncrementalVacuum(incrementalVacuum);
            btree.commit();
        } catch (SqlJetException e) {
            btree.rollback();
            throw e;
        } } finally {
            if(inTrans && transMode!=null){
                if(!btree.isInTrans()) {
                    btree.beginTrans(transMode);
                } else if(btree.getTransMode()!=transMode) {
                    btree.commit();
                    btree.beginTrans(transMode);
                }
            }
        }
    }

    private void writeSchemaCookie(int schemaCookie) throws SqlJetException {
        btree.updateMeta(SCHEMA_COOKIE, schemaCookie);
    }

    private void writeEncoding(SqlJetEncoding encoding) throws SqlJetException {
        switch (encoding) {
        case UTF8:
            btree.updateMeta(ENCODING, 1);
            break;
        case UTF16LE:
            btree.updateMeta(ENCODING, 2);
            break;
        case UTF16BE:
            btree.updateMeta(ENCODING, 3);
            break;
        default:
            throw new SqlJetException(SqlJetErrorCode.CORRUPT);
        }
    }

    private void writeIncrementalVacuum(boolean incrementalVacuum) throws SqlJetException {
        btree.updateMeta(INCREMENTAL_VACUUM, incrementalVacuum ? 1 : 0);
    }

    private void writeAutoVacuum(boolean autovacuum) throws SqlJetException {
        btree.updateMeta(AUTOVACUUM, autovacuum ? 1 : 0);
    }

    private void writePageCacheSize(int pageCacheSize) throws SqlJetException {
        checkPageCacheSize();
        btree.updateMeta(PAGE_CACHE_SIZE, pageCacheSize);
    }

    private void checkPageCacheSize() throws SqlJetException {
        if (pageCacheSize < SqlJetPageCache.PAGE_CACHE_SIZE_MINIMUM)
            pageCacheSize = SqlJetPageCache.PAGE_CACHE_SIZE_DEFAULT;
    }

    private void writeFileFormat(int fileFormat) throws SqlJetException {
        checkFileFormat(fileFormat);
        btree.updateMeta(FILE_FORMAT, fileFormat);
    }

    public void setUserVersion(int userCookie) throws SqlJetException {
        dbHandle.getMutex().enter();
        try {
            if (!btree.isInTrans())
                throw new SqlJetException("It can be performed only in active transaction");
            writeUserCookie(this.userCookie = userCookie);
        } finally {
            dbHandle.getMutex().leave();
        }
    }

    private void writeUserCookie(int userCookie) throws SqlJetException {
        btree.updateMeta(USER_COOKIE, userCookie);
    }

    private void checkSchema() throws SqlJetException {
        if (readSchemaCookie() != 1) {
            throw new SqlJetException(SqlJetErrorCode.MISUSE);
        }
    }

    public void setFileFormat(int fileFormat) throws SqlJetException {
        dbHandle.getMutex().enter();
        try {
            checkSchema();
            if (btree.isInTrans())
                throw new SqlJetException("It can't be performed in active transaction");
            btree.beginTrans(SqlJetTransactionMode.EXCLUSIVE);
            try {
                writeFileFormat(this.fileFormat = fileFormat);
                btree.commit();
            } catch (SqlJetException e) {
                btree.rollback();
                throw e;
            }
        } finally {
            dbHandle.getMutex().leave();
        }
    }

    public void setCacheSize(int pageCacheSize) throws SqlJetException {
        dbHandle.getMutex().enter();
        try {
            if (!btree.isInTrans())
                throw new SqlJetException("It can be performed only in active transaction");
            writePageCacheSize(this.pageCacheSize = pageCacheSize);
        } finally {
            dbHandle.getMutex().leave();
        }
    }

    public void setAutovacuum(boolean autovacuum) throws SqlJetException {
        dbHandle.getMutex().enter();
        try {
            checkSchema();
            if (btree.isInTrans())
                throw new SqlJetException("It can't be performed in active transaction");
            btree.beginTrans(SqlJetTransactionMode.EXCLUSIVE);
            try {
                writeAutoVacuum(this.autovacuum = autovacuum);
                btree.commit();
            } catch (SqlJetException e) {
                btree.rollback();
                throw e;
            }
        } finally {
            dbHandle.getMutex().leave();
        }
    }

    public void setEncoding(SqlJetEncoding encoding) throws SqlJetException {
        dbHandle.getMutex().enter();
        try {
            checkSchema();
            if (btree.isInTrans())
                throw new SqlJetException("It can't be performed in active transaction");
            btree.beginTrans(SqlJetTransactionMode.EXCLUSIVE);
            try {
                writeEncoding(this.encoding = encoding);
                btree.commit();
            } catch (SqlJetException e) {
                btree.rollback();
                throw e;
            }
        } finally {
            dbHandle.getMutex().leave();
        }
    }

    public void setIncrementalVacuum(boolean incrementalVacuum) throws SqlJetException {
        dbHandle.getMutex().enter();
        try {
            checkSchema();
            if (btree.isInTrans())
                throw new SqlJetException("It can't be performed in active transaction");
            btree.beginTrans(SqlJetTransactionMode.EXCLUSIVE);
            try {
                writeIncrementalVacuum(this.incrementalVacuum = incrementalVacuum);
                btree.commit();
            } catch (SqlJetException e) {
                btree.rollback();
                throw e;
            }
        } finally {
            dbHandle.getMutex().leave();
        }
    }
}
