/*
 * Copyright 2013 Square Inc.
 *
 * 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 flowless;

import android.support.annotation.NonNull;
import android.support.annotation.Nullable;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.Locale;

import static flowless.Preconditions.checkArgument;
import static java.util.Collections.unmodifiableList;

/**
 * Describes the history of a {@link Flow} at a specific point in time.
 */
public /*final*/ class History
        implements Iterable<Object> {

    private final List<Object> history;

    @NonNull
    public static Builder emptyBuilder() {
        return new Builder(Collections.emptyList());
    }

    /**
     * Create a history that contains a single key.
     */
    @NonNull
    public static History single(@NonNull Object key) {
        return emptyBuilder().push(key).build();
    }

    private History(List<Object> history) {
        checkArgument(history != null && !history.isEmpty(), "History may not be empty");
        this.history = history;
    }

    @NonNull
    public <T> Iterator<T> reverseIterator() {
        return new ReadStateIterator<>(history.iterator());
    }

    @NonNull
    @Override
    public Iterator<Object> iterator() {
        return new ReadStateIterator<>(new ReverseIterator<>(history));
    }

    public int size() {
        return history.size();
    }

    @NonNull
    public <T> T top() {
        return peek(0);
    }

    /**
     * Returns the app state at the provided index in history. 0 is the newest entry.
     */
    @NonNull
    public <T> T peek(int index) {
        //noinspection unchecked
        return (T) history.get(history.size() - index - 1);
    }

    @NonNull
    List<Object> asList() {
        final ArrayList<Object> copy = new ArrayList<>(history);
        return unmodifiableList(copy);
    }

    /**
     * Get a builder to modify a copy of this history.
     * <p>
     * The builder returned will retain all internal information related to the keys in the
     * history, including their states. It is safe to remove keys from the builder and push them back
     * on; nothing will be lost in those operations.
     */
    @NonNull
    public Builder buildUpon() {
        return new Builder(history);
    }

    @Override
    public String toString() {
        return Arrays.deepToString(history.toArray());
    }

    public static /*final*/ class Builder {
        private final List<Object> history;

        private Builder(Collection<Object> history) {
            this.history = new ArrayList<>(history);
        }

        /**
         * Removes all keys from this builder. But note that if this builder was created
         * via {@link #buildUpon()}, any state associated with the cleared
         * keys will be preserved and will be restored if they are {@link #push pushed}
         * back on.
         */
        @NonNull
        public Builder clear() {
            // Clear by popping everything (rather than just calling history.clear()) to
            // fill up entryMemory. Otherwise we drop view state on the floor.
            while(!isEmpty()) {
                pop();
            }

            return this;
        }

        /**
         * Adds a key to the builder. If this builder was created via {@link #buildUpon()},
         * and the pushed key was previously {@link #pop() popped} or {@link #clear cleared}
         * from the builder, the key's associated state will be restored.
         */
        @NonNull
        public Builder push(@NonNull Object key) {
            history.add(key);
            return this;
        }

        /**
         * {@link #push Pushes} all of the keys in the collection onto this builder.
         */
        @NonNull
        public Builder pushAll(@NonNull Collection<?> c) {
            for(Object key : c) {
                //noinspection CheckResult
                push(key);
            }
            return this;
        }

        /**
         * @return null if the history is empty.
         */
        @Nullable
        public Object peek() {
            return history.isEmpty() ? null : history.get(history.size() - 1);
        }

        @NonNull
        public boolean isEmpty() {
            return history.isEmpty();
        }

        /**
         * Removes the last state added. Note that if this builder was created
         * via {@link #buildUpon()}, any view state associated with the popped
         * state will be preserved, and restored if it is {@link #push pushed}
         * back in.
         *
         * @throws IllegalStateException if empty
         */
        public Object pop() {
            if(isEmpty()) {
                throw new IllegalStateException("Cannot pop from an empty builder");
            }
            return history.remove(history.size() - 1);
        }

        /**
         * Pops the history until the given state is at the top.
         *
         * @throws IllegalArgumentException if the given state isn't in the history.
         */
        @NonNull
        public Builder popTo(@NonNull Object state) {
            //noinspection ConstantConditions
            while(!isEmpty() && !peek().equals(state)) {
                pop();
            }
            checkArgument(!isEmpty(), String.format("%s not found in history", state));
            return this;
        }

        @NonNull
        public Builder pop(int count) {
            final int size = history.size();
            checkArgument(count <= size, String.format((Locale) null, "Cannot pop %d elements, history only has %d", count, size));
            while(count-- > 0) {
                pop();
            }
            return this;
        }

        @NonNull
        public History build() {
            return new History(history);
        }

        @Override
        public String toString() {
            return Arrays.deepToString(history.toArray());
        }
    }

    private static class ReverseIterator<T>
            implements Iterator<T> {
        private final ListIterator<T> wrapped;

        ReverseIterator(List<T> list) {
            wrapped = list.listIterator(list.size());
        }

        @Override
        public boolean hasNext() {
            return wrapped.hasPrevious();
        }

        @Override
        public T next() {
            return wrapped.previous();
        }

        @Override
        public void remove() {
            wrapped.remove();
        }
    }

    private static class ReadStateIterator<T>
            implements Iterator<T> {
        private final Iterator<Object> iterator;

        ReadStateIterator(Iterator<Object> iterator) {
            this.iterator = iterator;
        }

        @Override
        public boolean hasNext() {
            return iterator.hasNext();
        }

        @Override
        public T next() {
            //noinspection unchecked
            return (T) iterator.next();
        }

        @Override
        public void remove() {
            throw new UnsupportedOperationException();
        }
    }
}
