package org.kohsuke.github;

import java.io.IOException;

import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;

// TODO: Auto-generated Javadoc
/**
 * An abstract data object builder/updater.
 *
 * This class can be use to make a Builder that supports both batch and single property changes.
 * <p>
 * Batching looks like this:
 * </p>
 *
 * <pre>
 * update().someName(value).otherName(value).done()
 * </pre>
 * <p>
 * Single changes look like this:
 * </p>
 *
 * <pre>
 * set().someName(value);
 * set().otherName(value);
 * </pre>
 * <p>
 * If {@link S} is the same as {@link R}, {@link #with(String, Object)} will commit changes after the first value change
 * and return a {@link R} from {@link #done()}.
 * </p>
 * <p>
 * If {@link S} is not the same as {@link R}, {@link #with(String, Object)} will batch together multiple changes and let
 * the user call {@link #done()} when they are ready.
 *
 * @author Liam Newman
 * @param <R>
 *            Final return type built by this builder returned when {@link #done()}} is called.
 * @param <S>
 *            Intermediate return type for this builder returned by calls to {@link #with(String, Object)}. If {@link S}
 *            the same as {@link R}, this builder will commit changes after each call to {@link #with(String, Object)}.
 */
abstract class AbstractBuilder<R, S> extends GitHubInteractiveObject {

    @Nonnull
    private final Class<R> returnType;

    private final boolean commitChangesImmediately;

    @CheckForNull
    private final R baseInstance;

    /** The requester. */
    @Nonnull
    protected final Requester requester;

    // TODO: Not sure how update-in-place behavior should be controlled
    // However, it certainly can be controlled dynamically down to the instance level or inherited for all children of
    // some
    /** The update in place. */
    // connection.
    protected boolean updateInPlace;

    /**
     * Creates a builder.
     *
     * @param finalReturnType
     *            the final return type for built by this builder returned when {@link #done()}} is called.
     * @param intermediateReturnType
     *            the intermediate return type of type {@link S} returned by calls to {@link #with(String, Object)}.
     *            Must either be equal to {@code builtReturnType} or this instance must be castable to this class. If
     *            not, the constructor will throw {@link IllegalArgumentException}.
     * @param root
     *            the GitHub instance to connect to.
     * @param baseInstance
     *            optional instance on which to base this builder.
     */
    protected AbstractBuilder(@Nonnull Class<R> finalReturnType,
            @Nonnull Class<S> intermediateReturnType,
            @Nonnull GitHub root,
            @CheckForNull R baseInstance) {
        super(root);
        this.requester = root.createRequest();
        this.returnType = finalReturnType;
        this.commitChangesImmediately = returnType.equals(intermediateReturnType);
        if (!commitChangesImmediately && !intermediateReturnType.isInstance(this)) {
            throw new IllegalArgumentException(
                    "Argument \"intermediateReturnType\": This instance must be castable to intermediateReturnType or finalReturnType must be equal to intermediateReturnType.");
        }

        this.baseInstance = baseInstance;
        this.updateInPlace = false;
    }

    /**
     * Finishes an update, committing changes.
     *
     * This method may update-in-place or not. Either way it returns the resulting instance.
     *
     * @return an instance with updated current data
     * @throws IOException
     *             if there is an I/O Exception
     */
    @Nonnull
    @BetaApi
    public R done() throws IOException {
        R result;
        if (updateInPlace && baseInstance != null) {
            result = requester.fetchInto(baseInstance);
        } else {
            result = requester.fetch(returnType);
        }
        return result;
    }

    /**
     * Applies a value to a name for this builder.
     *
     * If {@link S} is the same as {@link R}, this method will commit changes after the first value change and return a
     * {@link R} from {@link #done()}.
     *
     * If {@link S} is not the same as {@link R}, this method will return an {@link S} and letting the caller batch
     * together multiple changes and call {@link #done()} when they are ready.
     *
     * @param name
     *            the name of the field
     * @param value
     *            the value of the field
     * @return either a continuing builder or an updated data record
     * @throws IOException
     *             if an I/O error occurs
     */
    @Nonnull
    @BetaApi
    protected S with(@Nonnull String name, Object value) throws IOException {
        requester.with(name, value);
        return continueOrDone();
    }

    /**
     * Chooses whether to return a continuing builder or an updated data record
     *
     * If {@link S} is the same as {@link R}, this method will commit changes after the first value change and return a
     * {@link R} from {@link #done()}.
     *
     * If {@link S} is not the same as {@link R}, this method will return an {@link S} and letting the caller batch
     * together multiple changes and call {@link #done()} when they are ready.
     *
     * @return either a continuing builder or an updated data record
     * @throws IOException
     *             if an I/O error occurs
     */
    @Nonnull
    @BetaApi
    protected S continueOrDone() throws IOException {
        // This little bit of roughness in this base class means all inheriting builders get to create Updater and
        // Setter classes from almost identical code. Creator can often be implemented with significant code reuse as
        // well.
        if (commitChangesImmediately) {
            // These casts look strange and risky, but they they're actually guaranteed safe due to the return path
            // being based on the previous comparison of class instances passed to the constructor.
            return (S) done();
        } else {
            return (S) this;
        }
    }
}
