/*
 * Copyright (C) 2018 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 android.database;

import android.annotation.NonNull;
import android.content.ContentResolver;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteQueryBuilder;
import android.net.Uri;
import android.os.CancellationSignal;
import android.util.ArraySet;

import com.android.internal.util.ArrayUtils;

import java.util.Arrays;
import java.util.Objects;

/**
 * Cursor that supports deprecation of {@code _data} like columns which represent raw filepaths,
 * typically by replacing values with fake paths that the OS then offers to redirect to
 * {@link ContentResolver#openFileDescriptor(Uri, String)}, which developers
 * should be using directly.
 *
 * @hide
 */
public class TranslatingCursor extends CrossProcessCursorWrapper {
    public static class Config {
        public final Uri baseUri;
        public final String auxiliaryColumn;
        public final String[] translateColumns;

        public Config(Uri baseUri, String auxiliaryColumn, String... translateColumns) {
            this.baseUri = baseUri;
            this.auxiliaryColumn = auxiliaryColumn;
            this.translateColumns = translateColumns;
        }
    }

    public interface Translator {
        String translate(String data, int auxiliaryColumnIndex,
                String matchingColumn, Cursor cursor);
    }

    private final @NonNull Config mConfig;
    private final @NonNull Translator mTranslator;
    private final boolean mDropLast;

    private final int mAuxiliaryColumnIndex;
    private final ArraySet<Integer> mTranslateColumnIndices;

    public TranslatingCursor(@NonNull Cursor cursor, @NonNull Config config,
            @NonNull Translator translator, boolean dropLast) {
        super(cursor);

        mConfig = Objects.requireNonNull(config);
        mTranslator = Objects.requireNonNull(translator);
        mDropLast = dropLast;

        mAuxiliaryColumnIndex = cursor.getColumnIndexOrThrow(config.auxiliaryColumn);
        mTranslateColumnIndices = new ArraySet<>();
        for (int i = 0; i < cursor.getColumnCount(); ++i) {
            String columnName = cursor.getColumnName(i);
            if (ArrayUtils.contains(config.translateColumns, columnName)) {
                mTranslateColumnIndices.add(i);
            }
        }
    }

    @Override
    public int getColumnCount() {
        if (mDropLast) {
            return super.getColumnCount() - 1;
        } else {
            return super.getColumnCount();
        }
    }

    @Override
    public String[] getColumnNames() {
        if (mDropLast) {
            return Arrays.copyOfRange(super.getColumnNames(), 0, super.getColumnCount() - 1);
        } else {
            return super.getColumnNames();
        }
    }

    public static Cursor query(@NonNull Config config, @NonNull Translator translator,
            SQLiteQueryBuilder qb, SQLiteDatabase db, String[] projectionIn, String selection,
            String[] selectionArgs, String groupBy, String having, String sortOrder, String limit,
            CancellationSignal signal) {
        final boolean requestedAuxiliaryColumn = ArrayUtils.isEmpty(projectionIn)
                || ArrayUtils.contains(projectionIn, config.auxiliaryColumn);
        final boolean requestedTranslateColumns = ArrayUtils.isEmpty(projectionIn)
                || ArrayUtils.containsAny(projectionIn, config.translateColumns);

        // If caller didn't request any columns that need to be translated,
        // we have nothing to redirect
        if (!requestedTranslateColumns) {
            return qb.query(db, projectionIn, selection, selectionArgs,
                    groupBy, having, sortOrder, limit, signal);
        }

        // If caller didn't request auxiliary column, we need to splice it in
        if (!requestedAuxiliaryColumn) {
            projectionIn = ArrayUtils.appendElement(String.class, projectionIn,
                    config.auxiliaryColumn);
        }

        final Cursor c = qb.query(db, projectionIn, selection, selectionArgs,
                groupBy, having, sortOrder);
        return new TranslatingCursor(c, config, translator, !requestedAuxiliaryColumn);
    }

    @Override
    public void fillWindow(int position, CursorWindow window) {
        // Fill window directly to ensure data is rewritten
        DatabaseUtils.cursorFillWindow(this, position, window);
    }

    @Override
    public CursorWindow getWindow() {
        // Returning underlying window risks leaking data
        return null;
    }

    @Override
    public Cursor getWrappedCursor() {
        throw new UnsupportedOperationException(
                "Returning underlying cursor risks leaking data");
    }

    @Override
    public double getDouble(int columnIndex) {
        if (ArrayUtils.contains(mTranslateColumnIndices, columnIndex)) {
            throw new IllegalArgumentException();
        } else {
            return super.getDouble(columnIndex);
        }
    }

    @Override
    public float getFloat(int columnIndex) {
        if (ArrayUtils.contains(mTranslateColumnIndices, columnIndex)) {
            throw new IllegalArgumentException();
        } else {
            return super.getFloat(columnIndex);
        }
    }

    @Override
    public int getInt(int columnIndex) {
        if (ArrayUtils.contains(mTranslateColumnIndices, columnIndex)) {
            throw new IllegalArgumentException();
        } else {
            return super.getInt(columnIndex);
        }
    }

    @Override
    public long getLong(int columnIndex) {
        if (ArrayUtils.contains(mTranslateColumnIndices, columnIndex)) {
            throw new IllegalArgumentException();
        } else {
            return super.getLong(columnIndex);
        }
    }

    @Override
    public short getShort(int columnIndex) {
        if (ArrayUtils.contains(mTranslateColumnIndices, columnIndex)) {
            throw new IllegalArgumentException();
        } else {
            return super.getShort(columnIndex);
        }
    }

    @Override
    public String getString(int columnIndex) {
        if (ArrayUtils.contains(mTranslateColumnIndices, columnIndex)) {
            return mTranslator.translate(super.getString(columnIndex),
                    mAuxiliaryColumnIndex, getColumnName(columnIndex), this);
        } else {
            return super.getString(columnIndex);
        }
    }

    @Override
    public void copyStringToBuffer(int columnIndex, CharArrayBuffer buffer) {
        if (ArrayUtils.contains(mTranslateColumnIndices, columnIndex)) {
            throw new IllegalArgumentException();
        } else {
            super.copyStringToBuffer(columnIndex, buffer);
        }
    }

    @Override
    public byte[] getBlob(int columnIndex) {
        if (ArrayUtils.contains(mTranslateColumnIndices, columnIndex)) {
            throw new IllegalArgumentException();
        } else {
            return super.getBlob(columnIndex);
        }
    }

    @Override
    public int getType(int columnIndex) {
        if (ArrayUtils.contains(mTranslateColumnIndices, columnIndex)) {
            return Cursor.FIELD_TYPE_STRING;
        } else {
            return super.getType(columnIndex);
        }
    }

    @Override
    public boolean isNull(int columnIndex) {
        if (ArrayUtils.contains(mTranslateColumnIndices, columnIndex)) {
            return getString(columnIndex) == null;
        } else {
            return super.isNull(columnIndex);
        }
    }
}
