/*
 * 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.models.sqlite;

import android.support.annotation.*;

import com.activeandroid.query.*;
import com.activeandroid.util.*;

import org.apache.commons.lang3.*;
import org.isoron.uhabits.models.*;
import org.isoron.uhabits.models.sqlite.records.*;

import java.util.*;

/**
 * Implementation of a {@link HabitList} that is backed by SQLite.
 */
public class SQLiteHabitList extends HabitList
{
    private static HashMap<Long, Habit> cache;

    private static SQLiteHabitList instance;

    @NonNull
    private final SQLiteUtils<HabitRecord> sqlite;

    @NonNull
    private final ModelFactory modelFactory;

    @NonNull
    private Order order;

    public SQLiteHabitList(@NonNull ModelFactory modelFactory)
    {
        super();
        this.modelFactory = modelFactory;

        if (cache == null) cache = new HashMap<>();
        sqlite = new SQLiteUtils<>(HabitRecord.class);
        order = Order.BY_POSITION;
    }

    protected SQLiteHabitList(@NonNull ModelFactory modelFactory,
                              @NonNull HabitMatcher filter,
                              @NonNull Order order)
    {
        super(filter);
        this.modelFactory = modelFactory;

        if (cache == null) cache = new HashMap<>();
        sqlite = new SQLiteUtils<>(HabitRecord.class);
        this.order = order;
    }

    public static SQLiteHabitList getInstance(
        @NonNull ModelFactory modelFactory)
    {
        if (instance == null) instance = new SQLiteHabitList(modelFactory);
        return instance;
    }

    @Override
    public void add(@NonNull Habit habit)
    {
        if (cache.containsValue(habit))
            throw new IllegalArgumentException("habit already added");

        HabitRecord record = new HabitRecord();
        record.copyFrom(habit);
        record.position = size();

        Long id = habit.getId();
        if (id == null) id = record.save();
        else record.save(id);

        if (id < 0)
            throw new IllegalArgumentException("habit could not be saved");

        habit.setId(id);
        cache.put(id, habit);
    }

    @Override
    @Nullable
    public Habit getById(long id)
    {
        if (!cache.containsKey(id))
        {
            HabitRecord record = HabitRecord.get(id);
            if (record == null) return null;

            Habit habit = modelFactory.buildHabit();
            record.copyTo(habit);
            cache.put(id, habit);
        }

        return cache.get(id);
    }

    @Override
    @NonNull
    public Habit getByPosition(int position)
    {
        return toList().get(position);
    }

    @NonNull
    @Override
    public HabitList getFiltered(HabitMatcher filter)
    {
        return new SQLiteHabitList(modelFactory, filter, order);
    }

    @Override
    @NonNull
    public Order getOrder()
    {
        return order;
    }

    @Override
    public void setOrder(@NonNull Order order)
    {
        this.order = order;
    }

    @Override
    public int indexOf(@NonNull Habit h)
    {
        return toList().indexOf(h);
    }

    @Override
    public Iterator<Habit> iterator()
    {
        return Collections.unmodifiableCollection(toList()).iterator();
    }

    public void rebuildOrder()
    {
        List<Habit> habits = toList();

        int i = 0;
        for (Habit h : habits)
        {
            HabitRecord record = HabitRecord.get(h.getId());
            if (record == null)
                throw new RuntimeException("habit not in database");

            record.position = i++;
            record.save();
        }

        update(habits);
    }

    @Override
    public void remove(@NonNull Habit habit)
    {
        if (!cache.containsKey(habit.getId()))
            throw new RuntimeException("habit not in cache");

        cache.remove(habit.getId());
        HabitRecord record = HabitRecord.get(habit.getId());
        if (record == null) throw new RuntimeException("habit not in database");
        record.cascadeDelete();
        rebuildOrder();
    }

    @Override
    public void removeAll()
    {
        sqlite.query("delete from checkmarks", null);
        sqlite.query("delete from score", null);
        sqlite.query("delete from streak", null);
        sqlite.query("delete from repetitions", null);
        sqlite.query("delete from habits", null);
    }

    @Override
    public synchronized void reorder(Habit from, Habit to)
    {
        if (from == to) return;

        HabitRecord fromRecord = HabitRecord.get(from.getId());
        HabitRecord toRecord = HabitRecord.get(to.getId());

        if (fromRecord == null)
            throw new RuntimeException("habit not in database");
        if (toRecord == null)
            throw new RuntimeException("habit not in database");

        Integer fromPos = fromRecord.position;
        Integer toPos = toRecord.position;

        Log.d("SQLiteHabitList",
            String.format("reorder: %d %d", fromPos, toPos));

        if (toPos < fromPos)
        {
            new Update(HabitRecord.class)
                .set("position = position + 1")
                .where("position >= ? and position < ?", toPos, fromPos)
                .execute();
        }
        else
        {
            new Update(HabitRecord.class)
                .set("position = position - 1")
                .where("position > ? and position <= ?", fromPos, toPos)
                .execute();
        }

        fromRecord.position = toPos;
        fromRecord.save();
        update(from);
        getObservable().notifyListeners();
    }

    @Override
    public void repair()
    {
        super.repair();
        rebuildOrder();
    }

    @Override
    public int size()
    {
        return toList().size();
    }

    @Override
    public void update(List<Habit> habits)
    {
        for (Habit h : habits)
        {
            HabitRecord record = HabitRecord.get(h.getId());
            if (record == null)
                throw new RuntimeException("habit not in database");
            record.copyFrom(h);
            record.save();
        }
    }

    protected List<Habit> toList()
    {
        String query = buildSelectQuery();
        List<HabitRecord> recordList = sqlite.query(query, null);

        List<Habit> habits = new LinkedList<>();
        for (HabitRecord record : recordList)
        {
            Habit habit = getById(record.getId());
            if (habit == null)
                throw new RuntimeException("habit not in database");

            if (!filter.matches(habit)) continue;
            habits.add(habit);
        }

        if(order == Order.BY_SCORE)
        {
            Collections.sort(habits, (lhs, rhs) -> {
                double s1 = lhs.getScores().getTodayValue();
                double s2 = rhs.getScores().getTodayValue();
                return Double.compare(s2, s1);
            });
        }

        return habits;
    }

    private void appendOrderBy(StringBuilder query)
    {
        switch (order)
        {
            case BY_POSITION:
                query.append("order by position ");
                break;

            case BY_NAME:
            case BY_SCORE:
                query.append("order by name ");
                break;

            case BY_COLOR:
                query.append("order by color, name ");
                break;

            default:
                throw new IllegalStateException();
        }
    }

    private void appendSelect(StringBuilder query)
    {
        query.append(HabitRecord.SELECT);
    }

    private void appendWhere(StringBuilder query)
    {
        ArrayList<Object> where = new ArrayList<>();
        if (filter.isReminderRequired()) where.add("reminder_hour is not null");
        if (!filter.isArchivedAllowed()) where.add("archived = 0");

        if (where.isEmpty()) return;
        query.append("where ");
        query.append(StringUtils.join(where, " and "));
        query.append(" ");
    }

    private String buildSelectQuery()
    {
        StringBuilder query = new StringBuilder();
        appendSelect(query);
        appendWhere(query);
        appendOrderBy(query);
        return query.toString();
    }
}
