/*
 * Copyright (C) 2012 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.internal.app;

import com.android.internal.R;

import android.app.Activity;
import android.app.Dialog;
import android.app.DialogFragment;
import android.app.MediaRouteActionProvider;
import android.app.MediaRouteButton;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.hardware.display.DisplayManager;
import android.media.MediaRouter;
import android.media.MediaRouter.RouteCategory;
import android.media.MediaRouter.RouteGroup;
import android.media.MediaRouter.RouteInfo;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.BaseAdapter;
import android.widget.CheckBox;
import android.widget.Checkable;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.SeekBar;
import android.widget.TextView;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

/**
 * This class implements the route chooser dialog for {@link MediaRouter}.
 *
 * @see MediaRouteButton
 * @see MediaRouteActionProvider
 */
public class MediaRouteChooserDialogFragment extends DialogFragment {
    private static final String TAG = "MediaRouteChooserDialogFragment";
    public static final String FRAGMENT_TAG = "android:MediaRouteChooserDialogFragment";

    private static final int[] ITEM_LAYOUTS = new int[] {
        R.layout.media_route_list_item_top_header,
        R.layout.media_route_list_item_section_header,
        R.layout.media_route_list_item,
        R.layout.media_route_list_item_checkable,
        R.layout.media_route_list_item_collapse_group
    };

    MediaRouter mRouter;
    DisplayManager mDisplayService;
    private int mRouteTypes;

    private LayoutInflater mInflater;
    private LauncherListener mLauncherListener;
    private View.OnClickListener mExtendedSettingsListener;
    private RouteAdapter mAdapter;
    private ListView mListView;
    private SeekBar mVolumeSlider;
    private ImageView mVolumeIcon;

    final RouteComparator mComparator = new RouteComparator();
    final MediaRouterCallback mCallback = new MediaRouterCallback();
    private boolean mIgnoreSliderVolumeChanges;
    private boolean mIgnoreCallbackVolumeChanges;

    public MediaRouteChooserDialogFragment() {
        setStyle(STYLE_NO_TITLE, R.style.Theme_DeviceDefault_Dialog);
    }

    public void setLauncherListener(LauncherListener listener) {
        mLauncherListener = listener;
    }

    @Override
    public void onAttach(Activity activity) {
        super.onAttach(activity);
        mRouter = (MediaRouter) activity.getSystemService(Context.MEDIA_ROUTER_SERVICE);
        mDisplayService = (DisplayManager) activity.getSystemService(Context.DISPLAY_SERVICE);
    }

    @Override
    public void onDetach() {
        super.onDetach();
        if (mLauncherListener != null) {
            mLauncherListener.onDetached(this);
        }
        if (mAdapter != null) {
            mAdapter = null;
        }
        mInflater = null;
        mRouter.removeCallback(mCallback);
        mRouter = null;
    }

    public void setExtendedSettingsClickListener(View.OnClickListener listener) {
        mExtendedSettingsListener = listener;
    }

    public void setRouteTypes(int types) {
        mRouteTypes = types;
        if ((mRouteTypes & MediaRouter.ROUTE_TYPE_LIVE_VIDEO) != 0 && mDisplayService == null) {
            final Context activity = getActivity();
            if (activity != null) {
                mDisplayService = (DisplayManager) activity.getSystemService(
                        Context.DISPLAY_SERVICE);
            }
        } else {
            mDisplayService = null;
        }
    }

    void updateVolume() {
        if (mRouter == null) return;

        final RouteInfo selectedRoute = mRouter.getSelectedRoute(mRouteTypes);
        mVolumeIcon.setImageResource(selectedRoute == null ||
                selectedRoute.getPlaybackType() == RouteInfo.PLAYBACK_TYPE_LOCAL ?
                R.drawable.ic_audio_vol : R.drawable.ic_media_route_on_holo_dark);

        mIgnoreSliderVolumeChanges = true;

        if (selectedRoute == null ||
                selectedRoute.getVolumeHandling() == RouteInfo.PLAYBACK_VOLUME_FIXED) {
            // Disable the slider and show it at max volume.
            mVolumeSlider.setMax(1);
            mVolumeSlider.setProgress(1);
            mVolumeSlider.setEnabled(false);
        } else {
            mVolumeSlider.setEnabled(true);
            mVolumeSlider.setMax(selectedRoute.getVolumeMax());
            mVolumeSlider.setProgress(selectedRoute.getVolume());
        }

        mIgnoreSliderVolumeChanges = false;
    }

