/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
 * You can obtain one at http://mozilla.org/MPL/2.0/. */

package org.mozilla.gecko.db;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;

import org.json.JSONArray;
import org.json.JSONException;
import org.mozilla.gecko.Tab;
import org.mozilla.gecko.util.ThreadUtils;
import org.mozilla.gecko.util.UIAsyncTask;

import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.text.TextUtils;
import android.util.Log;

public class LocalTabsAccessor implements TabsAccessor {
    private static final String LOGTAG = "GeckoTabsAccessor";
    private static final long THREE_WEEKS_IN_MILLISECONDS = TimeUnit.MILLISECONDS.convert(21L, TimeUnit.DAYS);

    public static final String[] TABS_PROJECTION_COLUMNS = new String[] {
                                                                BrowserContract.Tabs.TITLE,
                                                                BrowserContract.Tabs.URL,
                                                                BrowserContract.Clients.GUID,
                                                                BrowserContract.Clients.NAME,
                                                                BrowserContract.Tabs.LAST_USED,
                                                                BrowserContract.Clients.LAST_MODIFIED,
                                                                BrowserContract.Clients.DEVICE_TYPE,
                                                            };

    public static final String[] CLIENTS_PROJECTION_COLUMNS = new String[] {
                                                                    BrowserContract.Clients.GUID,
                                                                    BrowserContract.Clients.NAME,
                                                                    BrowserContract.Clients.LAST_MODIFIED,
                                                                    BrowserContract.Clients.DEVICE_TYPE
                                                            };

    private static final String REMOTE_CLIENTS_SELECTION = BrowserContract.Clients.GUID + " IS NOT NULL";
    private static final String LOCAL_TABS_SELECTION = BrowserContract.Tabs.CLIENT_GUID + " IS NULL";
    private static final String REMOTE_TABS_SELECTION = BrowserContract.Tabs.CLIENT_GUID + " IS NOT NULL";
    private static final String REMOTE_TABS_SELECTION_CLIENT_RECENCY = REMOTE_TABS_SELECTION +
            " AND " + BrowserContract.Clients.LAST_MODIFIED + " > ?";

    private static final String REMOTE_TABS_SORT_ORDER =
            // Most recently synced clients first.
            BrowserContract.Clients.LAST_MODIFIED + " DESC, " +
            // If two clients somehow had the same last modified time, this will
            // group them (arbitrarily).
            BrowserContract.Clients.GUID + " DESC, " +
            // Within a single client, most recently used tabs first.
            BrowserContract.Tabs.LAST_USED + " DESC";

    private static final String LOCAL_CLIENT_SELECTION = BrowserContract.Clients.GUID + " IS NULL";

    private static final Pattern FILTERED_URL_PATTERN = Pattern.compile("^(about|chrome|wyciwyg|file):");

    private final Uri clientsRecencyUriWithProfile;
    private final Uri tabsUriWithProfile;
    private final Uri clientsUriWithProfile;

    public LocalTabsAccessor(String profileName) {
        tabsUriWithProfile = DBUtils.appendProfileWithDefault(profileName, BrowserContract.Tabs.CONTENT_URI);
        clientsUriWithProfile = DBUtils.appendProfileWithDefault(profileName, BrowserContract.Clients.CONTENT_URI);
        clientsRecencyUriWithProfile = DBUtils.appendProfileWithDefault(profileName, BrowserContract.Clients.CONTENT_RECENCY_URI);
    }

    /**
     * Extracts a List of just RemoteClients from a cursor.
     * The supplied cursor should be grouped by guid and sorted by most recently used.
     */
    @Override
    public List<RemoteClient> getClientsWithoutTabsByRecencyFromCursor(Cursor cursor) {
        final ArrayList<RemoteClient> clients = new ArrayList<>(cursor.getCount());

        final int originalPosition = cursor.getPosition();
        try {
            if (!cursor.moveToFirst()) {
                return clients;
            }

            final int clientGuidIndex = cursor.getColumnIndex(BrowserContract.Clients.GUID);
            final int clientNameIndex = cursor.getColumnIndex(BrowserContract.Clients.NAME);
            final int clientLastModifiedIndex = cursor.getColumnIndex(BrowserContract.Clients.LAST_MODIFIED);
            final int clientDeviceTypeIndex = cursor.getColumnIndex(BrowserContract.Clients.DEVICE_TYPE);

            while (!cursor.isAfterLast()) {
                final String clientGuid = cursor.getString(clientGuidIndex);
                final String clientName = cursor.getString(clientNameIndex);
                final String deviceType = cursor.getString(clientDeviceTypeIndex);
                final long lastModified = cursor.getLong(clientLastModifiedIndex);

                clients.add(new RemoteClient(clientGuid, clientName, lastModified, deviceType));

                cursor.moveToNext();
            }
        } finally {
            cursor.moveToPosition(originalPosition);
        }
        return clients;
    }

