/*
 * Copyright (C) 2008 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.launcher;

import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.res.Configuration;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.util.AttributeSet;
import android.util.Log;
import android.view.KeyEvent;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.View.OnKeyListener;
import android.view.View.OnLongClickListener;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.view.animation.Animation;
import android.view.animation.Interpolator;
import android.view.animation.Transformation;
import android.view.inputmethod.InputMethodManager;
import android.widget.ImageButton;
import android.widget.LinearLayout;
import android.widget.TextView;

public class Search extends LinearLayout 
        implements OnClickListener, OnKeyListener, OnLongClickListener {

    // Speed at which the widget slides up/down, in pixels/ms.
    private static final float ANIMATION_VELOCITY = 1.0f;

    /** The distance in dips between the optical top of the widget and the top if its bounds */
    private static final float WIDGET_TOP_OFFSET = 9;

    private final String TAG = "SearchWidget";

    private Launcher mLauncher;

    private TextView mSearchText;
    private ImageButton mVoiceButton;

    /** The animation that morphs the search widget to the search dialog. */
    private Animation mMorphAnimation;

    /** The animation that morphs the search widget back to its normal position. */
    private Animation mUnmorphAnimation;

    // These four are passed to Launcher.startSearch() when the search widget
    // has finished morphing. They are instance variables to make it possible to update
    // them while the widget is morphing.
    private String mInitialQuery;
    private boolean mSelectInitialQuery;    
    private Bundle mAppSearchData;
    private boolean mGlobalSearch;

    // For voice searching
    private Intent mVoiceSearchIntent;
    
    private int mWidgetTopOffset;

    /**
     * Used to inflate the Workspace from XML.
     *
     * @param context The application's context.
     * @param attrs The attributes set containing the Workspace's customization values.
     */
    public Search(Context context, AttributeSet attrs) {
        super(context, attrs);

        final float scale = context.getResources().getDisplayMetrics().density;
        mWidgetTopOffset = Math.round(WIDGET_TOP_OFFSET * scale);
        
        Interpolator interpolator = new AccelerateDecelerateInterpolator();

        mMorphAnimation = new ToParentOriginAnimation();
        // no need to apply transformation before the animation starts,
        // since the gadget is already in its normal place.
        mMorphAnimation.setFillBefore(false);
        // stay in the top position after the animation finishes
        mMorphAnimation.setFillAfter(true);
        mMorphAnimation.setInterpolator(interpolator);
        mMorphAnimation.setAnimationListener(new Animation.AnimationListener() {
            // The amount of time before the animation ends to show the search dialog.
            private static final long TIME_BEFORE_ANIMATION_END = 80;
            
            // The runnable which we'll pass to our handler to show the search dialog.
            private final Runnable mShowSearchDialogRunnable = new Runnable() {
                public void run() {
                    showSearchDialog();
                }
            };
            
            public void onAnimationEnd(Animation animation) { }
            public void onAnimationRepeat(Animation animation) { }
            public void onAnimationStart(Animation animation) {
                // Make the search dialog show up ideally *just* as the animation reaches
                // the top, to aid the illusion that the widget becomes the search dialog.
                // Otherwise, there is a short delay when the widget reaches the top before
                // the search dialog shows. We do this roughly 80ms before the animation ends.
                getHandler().postDelayed(
                        mShowSearchDialogRunnable,
                        Math.max(mMorphAnimation.getDuration() - TIME_BEFORE_ANIMATION_END, 0));
            }
        });

        mUnmorphAnimation = new FromParentOriginAnimation();
        // stay in the top position until the animation starts
        mUnmorphAnimation.setFillBefore(true);
        // no need to apply transformation after the animation finishes,
        // since the gadget is now back in its normal place.
        mUnmorphAnimation.setFillAfter(false);
        mUnmorphAnimation.setInterpolator(interpolator);
        mUnmorphAnimation.setAnimationListener(new Animation.AnimationListener(){
            public void onAnimationEnd(Animation animation) {
                clearAnimation();
            }
            public void onAnimationRepeat(Animation animation) { }
            public void onAnimationStart(Animation animation) { }
        });
        
        mVoiceSearchIntent = new Intent(android.speech.RecognizerIntent.ACTION_WEB_SEARCH);
        mVoiceSearchIntent.putExtra(android.speech.RecognizerIntent.EXTRA_LANGUAGE_MODEL,
                android.speech.RecognizerIntent.LANGUAGE_MODEL_WEB_SEARCH);
    }

    /**
     * Implements OnClickListener.
     */
    public void onClick(View v) {
        if (v == mVoiceButton) {
            startVoiceSearch();
        } else {
            mLauncher.onSearchRequested();
        }
    }

    private void startVoiceSearch() {
        try {
            getContext().startActivity(mVoiceSearchIntent);
        } catch (ActivityNotFoundException ex) {
            // Should not happen, since we check the availability of
            // voice search before showing the button. But just in case...
            Log.w(TAG, "Could not find voice search activity");
        }
    }

    /**
     * Sets the query text. The query field is not editable, instead we forward
     * the key events to the launcher, which keeps track of the text, 
     * calls setQuery() to show it, and gives it to the search dialog.
     */
    public void setQuery(String query) {
        mSearchText.setText(query, TextView.BufferType.NORMAL);
    }

    /**
     * Morph the search gadget to the search dialog.
     * See {@link Activity#startSearch()} for the arguments.
     */
    public void startSearch(String initialQuery, boolean selectInitialQuery, 
            Bundle appSearchData, boolean globalSearch) {
        mInitialQuery = initialQuery;
        mSelectInitialQuery = selectInitialQuery;
        mAppSearchData = appSearchData;
        mGlobalSearch = globalSearch;
        
        if (isAtTop()) {
            showSearchDialog();
        } else {
            // Call up the keyboard before we actually call the search dialog so that it
            // (hopefully) animates in at about the same time as the widget animation, and
            // so that it becomes available as soon as possible. Only do this if a hard
            // keyboard is not currently available.
            if (getContext().getResources().getConfiguration().hardKeyboardHidden ==
                    Configuration.HARDKEYBOARDHIDDEN_YES) {
                InputMethodManager inputManager = (InputMethodManager)
                        getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
                inputManager.showSoftInputUnchecked(0, null);
            }
            
            // Start the animation, unless it has already started.
            if (getAnimation() != mMorphAnimation) {
                mMorphAnimation.setDuration(getAnimationDuration());
                startAnimation(mMorphAnimation);
            }
        }
    }

    /**
     * Shows the system search dialog immediately, without any animation.
     */
    private void showSearchDialog() {
        mLauncher.showSearchDialog(
                mInitialQuery, mSelectInitialQuery, mAppSearchData, mGlobalSearch);
    }

    /**
     * Restore the search gadget to its normal position.
     * 
     * @param animate Whether to animate the movement of the gadget.
     */
    public void stopSearch(boolean animate) {
        setQuery("");
        
        // Only restore if we are not already restored.
        if (getAnimation() == mMorphAnimation) {
            if (animate && !isAtTop()) {
                mUnmorphAnimation.setDuration(getAnimationDuration());
                startAnimation(mUnmorphAnimation);
            } else {
                clearAnimation();
            }
        }
    }

    private boolean isAtTop() {
        return getWidgetTop() == 0;
    }

    private int getAnimationDuration() {
        return (int) (getWidgetTop() / ANIMATION_VELOCITY);
    }

    /**
     * Modify clearAnimation() to invalidate the parent. This works around
     * an issue where the region where the end of the animation placed the view
     * was not redrawn after clearing the animation.
     */
    @Override
    public void clearAnimation() {
        Animation animation = getAnimation();
        if (animation != null) {
            super.clearAnimation();
            if (animation.hasEnded() 
                    && animation.getFillAfter()
                    && animation.willChangeBounds()) {
                ((View) getParent()).invalidate();
            } else {
                invalidate();
            }
        }
    }
    
    public boolean onKey(View v, int keyCode, KeyEvent event) {
        if (!event.isSystem() && 
                (keyCode != KeyEvent.KEYCODE_DPAD_UP) &&
                (keyCode != KeyEvent.KEYCODE_DPAD_DOWN) &&
                (keyCode != KeyEvent.KEYCODE_DPAD_LEFT) &&
                (keyCode != KeyEvent.KEYCODE_DPAD_RIGHT) &&
                (keyCode != KeyEvent.KEYCODE_DPAD_CENTER)) {
            // Forward key events to Launcher, which will forward text 
            // to search dialog
            switch (event.getAction()) {
                case KeyEvent.ACTION_DOWN:
                    return mLauncher.onKeyDown(keyCode, event);
                case KeyEvent.ACTION_MULTIPLE:
                    return mLauncher.onKeyMultiple(keyCode, event.getRepeatCount(), event);
                case KeyEvent.ACTION_UP:
                    return mLauncher.onKeyUp(keyCode, event);
            }
        }
        return false;
    }

    /**
     * Implements OnLongClickListener to pass long clicks on child views 
     * to the widget. This makes it possible to pick up the widget by long
     * clicking on the text field or a button.
     */
    public boolean onLongClick(View v) {
        return performLongClick();
    }

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();

        mSearchText = (TextView) findViewById(R.id.search_src_text);
        mVoiceButton = (ImageButton) findViewById(R.id.search_voice_btn);
        
        mSearchText.setOnKeyListener(this);

        mSearchText.setOnClickListener(this);
        mVoiceButton.setOnClickListener(this);
        setOnClickListener(this);        

        mSearchText.setOnLongClickListener(this);
        mVoiceButton.setOnLongClickListener(this);

        // Set the placeholder text to be the Google logo within the search widget.
        Drawable googlePlaceholder =
                getContext().getResources().getDrawable(R.drawable.placeholder_google);
        mSearchText.setCompoundDrawablesWithIntrinsicBounds(googlePlaceholder, null, null, null);

        configureVoiceSearchButton();
    }
    
    @Override
    public void onDetachedFromWindow() {
        super.onDetachedFromWindow();
    }

    /**
     * If appropriate & available, configure voice search
     * 
     * Note:  Because the home screen search widget is always web search, we only check for
     * getVoiceSearchLaunchWebSearch() modes.  We don't support the alternate form of app-specific
     * voice search.
     */
    private void configureVoiceSearchButton() {
        // Enable the voice search button if there is an activity that can handle it
        PackageManager pm = getContext().getPackageManager();
        ResolveInfo ri = pm.resolveActivity(mVoiceSearchIntent,
                PackageManager.MATCH_DEFAULT_ONLY);
        boolean voiceSearchVisible = ri != null;

        // finally, set visible state of voice search button, as appropriate
        mVoiceButton.setVisibility(voiceSearchVisible ? View.VISIBLE : View.GONE);
    }
    
    /**
     * Sets the {@link Launcher} that this gadget will call on to display the search dialog. 
     */
    public void setLauncher(Launcher launcher) {
        mLauncher = launcher;
    }
        
    /** 
     * Moves the view to the top left corner of its parent.
     */
    private class ToParentOriginAnimation extends Animation {
        @Override
        protected void applyTransformation(float interpolatedTime, Transformation t) {
            float dx = -getLeft() * interpolatedTime;
            float dy = -getWidgetTop() * interpolatedTime;
            t.getMatrix().setTranslate(dx, dy);
        }
    }

    /** 
     * Moves the view from the top left corner of its parent.
     */
    private class FromParentOriginAnimation extends Animation {
        @Override
        protected void applyTransformation(float interpolatedTime, Transformation t) {
            float dx = -getLeft() * (1.0f - interpolatedTime);
            float dy = -getWidgetTop() * (1.0f - interpolatedTime);
            t.getMatrix().setTranslate(dx, dy);
        }
    }
    
    /**
     * The widget is centered vertically within it's 4x1 slot. This is
     * accomplished by nesting the actual widget inside another view. For
     * animation purposes, we care about the top of the actual widget rather
     * than it's container. This method return the top of the actual widget.
     */
    private int getWidgetTop() {
        return getTop() + getChildAt(0).getTop() + mWidgetTopOffset;
    }

}