    void changeVolume(int newValue) {
        if (mIgnoreSliderVolumeChanges) return;

        final RouteInfo selectedRoute = mRouter.getSelectedRoute(mRouteTypes);
        if (selectedRoute != null &&
                selectedRoute.getVolumeHandling() == RouteInfo.PLAYBACK_VOLUME_VARIABLE) {
            final int maxVolume = selectedRoute.getVolumeMax();
            newValue = Math.max(0, Math.min(newValue, maxVolume));
            selectedRoute.requestSetVolume(newValue);
        }
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
            Bundle savedInstanceState) {
        mInflater = inflater;
        final View layout = inflater.inflate(R.layout.media_route_chooser_layout, container, false);

        mVolumeIcon = (ImageView) layout.findViewById(R.id.volume_icon);
        mVolumeSlider = (SeekBar) layout.findViewById(R.id.volume_slider);
        updateVolume();
        mVolumeSlider.setOnSeekBarChangeListener(new VolumeSliderChangeListener());

        if (mExtendedSettingsListener != null) {
            final View extendedSettingsButton = layout.findViewById(R.id.extended_settings);
            extendedSettingsButton.setVisibility(View.VISIBLE);
            extendedSettingsButton.setOnClickListener(mExtendedSettingsListener);
        }

        final ListView list = (ListView) layout.findViewById(R.id.list);
        list.setItemsCanFocus(true);
        list.setAdapter(mAdapter = new RouteAdapter());
        list.setOnItemClickListener(mAdapter);

        mListView = list;
        mRouter.addCallback(mRouteTypes, mCallback);

        mAdapter.scrollToSelectedItem();

        return layout;
    }

    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {
        return new RouteChooserDialog(getActivity(), getTheme());
    }

    @Override
    public void onResume() {
        super.onResume();
        if (mDisplayService != null) {
            mDisplayService.scanWifiDisplays();
        }
    }

    private static class ViewHolder {
        public TextView text1;
        public TextView text2;
        public ImageView icon;
        public ImageButton expandGroupButton;
        public RouteAdapter.ExpandGroupListener expandGroupListener;
        public int position;
        public CheckBox check;
    }

    private class RouteAdapter extends BaseAdapter implements ListView.OnItemClickListener {
        private static final int VIEW_TOP_HEADER = 0;
        private static final int VIEW_SECTION_HEADER = 1;
        private static final int VIEW_ROUTE = 2;
        private static final int VIEW_GROUPING_ROUTE = 3;
        private static final int VIEW_GROUPING_DONE = 4;

        private int mSelectedItemPosition = -1;
        private final ArrayList<Object> mItems = new ArrayList<Object>();

        private RouteCategory mCategoryEditingGroups;
        private RouteGroup mEditingGroup;

        // Temporary lists for manipulation
        private final ArrayList<RouteInfo> mCatRouteList = new ArrayList<RouteInfo>();
        private final ArrayList<RouteInfo> mSortRouteList = new ArrayList<RouteInfo>();

        private boolean mIgnoreUpdates;

        RouteAdapter() {
            update();
        }

        void update() {
            /*
             * This is kind of wacky, but our data sets are going to be
             * fairly small on average. Ideally we should be able to do some of this stuff
             * in-place instead.
             *
             * Basic idea: each entry in mItems represents an item in the list for quick access.
             * Entries can be a RouteCategory (section header), a RouteInfo with a category of
             * mCategoryEditingGroups (a flattened RouteInfo pulled out of its group, allowing
             * the user to change the group),
             */
            if (mIgnoreUpdates) return;

            mItems.clear();

            final RouteInfo selectedRoute = mRouter.getSelectedRoute(mRouteTypes);
            mSelectedItemPosition = -1;

            List<RouteInfo> routes;
            final int catCount = mRouter.getCategoryCount();
            for (int i = 0; i < catCount; i++) {
                final RouteCategory cat = mRouter.getCategoryAt(i);
                routes = cat.getRoutes(mCatRouteList);

                if (!cat.isSystem()) {
                    mItems.add(cat);
                }

                if (cat == mCategoryEditingGroups) {
                    addGroupEditingCategoryRoutes(routes);
                } else {
                    addSelectableRoutes(selectedRoute, routes);
                }

                routes.clear();
            }

            notifyDataSetChanged();
            if (mListView != null && mSelectedItemPosition >= 0) {
                mListView.setItemChecked(mSelectedItemPosition, true);
            }
        }

