/*
 * Copyright (c) 2015-2019 Snowplow Analytics Ltd. All rights reserved.
 *
 * This program is licensed to you under the Apache License Version 2.0,
 * and you may not use this file except in compliance with the Apache License Version 2.0.
 * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the Apache License Version 2.0 is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
 */

package com.snowplowanalytics.snowplow.tracker.storage;

import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.DatabaseUtils;
import android.database.sqlite.SQLiteDatabase;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import com.snowplowanalytics.snowplow.tracker.payload.Payload;
import com.snowplowanalytics.snowplow.tracker.utils.Logger;
import com.snowplowanalytics.snowplow.tracker.payload.TrackerPayload;
import com.snowplowanalytics.snowplow.tracker.emitter.EmittableEvents;
import com.snowplowanalytics.snowplow.tracker.utils.Util;

/**
 * Helper class for storing, getting and removing
 * events from the SQLite database.
 */
public class EventStore {

    private String TAG = EventStore.class.getSimpleName();

    private SQLiteDatabase database;
    private EventStoreHelper dbHelper;
    private String[] allColumns = {
            EventStoreHelper.COLUMN_ID,
            EventStoreHelper.COLUMN_EVENT_DATA,
            EventStoreHelper.COLUMN_DATE_CREATED
    };
    private long lastInsertedRowId = -1;

    private int sendLimit;

    /**
     * Creates a new Event Store
     *
     * @param context The android context object
     * @param sendLimit The maximum amount of events that can be sent
     *                  concurrently
     */
    public EventStore(Context context, int sendLimit) {
        dbHelper = EventStoreHelper.getInstance(context);
        open();
        this.sendLimit = sendLimit;

        Logger.d(TAG, "DB Path: %s", database.getPath());
    }

    /**
     * Adds an event to the database.
     *
     * @param payload the payload to be added
     */
    public void add(Payload payload) {
        insertEvent(payload);
    }

    /**
     * Opens a new writable database if it
     * is currently closed.
     */
    public void open() {
        if (!isDatabaseOpen()) {
            database = dbHelper.getWritableDatabase();
            database.enableWriteAheadLogging();
        }
    }

    /**
     * Closes the database
     */
    public void close() {
        dbHelper.close();
    }

    /**
     * Inserts a payload into the database
     *
     * @param payload The event payload to
     *                be stored
     * @return a boolean stating if the insert
     * was a success or not
     */
    @SuppressWarnings("unchecked")
    public long insertEvent(Payload payload) {
        if (isDatabaseOpen()) {
            byte[] bytes = Util.serialize(payload.getMap());
            ContentValues values = new ContentValues(2);
            values.put(EventStoreHelper.COLUMN_EVENT_DATA, bytes);
            lastInsertedRowId = database.insert(EventStoreHelper.TABLE_EVENTS, null, values);
        }
        Logger.d(TAG, "Added event to database: %s", lastInsertedRowId);
        return lastInsertedRowId;
    }

    /**
     * Removes an event from the database
     *
     * @param id the row id of the event
     * @return a boolean of success to remove
     */
    public boolean removeEvent(long id) {
        int retval = -1;
        if (isDatabaseOpen()) {
            retval = database.delete(EventStoreHelper.TABLE_EVENTS,
                    EventStoreHelper.COLUMN_ID + "=" + id, null);
        }
        Logger.d(TAG, "Removed event from database: %s", "" + id);
        return retval == 1;
    }

    /**
     * Removes a range of events from the database
     *
     * @param ids the ids to remove
     * @return a boolean of success to remove
     */
    public boolean removeEvents(List<Long> ids) {
        if (ids.size() == 0) {
            return false;
        }

        int retval = -1;
        if (isDatabaseOpen()) {
            retval = database.delete(EventStoreHelper.TABLE_EVENTS,
                    EventStoreHelper.COLUMN_ID + " in (" + (Util.joinLongList(ids)) + ")", null);
        }
        Logger.d(TAG, "Removed events from database: %s", retval);
        return retval == ids.size();
    }

