/*
 * 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.view.textclassifier;

import android.annotation.WorkerThread;
import android.view.textclassifier.SelectionEvent.InvocationMethod;

import com.android.internal.util.Preconditions;

/**
 * Session-aware TextClassifier.
 */
@WorkerThread
final class TextClassificationSession implements TextClassifier {

    /* package */ static final boolean DEBUG_LOG_ENABLED = true;
    private static final String LOG_TAG = "TextClassificationSession";

    private final TextClassifier mDelegate;
    private final SelectionEventHelper mEventHelper;
    private final TextClassificationSessionId mSessionId;
    private final TextClassificationContext mClassificationContext;

    private boolean mDestroyed;

    TextClassificationSession(TextClassificationContext context, TextClassifier delegate) {
        mClassificationContext = Preconditions.checkNotNull(context);
        mDelegate = Preconditions.checkNotNull(delegate);
        mSessionId = new TextClassificationSessionId();
        mEventHelper = new SelectionEventHelper(mSessionId, mClassificationContext);
        initializeRemoteSession();
    }

    @Override
    public TextSelection suggestSelection(TextSelection.Request request) {
        checkDestroyed();
        return mDelegate.suggestSelection(request);
    }

    private void initializeRemoteSession() {
        if (mDelegate instanceof SystemTextClassifier) {
            ((SystemTextClassifier) mDelegate).initializeRemoteSession(
                    mClassificationContext, mSessionId);
        }
    }

    @Override
    public TextClassification classifyText(TextClassification.Request request) {
        checkDestroyed();
        return mDelegate.classifyText(request);
    }

    @Override
    public TextLinks generateLinks(TextLinks.Request request) {
        checkDestroyed();
        return mDelegate.generateLinks(request);
    }

    @Override
    public void onSelectionEvent(SelectionEvent event) {
        checkDestroyed();
        Preconditions.checkNotNull(event);
        if (mEventHelper.sanitizeEvent(event)) {
            mDelegate.onSelectionEvent(event);
        }
    }

    @Override
    public void destroy() {
        mEventHelper.endSession();
        mDelegate.destroy();
        mDestroyed = true;
    }

    @Override
    public boolean isDestroyed() {
        return mDestroyed;
    }

    /**
     * @throws IllegalStateException if this TextClassification session has been destroyed.
     * @see #isDestroyed()
     * @see #destroy()
     */
    private void checkDestroyed() {
        if (mDestroyed) {
            throw new IllegalStateException("This TextClassification session has been destroyed");
        }
    }

    /**
     * Helper class for updating SelectionEvent fields.
     */
    private static final class SelectionEventHelper {

        private final TextClassificationSessionId mSessionId;
        private final TextClassificationContext mContext;

        @InvocationMethod
        private int mInvocationMethod = SelectionEvent.INVOCATION_UNKNOWN;
        private SelectionEvent mPrevEvent;
        private SelectionEvent mSmartEvent;
        private SelectionEvent mStartEvent;

        SelectionEventHelper(
                TextClassificationSessionId sessionId, TextClassificationContext context) {
            mSessionId = Preconditions.checkNotNull(sessionId);
            mContext = Preconditions.checkNotNull(context);
        }

        /**
         * Updates the necessary fields in the event for the current session.
         *
         * @return true if the event should be reported. false if the event should be ignored
         */
        boolean sanitizeEvent(SelectionEvent event) {
            updateInvocationMethod(event);
            modifyAutoSelectionEventType(event);

            if (event.getEventType() != SelectionEvent.EVENT_SELECTION_STARTED
                    && mStartEvent == null) {
                if (DEBUG_LOG_ENABLED) {
                    Log.d(LOG_TAG, "Selection session not yet started. Ignoring event");
                }
                return false;
            }

            final long now = System.currentTimeMillis();
            switch (event.getEventType()) {
                case SelectionEvent.EVENT_SELECTION_STARTED:
                    Preconditions.checkArgument(
                            event.getAbsoluteEnd() == event.getAbsoluteStart() + 1);
                    event.setSessionId(mSessionId);
                    mStartEvent = event;
                    break;
                case SelectionEvent.EVENT_SMART_SELECTION_SINGLE:  // fall through
                case SelectionEvent.EVENT_SMART_SELECTION_MULTI:
                    mSmartEvent = event;
                    break;
                case SelectionEvent.EVENT_SELECTION_MODIFIED:  // fall through
                case SelectionEvent.EVENT_AUTO_SELECTION:
                    if (mPrevEvent != null
                            && mPrevEvent.getAbsoluteStart() == event.getAbsoluteStart()
                            && mPrevEvent.getAbsoluteEnd() == event.getAbsoluteEnd()) {
                        // Selection did not change. Ignore event.
                        return false;
                    }
                    break;
                default:
                    // do nothing.
            }

            event.setEventTime(now);
            if (mStartEvent != null) {
                event.setSessionId(mStartEvent.getSessionId())
                        .setDurationSinceSessionStart(now - mStartEvent.getEventTime())
                        .setStart(event.getAbsoluteStart() - mStartEvent.getAbsoluteStart())
                        .setEnd(event.getAbsoluteEnd() - mStartEvent.getAbsoluteStart());
            }
            if (mSmartEvent != null) {
                event.setResultId(mSmartEvent.getResultId())
                        .setSmartStart(
                                mSmartEvent.getAbsoluteStart() - mStartEvent.getAbsoluteStart())
                        .setSmartEnd(mSmartEvent.getAbsoluteEnd() - mStartEvent.getAbsoluteStart());
            }
            if (mPrevEvent != null) {
                event.setDurationSincePreviousEvent(now - mPrevEvent.getEventTime())
                        .setEventIndex(mPrevEvent.getEventIndex() + 1);
            }
            mPrevEvent = event;
            return true;
        }

        void endSession() {
            mPrevEvent = null;
            mSmartEvent = null;
            mStartEvent = null;
        }

        private void updateInvocationMethod(SelectionEvent event) {
            event.setTextClassificationSessionContext(mContext);
            if (event.getInvocationMethod() == SelectionEvent.INVOCATION_UNKNOWN) {
                event.setInvocationMethod(mInvocationMethod);
            } else {
                mInvocationMethod = event.getInvocationMethod();
            }
        }

        private void modifyAutoSelectionEventType(SelectionEvent event) {
            switch (event.getEventType()) {
                case SelectionEvent.EVENT_SMART_SELECTION_SINGLE:  // fall through
                case SelectionEvent.EVENT_SMART_SELECTION_MULTI:  // fall through
                case SelectionEvent.EVENT_AUTO_SELECTION:
                    if (isPlatformLocalTextClassifierSmartSelection(event.getResultId())) {
                        if (event.getAbsoluteEnd() - event.getAbsoluteStart() > 1) {
                            event.setEventType(SelectionEvent.EVENT_SMART_SELECTION_MULTI);
                        } else {
                            event.setEventType(SelectionEvent.EVENT_SMART_SELECTION_SINGLE);
                        }
                    } else {
                        event.setEventType(SelectionEvent.EVENT_AUTO_SELECTION);
                    }
                    return;
                default:
                    return;
            }
        }

        private static boolean isPlatformLocalTextClassifierSmartSelection(String signature) {
            return SelectionSessionLogger.CLASSIFIER_ID.equals(
                    SelectionSessionLogger.SignatureParser.getClassifierId(signature));
        }
    }
}