        void scrollToEditingGroup() {
            if (mCategoryEditingGroups == null || mListView == null) return;

            int pos = 0;
            int bound = 0;
            final int itemCount = mItems.size();
            for (int i = 0; i < itemCount; i++) {
                final Object item = mItems.get(i);
                if (item != null && item == mCategoryEditingGroups) {
                    bound = i;
                }
                if (item == null) {
                    pos = i;
                    break; // this is always below the category header; we can stop here.
                }
            }

            mListView.smoothScrollToPosition(pos, bound);
        }

        void scrollToSelectedItem() {
            if (mListView == null || mSelectedItemPosition < 0) return;

            mListView.smoothScrollToPosition(mSelectedItemPosition);
        }

        void addSelectableRoutes(RouteInfo selectedRoute, List<RouteInfo> from) {
            final int routeCount = from.size();
            for (int j = 0; j < routeCount; j++) {
                final RouteInfo info = from.get(j);
                if (info == selectedRoute) {
                    mSelectedItemPosition = mItems.size();
                }
                mItems.add(info);
            }
        }

        void addGroupEditingCategoryRoutes(List<RouteInfo> from) {
            // Unpack groups and flatten for presentation
            // mSortRouteList will always be empty here.
            final int topCount = from.size();
            for (int i = 0; i < topCount; i++) {
                final RouteInfo route = from.get(i);
                final RouteGroup group = route.getGroup();
                if (group == route) {
                    // This is a group, unpack it.
                    final int groupCount = group.getRouteCount();
                    for (int j = 0; j < groupCount; j++) {
                        final RouteInfo innerRoute = group.getRouteAt(j);
                        mSortRouteList.add(innerRoute);
                    }
                } else {
                    mSortRouteList.add(route);
                }
            }
            // Sort by name. This will keep the route positions relatively stable even though they
            // will be repeatedly added and removed.
            Collections.sort(mSortRouteList, mComparator);

            mItems.addAll(mSortRouteList);
            mSortRouteList.clear();

            mItems.add(null); // Sentinel reserving space for the "done" button.
        }

        @Override
        public int getCount() {
            return mItems.size();
        }

        @Override
        public int getViewTypeCount() {
            return 5;
        }

        @Override
        public int getItemViewType(int position) {
            final Object item = getItem(position);
            if (item instanceof RouteCategory) {
                return position == 0 ? VIEW_TOP_HEADER : VIEW_SECTION_HEADER;
            } else if (item == null) {
                return VIEW_GROUPING_DONE;
            } else {
                final RouteInfo info = (RouteInfo) item;
                if (info.getCategory() == mCategoryEditingGroups) {
                    return VIEW_GROUPING_ROUTE;
                }
                return VIEW_ROUTE;
            }
        }

        @Override
        public boolean areAllItemsEnabled() {
            return false;
        }

        @Override
        public boolean isEnabled(int position) {
            switch (getItemViewType(position)) {
                case VIEW_ROUTE:
                    return ((RouteInfo) mItems.get(position)).isEnabled();
                case VIEW_GROUPING_ROUTE:
                case VIEW_GROUPING_DONE:
                    return true;
                default:
                    return false;
            }
        }

        @Override
        public Object getItem(int position) {
            return mItems.get(position);
        }

        @Override
        public long getItemId(int position) {
            return position;
        }

        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            final int viewType = getItemViewType(position);

            ViewHolder holder;
            if (convertView == null) {
                convertView = mInflater.inflate(ITEM_LAYOUTS[viewType], parent, false);
                holder = new ViewHolder();
                holder.position = position;
                holder.text1 = (TextView) convertView.findViewById(R.id.text1);
                holder.text2 = (TextView) convertView.findViewById(R.id.text2);
                holder.icon = (ImageView) convertView.findViewById(R.id.icon);
                holder.check = (CheckBox) convertView.findViewById(R.id.check);
                holder.expandGroupButton = (ImageButton) convertView.findViewById(
                        R.id.expand_button);
                if (holder.expandGroupButton != null) {
                    holder.expandGroupListener = new ExpandGroupListener();
                    holder.expandGroupButton.setOnClickListener(holder.expandGroupListener);
                }

                final View fview = convertView;
                final ListView list = (ListView) parent;
                final ViewHolder fholder = holder;
                convertView.setOnClickListener(new View.OnClickListener() {
                    @Override public void onClick(View v) {
                        list.performItemClick(fview, fholder.position, 0);
                    }
                });
                convertView.setTag(holder);
            } else {
                holder = (ViewHolder) convertView.getTag();
                holder.position = position;
            }

