/*
 * Copyright 2016 requery.io
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package io.requery.android.sqlite;

import android.annotation.TargetApi;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.os.Build;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import io.requery.android.DefaultMapping;
import io.requery.android.LoggingListener;
import io.requery.meta.EntityModel;
import io.requery.sql.Configuration;
import io.requery.sql.ConfigurationBuilder;
import io.requery.sql.Mapping;
import io.requery.sql.Platform;
import io.requery.sql.SchemaModifier;
import io.requery.sql.TableCreationMode;
import io.requery.sql.platform.SQLite;
import io.requery.util.function.Function;

import java.sql.Connection;
import java.sql.SQLException;
import java.sql.SQLNonTransientConnectionException;

/**
 * Wrapper for working with Android SQLite databases. This provides a {@link Connection} wrapping
 * the standard java SQLite API turning it into standard JDBC APIs.
 *
 * <p> This class extends the standard {@link SQLiteOpenHelper} and is used to create the database.
 * {@link #onCreate(SQLiteDatabase)} will create the database tables & columns automatically.
 *
 * <p> {@link #onUpgrade(SQLiteDatabase, int, int)} will create any missing tables/columns, however
 * it will not remove any tables/columns more complex upgrades should be handled by overriding
 * {@link #onUpgrade(SQLiteDatabase, int, int)} and implementing a script to handle the migration.
 *
 * @author Nikhil Purushe
 */
@SuppressWarnings("WeakerAccess")
public class DatabaseSource extends SQLiteOpenHelper implements DatabaseProvider<SQLiteDatabase> {

    private final Platform platform;
    private final EntityModel model;
    private final Mapping mapping;
    private SQLiteDatabase db;
    private Configuration configuration;
    private boolean configured;
    private boolean loggingEnabled;
    private TableCreationMode mode;

    /**
     * Creates a new {@link DatabaseSource} instance.
     *
     * @param context context
     * @param model   the entity model
     * @param version the schema version
     */
    public DatabaseSource(Context context, EntityModel model, int version) {
        this(context, model, getDefaultDatabaseName(context, model), null, version);
    }

    /**
     * Creates a new {@link DatabaseSource} instance.
     *
     * @param context context
     * @param model   the entity model
     * @param name    database filename
     * @param version the schema version
     */
    public DatabaseSource(Context context, EntityModel model, @Nullable String name, int version) {
        this(context, model, name, null, version);
    }

    /**
     * Creates a new {@link DatabaseSource} instance.
     *
     * @param context context
     * @param model   the entity model
     * @param name    database filename
     * @param factory optional {@link android.database.sqlite.SQLiteDatabase.CursorFactory}
     * @param version the schema version
     */
    public DatabaseSource(Context context,
                          EntityModel model,
                          @Nullable String name,
                          @Nullable SQLiteDatabase.CursorFactory factory,
                          int version) {
        this(context, model, name, factory, version, new SQLite());
    }

    /**
     * Creates a new {@link DatabaseSource} instance.
     *
     * @param context context
     * @param model   the entity model
     * @param name    database filename
     * @param factory optional {@link android.database.sqlite.SQLiteDatabase.CursorFactory}
     * @param version the schema version
     * @param platform platform instance
     */
    public DatabaseSource(Context context,
                          EntityModel model,
                          @Nullable String name,
                          @Nullable SQLiteDatabase.CursorFactory factory,
                          int version,
                          SQLite platform) {
        super(context, name, factory, version);
        if (model == null) {
            throw new IllegalArgumentException("null model");
        }
        this.platform = platform;
        this.mapping = onCreateMapping(platform);
        this.model = model;
        this.mode = TableCreationMode.CREATE_NOT_EXISTS;
    }

    @Override
    public void setLoggingEnabled(boolean enable) {
        this.loggingEnabled = enable;
    }

    @Override
    public void setTableCreationMode(TableCreationMode mode) {
        this.mode = mode;
    }

    private static String getDefaultDatabaseName(Context context, EntityModel model) {
        return TextUtils.isEmpty(model.getName()) ?
                context.getPackageName() : model.getName();
    }

    /**
     * Override to change the default {@link Mapping}.
     *
     * @param platform platform instance
     * @return the configured mapping.
     */
    protected Mapping onCreateMapping(Platform platform) {
        return new DefaultMapping(platform);
    }

    /**
     * Override to modify the configuration before it is built.
     *
     * @param builder {@link ConfigurationBuilder instance} to configure.
     */
    protected void onConfigure(ConfigurationBuilder builder) {
        if (loggingEnabled) {
            LoggingListener loggingListener = new LoggingListener();
            builder.addStatementListener(loggingListener);
        }
    }

    private Connection getConnection(SQLiteDatabase db) throws SQLException {
        synchronized (this) {
            if (!db.isOpen()) {
                throw new SQLNonTransientConnectionException();
            }
            return new SqliteConnection(db);
        }
    }

    @Override
    public Configuration getConfiguration() {
        if (configuration == null) {
            ConfigurationBuilder builder = new ConfigurationBuilder(this, model)
                .setMapping(mapping)
                .setPlatform(platform)
                .setBatchUpdateSize(1000);
            onConfigure(builder);
            configuration = builder.build();
        }
        return configuration;
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        this.db = db;
        new SchemaModifier(getConfiguration()).createTables(TableCreationMode.CREATE);
    }

    @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
    @Override
    public void onConfigure(SQLiteDatabase db) {
        super.onConfigure(db);
        if (!db.isReadOnly()) {
            db.setForeignKeyConstraintsEnabled(true);
        }
    }

    @Override
    public void onUpgrade(final SQLiteDatabase db, int oldVersion, int newVersion) {
        this.db = db;
        SchemaUpdater updater = new SchemaUpdater(getConfiguration(),
            new Function<String, Cursor>() {
            @Override
            public Cursor apply(String s) {
                return db.rawQuery(s, null);
            }
        }, mode);
        updater.update();
    }

    @Override
    public Connection getConnection() throws SQLException {
        synchronized (this) {
            if (db == null) {
                db = getWritableDatabase();
            }
            if (!configured && Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
                db.execSQL("PRAGMA foreign_keys = ON");
                long pageSize = db.getPageSize();
                // on later Android versions page size was correctly set to 4096,
                // do the same for older versions
                if (pageSize == 1024) {
                    db.setPageSize(4096);
                }
                configured = true;
            }
            return getConnection(db);
        }
    }
}

@@ -2375,22 +2375,16 @@
 private 
-final 
 Mapping 
@@ -4798,58 +4798,8 @@
 rm;%0A
-        this.mapping = onCreateMapping(platform);%0A
     
@@ -6317,24 +6317,202 @@
 uration() %7B%0A
+        if (mapping == null) %7B%0A            mapping = onCreateMapping(platform);%0A        %7D%0A        if (mapping == null) %7B%0A            throw new IllegalStateException();%0A        %7D%0A
         if (

Resolve #277 Create mapping after constructor