    /**
     * Empties the database of all events
     *
     * @return a boolean of success to remove
     */
    public boolean removeAllEvents() {
        int retval = -1;
        Logger.d(TAG, "Removing all events from database.");
        if (isDatabaseOpen()) {
            retval = database.delete(EventStoreHelper.TABLE_EVENTS, null, null);
        } else {
            Logger.e(TAG, "Database is not open.");
        }
        return retval >= 0;
    }

    /**
     * Returns the events that validate a
     * specific query.
     *
     * @param query the query to be passed against
     *              the database
     * @param orderBy what to order the query by
     * @return the list of events that satisfied
     * the query
     */
    private List<Map<String, Object>> queryDatabase(String query, String orderBy) {
        List<Map<String, Object>> res = new ArrayList<>();
        if (isDatabaseOpen()) {
            Cursor cursor = null;
            try {
                cursor = database.query(EventStoreHelper.TABLE_EVENTS, allColumns, query, null, null, null, orderBy);
                cursor.moveToFirst();
                while (!cursor.isAfterLast()) {
                    Map<String, Object> eventMetadata = new HashMap<>();
                    eventMetadata.put(EventStoreHelper.METADATA_ID, cursor.getLong(0));
                    eventMetadata.put(EventStoreHelper.METADATA_EVENT_DATA,
                            Util.deserializer(cursor.getBlob(1)));
                    eventMetadata.put(EventStoreHelper.METADATA_DATE_CREATED, cursor.getString(2));
                    cursor.moveToNext();
                    res.add(eventMetadata);
                }
            } finally {
                if (cursor != null) {
                    cursor.close();
                }
            }
        }
        return res;
    }

    // Getters

    /**
     * Returns amount of events currently
     * in the database.
     *
     * @return the count of events in the
     * database
     */
    public long getSize() {
        return DatabaseUtils.queryNumEntries(database, EventStoreHelper.TABLE_EVENTS);
    }

    /**
     * Returns the last rowId to be
     * inserted.
     *
     * @return the last inserted rowId
     */
    public long getLastInsertedRowId() {
        return lastInsertedRowId;
    }

    /**
     * Returns an EmittableEvents object which
     * contains events and eventIds within a
     * defined range of the database.
     *
     * @return an EmittableEvents object containing
     * eventIds and event payloads.
     */
    @SuppressWarnings("unchecked")
    public EmittableEvents getEmittableEvents() {

        LinkedList<Long> eventIds = new LinkedList<>();
        ArrayList<Payload> events = new ArrayList<>();

        // FIFO Pattern for sending events
        for (Map<String, Object> eventMetadata : getDescEventsInRange(this.sendLimit)) {

            // Create a TrackerPayload for each event
            TrackerPayload payload = new TrackerPayload();
            Map<String, Object> eventData = (Map<String, Object>)
                    eventMetadata.get(EventStoreHelper.METADATA_EVENT_DATA);
            payload.addMap(eventData);

            // Store the eventId
            Long eventId = (Long) eventMetadata.get(EventStoreHelper.METADATA_ID);
            eventIds.add(eventId);

            // Add the payload to the list
            events.add(payload);
        }
        return new EmittableEvents(events, eventIds);
    }

    /**
     * Returns a Map containing the event 
     * payload values, the table row ID and
     * the date it was created.
     *
     * @param id the row id of the event to get
     * @return event metadata
     */
    public Map<String, Object> getEvent(long id) {
        List<Map<String, Object>> res =
                queryDatabase(EventStoreHelper.COLUMN_ID + "=" + id, null);

        if (!res.isEmpty()) {
            return res.get(0);
        } else {
            return null;
        }
    }

    /**
     * Returns a list of all the events in the
     * database.
     *
     * @return the events in the database
     */
    public List<Map<String, Object>> getAllEvents() {
        return queryDatabase(null, null);
    }

    /**
     * Returns a descending range of events
     * from the top of the database.
     *
     * @param range amount of rows to take
     * @return a list of event metadata
     */
    public List<Map<String, Object>> getDescEventsInRange(int range) {
        return queryDatabase(null, "id DESC LIMIT " + range);
    }

    /**
     * Returns truth on if database is open.
     *
     * @return a boolean for database status
     */
    public boolean isDatabaseOpen() {
        return database != null && database.isOpen();
    }
}