    /**
     * Extract client and tab records from a cursor.
     * <p>
     * The position of the cursor is moved to before the first record before
     * reading. The cursor is advanced until there are no more records to be
     * read. The position of the cursor is restored before returning.
     *
     * @param cursor
     *            to extract records from. The records should already be grouped
     *            by client GUID.
     * @return list of clients, each containing list of tabs.
     */
    @Override
    public List<RemoteClient> getClientsFromCursor(final Cursor cursor) {
        final ArrayList<RemoteClient> clients = new ArrayList<RemoteClient>();

        final int originalPosition = cursor.getPosition();
        try {
            if (!cursor.moveToFirst()) {
                return clients;
            }

            final int tabTitleIndex = cursor.getColumnIndex(BrowserContract.Tabs.TITLE);
            final int tabUrlIndex = cursor.getColumnIndex(BrowserContract.Tabs.URL);
            final int tabLastUsedIndex = cursor.getColumnIndex(BrowserContract.Tabs.LAST_USED);
            final int clientGuidIndex = cursor.getColumnIndex(BrowserContract.Clients.GUID);
            final int clientNameIndex = cursor.getColumnIndex(BrowserContract.Clients.NAME);
            final int clientLastModifiedIndex = cursor.getColumnIndex(BrowserContract.Clients.LAST_MODIFIED);
            final int clientDeviceTypeIndex = cursor.getColumnIndex(BrowserContract.Clients.DEVICE_TYPE);

            // A walking partition, chunking by client GUID. We assume the
            // cursor records are already grouped by client GUID; see the query
            // sort order.
            RemoteClient lastClient = null;
            while (!cursor.isAfterLast()) {
                final String clientGuid = cursor.getString(clientGuidIndex);
                if (lastClient == null || !TextUtils.equals(lastClient.guid, clientGuid)) {
                    final String clientName = cursor.getString(clientNameIndex);
                    final long lastModified = cursor.getLong(clientLastModifiedIndex);
                    final String deviceType = cursor.getString(clientDeviceTypeIndex);
                    lastClient = new RemoteClient(clientGuid, clientName, lastModified, deviceType);
                    clients.add(lastClient);
                }

                final String tabTitle = cursor.getString(tabTitleIndex);
                final String tabUrl = cursor.getString(tabUrlIndex);
                final long tabLastUsed = cursor.getLong(tabLastUsedIndex);
                lastClient.tabs.add(new RemoteTab(tabTitle, tabUrl, tabLastUsed));

                cursor.moveToNext();
            }
        } finally {
            cursor.moveToPosition(originalPosition);
        }

        return clients;
    }

    @Override
    public Cursor getRemoteClientsByRecencyCursor(Context context) {
        final Uri uri = clientsRecencyUriWithProfile;
        return context.getContentResolver().query(uri, CLIENTS_PROJECTION_COLUMNS,
                REMOTE_CLIENTS_SELECTION, null, null);
    }

    @Override
    public Cursor getRemoteTabsCursor(Context context) {
        return getRemoteTabsCursor(context, -1);
    }

    @Override
    public Cursor getRemoteTabsCursor(Context context, int limit) {
        Uri uri = tabsUriWithProfile;

        if (limit > 0) {
            uri = uri.buildUpon()
                     .appendQueryParameter(BrowserContract.PARAM_LIMIT, String.valueOf(limit))
                     .build();
        }

        final String threeWeeksAgoTimestampMillis = Long.valueOf(
                System.currentTimeMillis() - THREE_WEEKS_IN_MILLISECONDS).toString();
        return context.getContentResolver().query(uri,
                                                            TABS_PROJECTION_COLUMNS,
                                                            REMOTE_TABS_SELECTION_CLIENT_RECENCY,
                                                            new String[] {threeWeeksAgoTimestampMillis},
                                                            REMOTE_TABS_SORT_ORDER);
    }

