/*
 * Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
 *
 * This file is part of Loop Habit Tracker.
 *
 * Loop Habit Tracker 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, either version 3 of the License, or (at your
 * option) any later version.
 *
 * Loop Habit Tracker 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.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program. If not, see <http://www.gnu.org/licenses/>.
 */

package org.isoron.uhabits.core.models;

import android.support.annotation.*;

import com.opencsv.*;

import org.isoron.uhabits.core.utils.*;

import java.io.*;
import java.util.*;

import javax.annotation.concurrent.*;

/**
 * An ordered collection of {@link Habit}s.
 */
@ThreadSafe
public abstract class HabitList implements Iterable<Habit>
{
    private final ModelObservable observable;

    @NonNull
    protected final HabitMatcher filter;

    /**
     * Creates a new HabitList.
     * <p>
     * Depending on the implementation, this list can either be empty or be
     * populated by some pre-existing habits, for example, from a certain
     * database.
     */
    public HabitList()
    {
        observable = new ModelObservable();
        filter = new HabitMatcherBuilder().setArchivedAllowed(true).build();
    }

    protected HabitList(@NonNull HabitMatcher filter)
    {
        observable = new ModelObservable();
        this.filter = filter;
    }

    /**
     * Inserts a new habit in the list.
     * <p>
     * If the id of the habit is null, the list will assign it a new id, which
     * is guaranteed to be unique in the scope of the list. If id is not null,
     * the caller should make sure that the list does not already contain
     * another habit with same id, otherwise a RuntimeException will be thrown.
     *
     * @param habit the habit to be inserted
     * @throws IllegalArgumentException if the habit is already on the list.
     */
    public abstract void add(@NonNull Habit habit)
        throws IllegalArgumentException;

    /**
     * Returns the habit with specified id.
     *
     * @param id the id of the habit
     * @return the habit, or null if none exist
     */
    @Nullable
    public abstract Habit getById(long id);

    /**
     * Returns the habit that occupies a certain position.
     *
     * @param position the position of the desired habit
     * @return the habit at that position
     * @throws IndexOutOfBoundsException when the position is invalid
     */
    @NonNull
    public abstract Habit getByPosition(int position);

    /**
     * Returns the list of habits that match a given condition.
     *
     * @param matcher the matcher that checks the condition
     * @return the list of matching habits
     */
    @NonNull
    public abstract HabitList getFiltered(HabitMatcher matcher);

    public ModelObservable getObservable()
    {
        return observable;
    }

    public abstract Order getOrder();

    /**
     * Changes the order of the elements on the list.
     *
     * @param order the new order criterea
     */
    public abstract void setOrder(@NonNull Order order);

    /**
     * Returns the index of the given habit in the list, or -1 if the list does
     * not contain the habit.
     *
     * @param h the habit
     * @return the index of the habit, or -1 if not in the list
     */
    public abstract int indexOf(@NonNull Habit h);

    public boolean isEmpty()
    {
        return size() == 0;
    }

    /**
     * Removes the given habit from the list.
     * <p>
     * If the given habit is not in the list, does nothing.
     *
     * @param h the habit to be removed.
     */
    public abstract void remove(@NonNull Habit h);

    /**
     * Removes all the habits from the list.
     */
    public void removeAll()
    {
        List<Habit> copy = new LinkedList<>();
        for (Habit h : this) copy.add(h);
        for (Habit h : copy) remove(h);
        observable.notifyListeners();
    }

    /**
     * Changes the position of a habit in the list.
     *
     * @param from the habit that should be moved
     * @param to   the habit that currently occupies the desired position
     */
    public abstract void reorder(@NonNull Habit from, @NonNull Habit to);

    public void repair()
    {
        for (Habit h : this)
        {
            h.getCheckmarks().invalidateNewerThan(Timestamp.ZERO);
            h.getStreaks().invalidateNewerThan(Timestamp.ZERO);
            h.getScores().invalidateNewerThan(Timestamp.ZERO);
        }
    }

    /**
     * Returns the number of habits in this list.
     *
     * @return number of habits
     */
    public abstract int size();

    /**
     * Notifies the list that a certain list of habits has been modified.
     * <p>
     * Depending on the implementation, this operation might trigger a write to
     * disk, or do nothing at all. To make sure that the habits get persisted,
     * this operation must be called.
     *
     * @param habits the list of habits that have been modified.
     */
    public abstract void update(List<Habit> habits);

    /**
     * Notifies the list that a certain habit has been modified.
     * <p>
     * See {@link #update(List)} for more details.
     *
     * @param habit the habit that has been modified.
     */
    public void update(@NonNull Habit habit)
    {
        update(Collections.singletonList(habit));
    }

    /**
     * Writes the list of habits to the given writer, in CSV format. There is
     * one line for each habit, containing the fields name, description,
     * frequency numerator, frequency denominator and color. The color is
     * written in HTML format (#000000).
     *
     * @param out the writer that will receive the result
     * @throws IOException if write operations fail
     */
    public void writeCSV(@NonNull Writer out) throws IOException
    {
        String header[] = {
            "Position",
            "Name",
            "Description",
            "NumRepetitions",
            "Interval",
            "Color"
        };

        CSVWriter csv = new CSVWriter(out);
        csv.writeNext(header, false);

        for (Habit habit : this)
        {
            Frequency freq = habit.getFrequency();

            String[] cols = {
                String.format("%03d", indexOf(habit) + 1),
                habit.getName(),
                habit.getDescription(),
                Integer.toString(freq.getNumerator()),
                Integer.toString(freq.getDenominator()),
                ColorConstants.CSV_PALETTE[habit.getColor()]
            };

            csv.writeNext(cols, false);
        }

        csv.close();
    }

    public enum Order
    {
        BY_NAME,
        BY_COLOR,
        BY_SCORE,
        BY_POSITION
    }
}