            switch (viewType) {
                case VIEW_ROUTE:
                case VIEW_GROUPING_ROUTE:
                    bindItemView(position, holder);
                    break;
                case VIEW_SECTION_HEADER:
                case VIEW_TOP_HEADER:
                    bindHeaderView(position, holder);
                    break;
            }

            convertView.setActivated(position == mSelectedItemPosition);
            convertView.setEnabled(isEnabled(position));

            return convertView;
        }

        void bindItemView(int position, ViewHolder holder) {
            RouteInfo info = (RouteInfo) mItems.get(position);
            holder.text1.setText(info.getName(getActivity()));
            final CharSequence status = info.getStatus();
            if (TextUtils.isEmpty(status)) {
                holder.text2.setVisibility(View.GONE);
            } else {
                holder.text2.setVisibility(View.VISIBLE);
                holder.text2.setText(status);
            }
            Drawable icon = info.getIconDrawable();
            if (icon != null) {
                // Make sure we have a fresh drawable where it doesn't matter if we mutate it
                icon = icon.getConstantState().newDrawable(getResources());
            }
            holder.icon.setImageDrawable(icon);
            holder.icon.setVisibility(icon != null ? View.VISIBLE : View.GONE);

            RouteCategory cat = info.getCategory();
            boolean canGroup = false;
            if (cat == mCategoryEditingGroups) {
                RouteGroup group = info.getGroup();
                holder.check.setEnabled(group.getRouteCount() > 1);
                holder.check.setChecked(group == mEditingGroup);
            } else {
                if (cat.isGroupable()) {
                    final RouteGroup group = (RouteGroup) info;
                    canGroup = group.getRouteCount() > 1 ||
                            getItemViewType(position - 1) == VIEW_ROUTE ||
                            (position < getCount() - 1 &&
                                    getItemViewType(position + 1) == VIEW_ROUTE);
                }
            }

            if (holder.expandGroupButton != null) {
                holder.expandGroupButton.setVisibility(canGroup ? View.VISIBLE : View.GONE);
                holder.expandGroupListener.position = position;
            }
        }

        void bindHeaderView(int position, ViewHolder holder) {
            RouteCategory cat = (RouteCategory) mItems.get(position);
            holder.text1.setText(cat.getName(getActivity()));
        }