    // This method returns all tabs from all remote clients,
    // ordered by most recent client first, most recent tab first
    @Override
    public void getTabs(final Context context, final OnQueryTabsCompleteListener listener) {
        getTabs(context, 0, listener);
    }

    // This method returns limited number of tabs from all remote clients,
    // ordered by most recent client first, most recent tab first
    @Override
    public void getTabs(final Context context, final int limit, final OnQueryTabsCompleteListener listener) {
        // If there is no listener, no point in doing work.
        if (listener == null)
            return;

        (new UIAsyncTask.WithoutParams<List<RemoteClient>>(ThreadUtils.getBackgroundHandler()) {
            @Override
            protected List<RemoteClient> doInBackground() {
                final Cursor cursor = getRemoteTabsCursor(context, limit);
                if (cursor == null)
                    return null;

                try {
                    return Collections.unmodifiableList(getClientsFromCursor(cursor));
                } finally {
                    cursor.close();
                }
            }

            @Override
            protected void onPostExecute(List<RemoteClient> clients) {
                listener.onQueryTabsComplete(clients);
            }
        }).execute();
    }

    // Updates the modified time of the local client with the current time.
    private void updateLocalClient(final ContentResolver cr) {
        ContentValues values = new ContentValues();
        values.put(BrowserContract.Clients.LAST_MODIFIED, System.currentTimeMillis());

        cr.update(clientsUriWithProfile, values, LOCAL_CLIENT_SELECTION, null);
    }

    // Deletes all local tabs.
    private void deleteLocalTabs(final ContentResolver cr) {
        cr.delete(tabsUriWithProfile, LOCAL_TABS_SELECTION, null);
    }

    /**
     * Tabs are positioned in the DB in the same order that they appear in the tabs param.
     *   - URL should never empty or null. Skip this tab if there's no URL.
     *   - TITLE should always a string, either a page title or empty.
     *   - LAST_USED should always be numeric.
     *   - FAVICON should be a URL or null.
     *   - HISTORY should be serialized JSON array of URLs.
     *   - POSITION should always be numeric.
     *   - CLIENT_GUID should always be null to represent the local client.
     */
    private void insertLocalTabs(final ContentResolver cr, final Iterable<Tab> tabs) {
        // Reuse this for serializing individual history URLs as JSON.
        JSONArray history = new JSONArray();
        ArrayList<ContentValues> valuesToInsert = new ArrayList<ContentValues>();

        int position = 0;
        for (Tab tab : tabs) {
            // Skip this tab if it has a null URL or is in private browsing mode, or is a filtered URL.
            String url = tab.getURL();
            if (url == null || tab.isPrivate() || isFilteredURL(url))
                continue;

            ContentValues values = new ContentValues();
            values.put(BrowserContract.Tabs.URL, url);
            values.put(BrowserContract.Tabs.TITLE, tab.getTitle());
            values.put(BrowserContract.Tabs.LAST_USED, tab.getLastUsed());

            String favicon = tab.getFaviconURL();
            if (favicon != null)
                values.put(BrowserContract.Tabs.FAVICON, favicon);
            else
                values.putNull(BrowserContract.Tabs.FAVICON);

            // We don't have access to session history in Java, so for now, we'll
            // just use a JSONArray that holds most recent history item.
            try {
                history.put(0, tab.getURL());
                values.put(BrowserContract.Tabs.HISTORY, history.toString());
            } catch (JSONException e) {
                Log.w(LOGTAG, "JSONException adding URL to tab history array.", e);
            }

            values.put(BrowserContract.Tabs.POSITION, position++);

            // A null client guid corresponds to the local client.
            values.putNull(BrowserContract.Tabs.CLIENT_GUID);

            valuesToInsert.add(values);
        }

        ContentValues[] valuesToInsertArray = valuesToInsert.toArray(new ContentValues[valuesToInsert.size()]);
        cr.bulkInsert(tabsUriWithProfile, valuesToInsertArray);
    }

    // Deletes all local tabs and replaces them with a new list of tabs.
    @Override
    public synchronized void persistLocalTabs(final ContentResolver cr, final Iterable<Tab> tabs) {
        deleteLocalTabs(cr);
        insertLocalTabs(cr, tabs);
        updateLocalClient(cr);
    }

    /**
     * Matches the supplied URL string against the set of URLs to filter.
     *
     * @return true if the supplied URL should be skipped; false otherwise.
     */
    private boolean isFilteredURL(String url) {
        return FILTERED_URL_PATTERN.matcher(url).lookingAt();
    }
}
