/*
 * Copyright (c) 2016, The Android Open Source Project
 *
 * 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 com.android.car.media.localmediaplayer;

import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteException;
import android.media.MediaDescription;
import android.media.MediaMetadata;
import android.media.browse.MediaBrowser.MediaItem;
import android.media.session.MediaSession.QueueItem;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.provider.MediaStore;
import android.provider.MediaStore.Audio.AlbumColumns;
import android.provider.MediaStore.Audio.AudioColumns;
import android.service.media.MediaBrowserService.Result;
import android.util.Log;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

public class DataModel {
    private static final String TAG = "LMBDataModel";

    private static final Uri[] ALL_AUDIO_URI = new Uri[] {
            MediaStore.Audio.Media.INTERNAL_CONTENT_URI,
            MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
    };

    private static final Uri[] ALBUMS_URI = new Uri[] {
            MediaStore.Audio.Albums.INTERNAL_CONTENT_URI,
            MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI
    };

    private static final Uri[] ARTISTS_URI = new Uri[] {
            MediaStore.Audio.Artists.INTERNAL_CONTENT_URI,
            MediaStore.Audio.Artists.EXTERNAL_CONTENT_URI
    };

    private static final Uri[] GENRES_URI = new Uri[] {
        MediaStore.Audio.Genres.INTERNAL_CONTENT_URI,
        MediaStore.Audio.Genres.EXTERNAL_CONTENT_URI
    };

    private static final String QUERY_BY_KEY_WHERE_CLAUSE =
            AudioColumns.ALBUM_KEY + "= ? or "
                    + AudioColumns.ARTIST_KEY + " = ? or "
                    + AudioColumns.TITLE_KEY + " = ? or "
                    + AudioColumns.DATA + " like ?";

    private static final String EXTERNAL = "external";
    private static final String INTERNAL = "internal";

    private static final Uri ART_BASE_URI = Uri.parse("content://media/external/audio/albumart");

    public static final String PATH_KEY = "PATH";

    private Context mContext;
    private ContentResolver mResolver;
    private AsyncTask mPendingTask;

    private List<QueueItem> mQueue = new ArrayList<>();

    public DataModel(Context context) {
        mContext = context;
        mResolver = context.getContentResolver();
    }

    public void onQueryByFolder(String parentId, Result<List<MediaItem>> result) {
        FilesystemListTask query = new FilesystemListTask(result, ALL_AUDIO_URI, mResolver);
        queryInBackground(result, query);
    }

    public void onQueryByAlbum(String parentId, Result<List<MediaItem>> result) {
        QueryTask query = new QueryTask.Builder()
                .setResolver(mResolver)
                .setResult(result)
                .setUri(ALBUMS_URI)
                .setKeyColumn(AudioColumns.ALBUM_KEY)
                .setTitleColumn(AudioColumns.ALBUM)
                .setFlags(MediaItem.FLAG_BROWSABLE)
                .build();
        queryInBackground(result, query);
    }

    public void onQueryByArtist(String parentId, Result<List<MediaItem>> result) {
        QueryTask query = new QueryTask.Builder()
                .setResolver(mResolver)
                .setResult(result)
                .setUri(ARTISTS_URI)
                .setKeyColumn(AudioColumns.ARTIST_KEY)
                .setTitleColumn(AudioColumns.ARTIST)
                .setFlags(MediaItem.FLAG_BROWSABLE)
                .build();
        queryInBackground(result, query);
    }

    public void onQueryByGenre(String parentId, Result<List<MediaItem>> result) {
        QueryTask query = new QueryTask.Builder()
                .setResolver(mResolver)
                .setResult(result)
                .setUri(GENRES_URI)
                .setKeyColumn(MediaStore.Audio.Genres._ID)
                .setTitleColumn(MediaStore.Audio.Genres.NAME)
                .setFlags(MediaItem.FLAG_BROWSABLE)
                .build();
        queryInBackground(result, query);
    }

    private void queryInBackground(Result<List<MediaItem>> result,
            AsyncTask<Void, Void, Void> task) {
        result.detach();

        if (mPendingTask != null) {
            mPendingTask.cancel(true);
        }

        mPendingTask = task;
        task.execute();
    }

    public List<QueueItem> getQueue() {
        return mQueue;
    }

    public MediaMetadata getMetadata(String key) {
        Cursor cursor = null;
        MediaMetadata.Builder metadata = new MediaMetadata.Builder();
        try {
            for (Uri uri : ALL_AUDIO_URI) {
                cursor = mResolver.query(uri, null, AudioColumns.TITLE_KEY + " = ?",
                        new String[]{ key }, null);
                if (cursor != null) {
                    int title = cursor.getColumnIndex(AudioColumns.TITLE);
                    int artist = cursor.getColumnIndex(AudioColumns.ARTIST);
                    int album = cursor.getColumnIndex(AudioColumns.ALBUM);
                    int albumId = cursor.getColumnIndex(AudioColumns.ALBUM_ID);
                    int duration = cursor.getColumnIndex(AudioColumns.DURATION);

                    while (cursor.moveToNext()) {
                        metadata.putString(MediaMetadata.METADATA_KEY_TITLE,
                                cursor.getString(title));
                        metadata.putString(MediaMetadata.METADATA_KEY_ARTIST,
                                cursor.getString(artist));
                        metadata.putString(MediaMetadata.METADATA_KEY_ALBUM,
                                cursor.getString(album));
                        metadata.putLong(MediaMetadata.METADATA_KEY_DURATION,
                                cursor.getLong(duration));

                        String albumArt = null;
                        Uri albumArtUri = ContentUris.withAppendedId(ART_BASE_URI,
                                cursor.getLong(albumId));
                        try {
                            InputStream unused = mResolver.openInputStream(albumArtUri);
                            albumArt = albumArtUri.toString();
                            unused.close();
                        } catch (IOException e) {
                            // Ignored because the albumArt is intialized correctly anyway.
                        }
                        metadata.putString(MediaMetadata.METADATA_KEY_ALBUM_ART_URI, albumArt);
                        break;
                    }
                }
            }
        } finally {
            if (cursor != null) {
                cursor.close();
            }
        }

        return metadata.build();
    }

    /**
     * Note: This clears out the queue. You should have a local copy of the queue before calling
     * this method.
     */
    public void onQueryByKey(String lastCategory, String parentId, Result<List<MediaItem>> result) {
        mQueue.clear();

        QueryTask.Builder query = new QueryTask.Builder()
                .setResolver(mResolver)
                .setResult(result);

        if (LocalMediaBrowserService.GENRES_ID.equals(lastCategory)) {
            // Genres come from a different table and don't use the where clause from the
            // usual media table so we need to have this condition.
            try {
                long id = Long.parseLong(parentId);
                query.setUri(new Uri[] {
                    MediaStore.Audio.Genres.Members.getContentUri(EXTERNAL, id),
                    MediaStore.Audio.Genres.Members.getContentUri(INTERNAL, id) });
            } catch (NumberFormatException e) {
                // This should never happen.
                Log.e(TAG, "Incorrect key type: " + parentId + ", sending empty result");
                result.sendResult(new ArrayList<MediaItem>());
                return;
            }
        } else {
            query.setUri(ALL_AUDIO_URI)
                    .setWhereClause(QUERY_BY_KEY_WHERE_CLAUSE)
                    .setWhereArgs(new String[] { parentId, parentId, parentId, parentId });
        }

        query.setKeyColumn(AudioColumns.TITLE_KEY)
                .setTitleColumn(AudioColumns.TITLE)
                .setSubtitleColumn(AudioColumns.ALBUM)
                .setFlags(MediaItem.FLAG_PLAYABLE)
                .setQueue(mQueue);
        queryInBackground(result, query.build());
    }

    // This async task is similar enough to all the others that it feels like it can be unified
    // but is different enough that unifying it makes the code for both cases look really weird
    // and over paramterized so at the risk of being a little more verbose, this is separated out
    // in the name of understandability.
    private static class FilesystemListTask extends AsyncTask<Void, Void, Void> {
        private static final String[] COLUMNS = { AudioColumns.DATA };
        private Result<List<MediaItem>> mResult;
        private Uri[] mUris;
        private ContentResolver mResolver;

        public FilesystemListTask(Result<List<MediaItem>> result, Uri[] uris,
                ContentResolver resolver) {
            mResult = result;
            mUris = uris;
            mResolver = resolver;
        }

        @Override
        protected Void doInBackground(Void... voids) {
            Set<String> paths = new HashSet<String>();

            Cursor cursor = null;
            for (Uri uri : mUris) {
                try {
                    cursor = mResolver.query(uri, COLUMNS, null , null, null);
                    if (cursor != null) {
                        int pathColumn = cursor.getColumnIndex(AudioColumns.DATA);

                        while (cursor.moveToNext()) {
                            // We want to de-dupe paths of each of the songs so we get just a list
                            // of containing directories.
                            String fullPath = cursor.getString(pathColumn);
                            int fileNameStart = fullPath.lastIndexOf(File.separator);
                            if (fileNameStart < 0) {
                                continue;
                            }

                            String dirPath = fullPath.substring(0, fileNameStart);
                            paths.add(dirPath);
                        }
                    }
                } catch (SQLiteException e) {
                    Log.e(TAG, "Failed to execute query " + e);  // Stack trace is noisy.
                } finally {
                    if (cursor != null) {
                        cursor.close();
                    }
                }
            }

            // Take the list of deduplicated directories and put them into the results list with
            // the full directory path as the key so we can match on it later.
            List<MediaItem> results = new ArrayList<>();
            for (String path : paths) {
                int dirNameStart = path.lastIndexOf(File.separator) + 1;
                String dirName = path.substring(dirNameStart, path.length());
                MediaDescription description = new MediaDescription.Builder()
                        .setMediaId(path + "%")  // Used in a like query.
                        .setTitle(dirName)
                        .setSubtitle(path)
                        .build();
                results.add(new MediaItem(description, MediaItem.FLAG_BROWSABLE));
            }
            mResult.sendResult(results);
            return null;
        }
    }

    private static class QueryTask extends AsyncTask<Void, Void, Void> {
        private Result<List<MediaItem>> mResult;
        private String[] mColumns;
        private String mWhereClause;
        private String[] mWhereArgs;
        private String mKeyColumn;
        private String mTitleColumn;
        private String mSubtitleColumn;
        private Uri[] mUris;
        private int mFlags;
        private ContentResolver mResolver;
        private List<QueueItem> mQueue;

        private QueryTask(Builder builder) {
            mColumns = builder.mColumns;
            mWhereClause = builder.mWhereClause;
            mWhereArgs = builder.mWhereArgs;
            mKeyColumn = builder.mKeyColumn;
            mTitleColumn = builder.mTitleColumn;
            mUris = builder.mUris;
            mFlags = builder.mFlags;
            mResolver = builder.mResolver;
            mResult = builder.mResult;
            mQueue = builder.mQueue;
            mSubtitleColumn = builder.mSubtitleColumn;
        }

        @Override
        protected Void doInBackground(Void... voids) {
            List<MediaItem> results = new ArrayList<>();

            long idx = 0;

            Cursor cursor = null;
            for (Uri uri : mUris) {
                try {
                    cursor = mResolver.query(uri, mColumns, mWhereClause, mWhereArgs, null);
                    if (cursor != null) {
                        int keyColumn = cursor.getColumnIndex(mKeyColumn);
                        int titleColumn = cursor.getColumnIndex(mTitleColumn);
                        int pathColumn = cursor.getColumnIndex(AudioColumns.DATA);
                        int subtitleColumn = -1;
                        if (mSubtitleColumn != null) {
                            subtitleColumn = cursor.getColumnIndex(mSubtitleColumn);
                        }

                        while (cursor.moveToNext()) {
                            Bundle path = new Bundle();
                            if (pathColumn != -1) {
                                path.putString(PATH_KEY, cursor.getString(pathColumn));
                            }

                            MediaDescription.Builder builder = new MediaDescription.Builder()
                                    .setMediaId(cursor.getString(keyColumn))
                                    .setTitle(cursor.getString(titleColumn))
                                    .setExtras(path);

                            if (subtitleColumn != -1) {
                                builder.setSubtitle(cursor.getString(subtitleColumn));
                            }

                            MediaDescription description = builder.build();
                            results.add(new MediaItem(description, mFlags));

                            // We rebuild the queue here so if the user selects the item then we
                            // can immediately use this queue.
                            if (mQueue != null) {
                                mQueue.add(new QueueItem(description, idx));
                            }
                            idx++;
                        }
                    }
                } catch (SQLiteException e) {
                    // Sometimes tables don't exist if the media scanner hasn't seen data of that
                    // type yet. For example, the genres table doesn't seem to exist at all until
                    // the first time a song with a genre is encountered. If we hit an exception,
                    // the result is never sent causing the other end to hang up, which is a bad
                    // thing. We can instead just be resilient and return an empty list.
                    Log.i(TAG, "Failed to execute query " + e);  // Stack trace is noisy.
                } finally {
                    if (cursor != null) {
                        cursor.close();
                    }
                }
            }

            mResult.sendResult(results);
            return null;  // Ignored.
        }

        //
        // Boilerplate Alert!
        //
        public static class Builder {
            private Result<List<MediaItem>> mResult;
            private String[] mColumns;
            private String mWhereClause;
            private String[] mWhereArgs;
            private String mKeyColumn;
            private String mTitleColumn;
            private String mSubtitleColumn;
            private Uri[] mUris;
            private int mFlags;
            private ContentResolver mResolver;
            private List<QueueItem> mQueue;

            public Builder setColumns(String[] columns) {
                mColumns = columns;
                return this;
            }

            public Builder setWhereClause(String whereClause) {
                mWhereClause = whereClause;
                return this;
            }

            public Builder setWhereArgs(String[] whereArgs) {
                mWhereArgs = whereArgs;
                return this;
            }

            public Builder setUri(Uri[] uris) {
                mUris = uris;
                return this;
            }

            public Builder setKeyColumn(String keyColumn) {
                mKeyColumn = keyColumn;
                return this;
            }

            public Builder setTitleColumn(String titleColumn) {
                mTitleColumn = titleColumn;
                return this;
            }

            public Builder setSubtitleColumn(String subtitleColumn) {
                mSubtitleColumn = subtitleColumn;
                return this;
            }

            public Builder setFlags(int flags) {
                mFlags = flags;
                return this;
            }

            public Builder setResult(Result<List<MediaItem>> result) {
                mResult = result;
                return this;
            }

            public Builder setResolver(ContentResolver resolver) {
                mResolver = resolver;
                return this;
            }

            public Builder setQueue(List<QueueItem> queue) {
                mQueue = queue;
                return this;
            }

            public QueryTask build() {
                if (mUris == null || mKeyColumn == null || mResolver == null ||
                        mResult == null || mTitleColumn == null) {
                    throw new IllegalStateException(
                            "uri, keyColumn, resolver, result and titleColumn are required.");
                }
                return new QueryTask(this);
            }
        }
    }
}