        @Override
        public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
            final int type = getItemViewType(position);
            if (type == VIEW_SECTION_HEADER || type == VIEW_TOP_HEADER) {
                return;
            } else if (type == VIEW_GROUPING_DONE) {
                finishGrouping();
                return;
            } else {
                final Object item = getItem(position);
                if (!(item instanceof RouteInfo)) {
                    // Oops. Stale event running around? Skip it.
                    return;
                }

                final RouteInfo route = (RouteInfo) item;
                if (type == VIEW_ROUTE) {
                    mRouter.selectRouteInt(mRouteTypes, route);
                    dismiss();
                } else if (type == VIEW_GROUPING_ROUTE) {
                    final Checkable c = (Checkable) view;
                    final boolean wasChecked = c.isChecked();

                    mIgnoreUpdates = true;
                    RouteGroup oldGroup = route.getGroup();
                    if (!wasChecked && oldGroup != mEditingGroup) {
                        // Assumption: in a groupable category oldGroup will never be null.
                        if (mRouter.getSelectedRoute(mRouteTypes) == oldGroup) {
                            // Old group was selected but is now empty. Select the group
                            // we're manipulating since that's where the last route went.
                            mRouter.selectRouteInt(mRouteTypes, mEditingGroup);
                        }
                        oldGroup.removeRoute(route);
                        mEditingGroup.addRoute(route);
                        c.setChecked(true);
                    } else if (wasChecked && mEditingGroup.getRouteCount() > 1) {
                        mEditingGroup.removeRoute(route);

                        // In a groupable category this will add
                        // the route into its own new group.
                        mRouter.addRouteInt(route);
                    }
                    mIgnoreUpdates = false;
                    update();
                }
            }
        }

        boolean isGrouping() {
            return mCategoryEditingGroups != null;
        }

        void finishGrouping() {
            mCategoryEditingGroups = null;
            mEditingGroup = null;
            getDialog().setCanceledOnTouchOutside(true);
            update();
            scrollToSelectedItem();
        }

        class ExpandGroupListener implements View.OnClickListener {
            int position;

            @Override
            public void onClick(View v) {
                // Assumption: this is only available for the user to click if we're presenting
                // a groupable category, where every top-level route in the category is a group.
                final RouteGroup group = (RouteGroup) getItem(position);
                mEditingGroup = group;
                mCategoryEditingGroups = group.getCategory();
                getDialog().setCanceledOnTouchOutside(false);
                mRouter.selectRouteInt(mRouteTypes, mEditingGroup);
                update();
                scrollToEditingGroup();
            }
        }
    }

    class MediaRouterCallback extends MediaRouter.Callback {
        @Override
        public void onRouteSelected(MediaRouter router, int type, RouteInfo info) {
            mAdapter.update();
            updateVolume();
        }

        @Override
        public void onRouteUnselected(MediaRouter router, int type, RouteInfo info) {
            mAdapter.update();
        }

        @Override
        public void onRouteAdded(MediaRouter router, RouteInfo info) {
            mAdapter.update();
        }

        @Override
        public void onRouteRemoved(MediaRouter router, RouteInfo info) {
            if (info == mAdapter.mEditingGroup) {
                mAdapter.finishGrouping();
            }
            mAdapter.update();
        }

        @Override
        public void onRouteChanged(MediaRouter router, RouteInfo info) {
            mAdapter.notifyDataSetChanged();
        }

        @Override
        public void onRouteGrouped(MediaRouter router, RouteInfo info,
                RouteGroup group, int index) {
            mAdapter.update();
        }

        @Override
        public void onRouteUngrouped(MediaRouter router, RouteInfo info, RouteGroup group) {
            mAdapter.update();
        }

        @Override
        public void onRouteVolumeChanged(MediaRouter router, RouteInfo info) {
            if (!mIgnoreCallbackVolumeChanges) {
                updateVolume();
            }
        }
    }

    class RouteComparator implements Comparator<RouteInfo> {
        @Override
        public int compare(RouteInfo lhs, RouteInfo rhs) {
            return lhs.getName(getActivity()).toString()
                    .compareTo(rhs.getName(getActivity()).toString());
        }
    }

    class RouteChooserDialog extends Dialog {
        public RouteChooserDialog(Context context, int theme) {
            super(context, theme);
        }

        @Override
        public void onBackPressed() {
            if (mAdapter != null && mAdapter.isGrouping()) {
                mAdapter.finishGrouping();
            } else {
                super.onBackPressed();
            }
        }
        
        public boolean onKeyDown(int keyCode, KeyEvent event) {
            if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN && mVolumeSlider.isEnabled()) {
                final RouteInfo selectedRoute = mRouter.getSelectedRoute(mRouteTypes);
                if (selectedRoute != null) {
                    selectedRoute.requestUpdateVolume(-1);
                    return true;
                }
            } else if (keyCode == KeyEvent.KEYCODE_VOLUME_UP && mVolumeSlider.isEnabled()) {
                final RouteInfo selectedRoute = mRouter.getSelectedRoute(mRouteTypes);
                if (selectedRoute != null) {
                    mRouter.getSelectedRoute(mRouteTypes).requestUpdateVolume(1);
                    return true;
                }
            }
            return super.onKeyDown(keyCode, event);
        }

        public boolean onKeyUp(int keyCode, KeyEvent event) {
            if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN && mVolumeSlider.isEnabled()) {
                return true;
            } else if (keyCode == KeyEvent.KEYCODE_VOLUME_UP && mVolumeSlider.isEnabled()) {
                return true;
            } else {
                return super.onKeyUp(keyCode, event);
            }
        }
    }

    /**
     * Implemented by the MediaRouteButton that launched this dialog
     */
    public interface LauncherListener {
        public void onDetached(MediaRouteChooserDialogFragment detachedFragment);
    }

    class VolumeSliderChangeListener implements SeekBar.OnSeekBarChangeListener {

        @Override
        public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
            changeVolume(progress);
        }

        @Override
        public void onStartTrackingTouch(SeekBar seekBar) {
            mIgnoreCallbackVolumeChanges = true;
        }

        @Override
        public void onStopTrackingTouch(SeekBar seekBar) {
            mIgnoreCallbackVolumeChanges = false;
            updateVolume();
        }

    }
}
