/*
 * Copyright (c) 2015 FUJI Goro (gfx).
 *
 * 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 com.github.gfx.android.orma;

import com.google.gson.annotations.JsonAdapter;

import com.github.gfx.android.orma.exception.InvalidModelException;
import com.github.gfx.android.orma.exception.NoValueException;
import com.github.gfx.android.orma.gson.SingleAssociationTypeAdapterFactory;
import com.github.gfx.android.orma.internal.Schemas;

import android.os.Parcel;
import android.os.Parcelable;
import android.support.annotation.CheckResult;
import android.support.annotation.NonNull;
import android.support.annotation.RestrictTo;

import io.reactivex.Single;
import io.reactivex.SingleEmitter;
import io.reactivex.SingleOnSubscribe;

/**
 * Lazy has-one association. The {@code Model} is assumed to have a primary key with the `long` type.
 * This is typically created from factory methods.
 *
 * @param <Model> The type of a model
 */
@JsonAdapter(SingleAssociationTypeAdapterFactory.class)
public class SingleAssociation<Model> implements Parcelable {

    final long id;

    final Single<Model> single;

    @RestrictTo(RestrictTo.Scope.LIBRARY)
    public SingleAssociation(long id, @NonNull Model model) {
        this.id = id;
        this.single = Single.just(model);
    }

    @RestrictTo(RestrictTo.Scope.LIBRARY)
    public SingleAssociation(long id, @NonNull Single<Model> single) {
        this.id = id;
        this.single = single;
    }

    // may be called from *_Schema
    public SingleAssociation(@NonNull final OrmaConnection conn, @NonNull final Schema<Model> schema, final long id) {
        this.id = id;
        single = Single.create(new SingleOnSubscribe<Model>() {
            @Override
            public void subscribe(SingleEmitter<Model> emitter) throws Exception {
                ColumnDef<Model, ?> primaryKey = schema.getPrimaryKey();
                String whereClause = primaryKey.getEscapedName() + " = ?";
                String[] whereArgs = {String.valueOf(id)};
                Model model = conn.querySingle(schema, schema.getDefaultResultColumns(),
                        whereClause, whereArgs, null, null, null, 0);
                if (model != null) {
                    emitter.onSuccess(model);
                } else {
                    emitter.onError(new NoValueException("No value found for "
                            + schema.getTableName() + "." + primaryKey.name + " = " + id));
                }
            }
        });
    }

    /**
     * The most typical factory method to create a {@code SingleAssociation} instance,
     * just wrapping the model with it.
     *
     * @param model A model to wrap, which must have a valid primary key
     * @param <T>   The type of the model to wrap
     * @return An instance of {@code SingleAssociation}
     */
    @SuppressWarnings("unchecked")
    @NonNull
    public static <T> SingleAssociation<T> just(@NonNull T model) {
        Schema<T> schema = Schemas.get((Class<T>) model.getClass());
        return just(schema, model);
    }

    @NonNull
    public static <T> SingleAssociation<T> just(long id, @NonNull T model) {
        return new SingleAssociation<>(id, model);
    }

    @NonNull
    public static <T> SingleAssociation<T> just(@NonNull Schema<T> schema, @NonNull T model) {
        return new SingleAssociation<>((long) schema.getPrimaryKey().getSerialized(model), model);
    }

    @NonNull
    public static <T> SingleAssociation<T> just(final long id) {
        return new SingleAssociation<>(id, Single.<T>error(new NoValueException("No value set for id=" + id)));
    }

    // use just(id) instead
    @Deprecated
    @NonNull
    public static <T> SingleAssociation<T> id(final long id) {
        return just(id);
    }

    /**
     * @return The primary key of the associated model.
     */
    public long getId() {
        return id;
    }

    @CheckResult
    @NonNull
    public Single<Model> single() {
        return single;
    }

    /**
     * A shortcut of {@code singleAssociation.single().blockingGet()}.
     *
     * @return A model that the instance refers to.
     */
    @NonNull
    public Model get() throws NoValueException {
        return single.blockingGet();
    }

    @Deprecated
    @NonNull
    public Model value() throws NoValueException {
        return single.blockingGet();
    }

    @Override
    public String toString() {
        return "SingleAssociation{" +
                "id=" + id + '}';
    }

    // Parcelable

    public static Parcelable.ClassLoaderCreator<SingleAssociation<?>> CREATOR
            = new ClassLoaderCreator<SingleAssociation<?>>() {
        @Override
        public SingleAssociation<?> createFromParcel(Parcel source) {
            return createFromParcel(source, null);
        }

        @Override
        public SingleAssociation<?>[] newArray(int size) {
            return new SingleAssociation<?>[size];
        }

        @Override
        public SingleAssociation<?> createFromParcel(Parcel source, ClassLoader loader) {
            long id = source.readLong();
            Parcelable parcelable = source.readParcelable(loader);
            return new SingleAssociation<>(id, parcelable);
        }
    };

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        Model model = single.blockingGet();
        if (!(model instanceof Parcelable)) {
            throw new InvalidModelException("Orma model " + model.getClass() + " is not a Parcelable");
        }
        dest.writeLong(id);
        dest.writeParcelable((Parcelable) model, flags);
    }
}

@@ -1166,82 +1166,8 @@
 gle;
-%0Aimport io.reactivex.SingleEmitter;%0Aimport io.reactivex.SingleOnSubscribe;
 %0A%0A/*
@@ -2089,815 +2089,178 @@
 gle.
-create(new SingleOnSubscribe%3CModel%3E() %7B%0A            @Override%0A            public void subscribe(SingleEmitter%3CModel%3E emitter) throws Exception %7B%0A                ColumnDef%3CModel, ?%3E primaryKey = schema.getPrimaryKey();%0A                String whereClause = primaryKey.getEscapedName() + %22 = ?%22;%0A                String%5B%5D whereArgs = %7BString.valueOf(id)%7D;%0A                Model model = conn.querySingle(schema, schema.getDefaultResultColumns(),%0A                        whereClause, whereArgs, null, null, null, 0);%0A                if (model != null) %7B%0A                    emitter.onSuccess(model);%0A                %7D else %7B%0A                    emitter.onError(new NoValueException(%22No value found for %22%0A                            + schema.getTableName() + %22.%22 + primaryKey.name + %22 = %22 + id));%0A                %7D
+fromCallable(new ModelFactory%3CModel%3E() %7B%0A            @NonNull%0A            @Override%0A            public Model call() %7B%0A                return conn.findByRowId(schema, id);
 %0A   

simplify SingleAssociation's constructor