/*
 * Copyright (C) 2015 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.tv.ui;

import android.content.Context;
import android.content.res.Resources;
import android.hardware.hdmi.HdmiDeviceInfo;
import android.media.tv.TvInputInfo;
import android.media.tv.TvInputManager;
import android.media.tv.TvInputManager.TvInputCallback;
import android.support.annotation.NonNull;
import android.support.v17.leanback.widget.VerticalGridView;
import android.support.v7.widget.RecyclerView;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.Log;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;

import com.android.tv.R;
import com.android.tv.ApplicationSingletons;
import com.android.tv.TvApplication;
import com.android.tv.analytics.DurationTimer;
import com.android.tv.analytics.Tracker;
import com.android.tv.data.Channel;
import com.android.tv.util.TvInputManagerHelper;
import com.android.tv.util.Utils;

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

public class SelectInputView extends VerticalGridView implements
        TvTransitionManager.TransitionLayout {
    private static final String TAG = "SelectInputView";
    private static final boolean DEBUG = false;
    public static final String SCREEN_NAME = "Input selection";
    private static final int TUNER_INPUT_POSITION = 0;

    private final TvInputManagerHelper mTvInputManagerHelper;
    private final List<TvInputInfo> mInputList = new ArrayList<>();
    private final InputsComparator mComparator = new InputsComparator();
    private final Tracker mTracker;
    private final DurationTimer mViewDurationTimer = new DurationTimer();
    private final TvInputCallback mTvInputCallback = new TvInputCallback() {
        @Override
        public void onInputAdded(String inputId) {
            buildInputListAndNotify();
            updateSelectedPositionIfNeeded();
        }

        @Override
        public void onInputRemoved(String inputId) {
            buildInputListAndNotify();
            updateSelectedPositionIfNeeded();
        }

        @Override
        public void onInputUpdated(String inputId) {
            buildInputListAndNotify();
            updateSelectedPositionIfNeeded();
        }

        @Override
        public void onInputStateChanged(String inputId, int state) {
            buildInputListAndNotify();
            updateSelectedPositionIfNeeded();
        }

        private void updateSelectedPositionIfNeeded() {
            if (!isFocusable() || mSelectedInput == null) {
                return;
            }
            if (!isInputEnabled(mSelectedInput)) {
                setSelectedPosition(TUNER_INPUT_POSITION);
                return;
            }
            if (getInputPosition(mSelectedInput.getId()) != getSelectedPosition()) {
                setSelectedPosition(getInputPosition(mSelectedInput.getId()));
            }
        }
    };

    private Channel mCurrentChannel;
    private OnInputSelectedCallback mCallback;

    private final Runnable mHideRunnable = new Runnable() {
        @Override
        public void run() {
            if (mSelectedInput == null) {
                return;
            }
            // TODO: pass english label to tracker http://b/22355024
            final String label = mSelectedInput.loadLabel(getContext()).toString();
            mTracker.sendInputSelected(label);
            if (mCallback != null) {
                if (mSelectedInput.isPassthroughInput()) {
                    mCallback.onPassthroughInputSelected(mSelectedInput);
                } else {
                    mCallback.onTunerInputSelected();
                }
            }
        }
    };

    private final int mInputItemHeight;
    private final long mShowDurationMillis;
    private final long mRippleAnimDurationMillis;
    private final int mTextColorPrimary;
    private final int mTextColorSecondary;
    private final int mTextColorDisabled;
    private final View mItemViewForMeasure;

    private boolean mResetTransitionAlpha;
    private TvInputInfo mSelectedInput;
    private int mMaxItemWidth;

    public SelectInputView(Context context) {
        this(context, null, 0);
    }

    public SelectInputView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public SelectInputView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        setAdapter(new InputListAdapter());

        ApplicationSingletons appSingletons = TvApplication.getSingletons(context);
        mTracker = appSingletons.getTracker();
        mTvInputManagerHelper = appSingletons.getTvInputManagerHelper();

        Resources resources = context.getResources();
        mInputItemHeight = resources.getDimensionPixelSize(R.dimen.input_banner_item_height);
        mShowDurationMillis = resources.getInteger(R.integer.select_input_show_duration);
        mRippleAnimDurationMillis = resources.getInteger(
                R.integer.select_input_ripple_anim_duration);
        mTextColorPrimary = Utils.getColor(resources, R.color.select_input_text_color_primary);
        mTextColorSecondary = Utils.getColor(resources, R.color.select_input_text_color_secondary);
        mTextColorDisabled = Utils.getColor(resources, R.color.select_input_text_color_disabled);

        mItemViewForMeasure = LayoutInflater.from(context).inflate(
                R.layout.select_input_item, this, false);
        buildInputListAndNotify();
    }

    @Override
    public boolean onKeyUp(int keyCode, KeyEvent event) {
        if (DEBUG) Log.d(TAG, "onKeyUp(keyCode=" + keyCode + ", event=" + event + ")");
        scheduleHide();

        if (keyCode == KeyEvent.KEYCODE_TV_INPUT) {
            // Go down to the next available input.
            int currentPosition = mInputList.indexOf(mSelectedInput);
            int nextPosition = currentPosition;
            while (true) {
                nextPosition = (nextPosition + 1) % mInputList.size();
                if (isInputEnabled(mInputList.get(nextPosition))) {
                    break;
                }
                if (nextPosition == currentPosition) {
                    nextPosition = 0;
                    break;
                }
            }
            setSelectedPosition(nextPosition);
            return true;
        }
        return super.onKeyUp(keyCode, event);
    }

    @Override
    public void onEnterAction(boolean fromEmptyScene) {
        mTracker.sendShowInputSelection();
        mTracker.sendScreenView(SCREEN_NAME);
        mViewDurationTimer.start();
        scheduleHide();

        mResetTransitionAlpha = fromEmptyScene;
        buildInputListAndNotify();
        mTvInputManagerHelper.addCallback(mTvInputCallback);
        String currentInputId = mCurrentChannel != null && mCurrentChannel.isPassthrough() ?
                mCurrentChannel.getInputId() : null;
        if (currentInputId != null
                && !isInputEnabled(mTvInputManagerHelper.getTvInputInfo(currentInputId))) {
            // If current input is disabled, the tuner input will be focused.
            setSelectedPosition(TUNER_INPUT_POSITION);
        } else {
            setSelectedPosition(getInputPosition(currentInputId));
        }
        setFocusable(true);
        requestFocus();
    }

    private int getInputPosition(String inputId) {
        if (inputId != null) {
            for (int i = 0; i < mInputList.size(); ++i) {
                if (TextUtils.equals(mInputList.get(i).getId(), inputId)) {
                    return i;
                }
            }
        }
        return TUNER_INPUT_POSITION;
    }

    @Override
    public void onExitAction() {
        mTracker.sendHideInputSelection(mViewDurationTimer.reset());
        mTvInputManagerHelper.removeCallback(mTvInputCallback);
        removeCallbacks(mHideRunnable);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int height = mInputItemHeight * mInputList.size();
        super.onMeasure(MeasureSpec.makeMeasureSpec(mMaxItemWidth, MeasureSpec.EXACTLY),
                MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY));
    }

    private void scheduleHide() {
        removeCallbacks(mHideRunnable);
        postDelayed(mHideRunnable, mShowDurationMillis);
    }

    private void buildInputListAndNotify() {
        mInputList.clear();
        Map<String, TvInputInfo> inputMap = new HashMap<>();
        boolean foundTuner = false;
        for (TvInputInfo input : mTvInputManagerHelper.getTvInputInfos(false, false)) {
            if (input.isPassthroughInput()) {
                if (!input.isHidden(getContext())) {
                    mInputList.add(input);
                    inputMap.put(input.getId(), input);
                }
            } else if (!foundTuner) {
                foundTuner = true;
                mInputList.add(input);
            }
        }
        // Do not show HDMI ports if a CEC device is directly connected to the port.
        for (TvInputInfo input : inputMap.values()) {
            if (input.getParentId() != null && !input.isConnectedToHdmiSwitch()) {
                mInputList.remove(inputMap.get(input.getParentId()));
            }
        }
        Collections.sort(mInputList, mComparator);

        // Update the max item width.
        mMaxItemWidth = 0;
        for (TvInputInfo input : mInputList) {
            setItemViewText(mItemViewForMeasure, input);
            mItemViewForMeasure.measure(0, 0);
            int width = mItemViewForMeasure.getMeasuredWidth();
            if (width > mMaxItemWidth) {
                mMaxItemWidth = width;
            }
        }

        getAdapter().notifyDataSetChanged();
    }

    private void setItemViewText(View v, TvInputInfo input) {
        TextView inputLabelView = (TextView) v.findViewById(R.id.input_label);
        TextView secondaryInputLabelView = (TextView) v.findViewById(R.id.secondary_input_label);
        CharSequence customLabel = input.loadCustomLabel(getContext());
        CharSequence label = input.loadLabel(getContext());
        if (TextUtils.isEmpty(customLabel) || customLabel.equals(label)) {
            inputLabelView.setText(label);
            secondaryInputLabelView.setVisibility(View.GONE);
        } else {
            inputLabelView.setText(customLabel);
            secondaryInputLabelView.setText(label);
            secondaryInputLabelView.setVisibility(View.VISIBLE);
        }
    }

    private boolean isInputEnabled(TvInputInfo input) {
        return mTvInputManagerHelper.getInputState(input)
                != TvInputManager.INPUT_STATE_DISCONNECTED;
    }

    /**
     * Sets a callback which receives the notifications of input selection.
     */
    public void setOnInputSelectedCallback(OnInputSelectedCallback callback) {
        mCallback = callback;
    }

    /**
     * Sets the current channel. The initial selection will be the input which contains the
     * {@code channel}.
     */
    public void setCurrentChannel(Channel channel) {
        mCurrentChannel = channel;
    }

    class InputListAdapter extends RecyclerView.Adapter<InputListAdapter.ViewHolder> {
        @Override
        public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
            View v = LayoutInflater.from(parent.getContext()).inflate(
                    R.layout.select_input_item, parent, false);
            return new ViewHolder(v);
        }

        @Override
        public void onBindViewHolder(ViewHolder holder, final int position) {
            TvInputInfo input = mInputList.get(position);
            if (input.isPassthroughInput()) {
                if (isInputEnabled(input)) {
                    holder.itemView.setFocusable(true);
                    holder.inputLabelView.setTextColor(mTextColorPrimary);
                    holder.secondaryInputLabelView.setTextColor(mTextColorSecondary);
                } else {
                    holder.itemView.setFocusable(false);
                    holder.inputLabelView.setTextColor(mTextColorDisabled);
                    holder.secondaryInputLabelView.setTextColor(mTextColorDisabled);
                }
                setItemViewText(holder.itemView, input);
            } else {
                holder.itemView.setFocusable(true);
                holder.inputLabelView.setTextColor(mTextColorPrimary);
                holder.inputLabelView.setText(R.string.input_long_label_for_tuner);
                holder.secondaryInputLabelView.setVisibility(View.GONE);
            }

            holder.itemView.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    mSelectedInput = mInputList.get(position);
                    // The user made a selection. Hide this view after the ripple animation. But
                    // first, disable focus to avoid any further focus change during the animation.
                    setFocusable(false);
                    removeCallbacks(mHideRunnable);
                    postDelayed(mHideRunnable, mRippleAnimDurationMillis);
                }
            });
            holder.itemView.setOnFocusChangeListener(new View.OnFocusChangeListener() {
                @Override
                public void onFocusChange(View view, boolean hasFocus) {
                    if (hasFocus) {
                        mSelectedInput = mInputList.get(position);
                    }
                }
            });

            if (mResetTransitionAlpha) {
                ViewUtils.setTransitionAlpha(holder.itemView, 1f);
            }
        }

        @Override
        public int getItemCount() {
            return mInputList.size();
        }

        class ViewHolder extends RecyclerView.ViewHolder {
            final TextView inputLabelView;
            final TextView secondaryInputLabelView;

            ViewHolder(View v) {
                super(v);
                inputLabelView = (TextView) v.findViewById(R.id.input_label);
                secondaryInputLabelView = (TextView) v.findViewById(R.id.secondary_input_label);
            }
        }
    }

    private class InputsComparator implements Comparator<TvInputInfo> {
        @Override
        public int compare(TvInputInfo lhs, TvInputInfo rhs) {
            if (lhs == null) {
                return (rhs == null) ? 0 : 1;
            }
            if (rhs == null) {
                return -1;
            }

            boolean enabledL = isInputEnabled(lhs);
            boolean enabledR = isInputEnabled(rhs);
            if (enabledL != enabledR) {
                return enabledL ? -1 : 1;
            }

            int priorityL = getPriority(lhs);
            int priorityR = getPriority(rhs);
            if (priorityL != priorityR) {
                return priorityR - priorityL;
            }

            String customLabelL = (String) lhs.loadCustomLabel(getContext());
            String customLabelR = (String) rhs.loadCustomLabel(getContext());
            if (!TextUtils.equals(customLabelL, customLabelR)) {
                customLabelL = customLabelL == null ? "" : customLabelL;
                customLabelR = customLabelR == null ? "" : customLabelR;
                return customLabelL.compareToIgnoreCase(customLabelR);
            }

            String labelL = (String) lhs.loadLabel(getContext());
            String labelR = (String) rhs.loadLabel(getContext());
            labelL = labelL == null ? "" : labelL;
            labelR = labelR == null ? "" : labelR;
            return labelL.compareToIgnoreCase(labelR);
        }

        private int getPriority(TvInputInfo info) {
            switch (info.getType()) {
                case TvInputInfo.TYPE_TUNER:
                    return 9;
                case TvInputInfo.TYPE_HDMI:
                    HdmiDeviceInfo hdmiInfo = info.getHdmiDeviceInfo();
                    if (hdmiInfo != null && hdmiInfo.isCecDevice()) {
                        return 8;
                    }
                    return 7;
                case TvInputInfo.TYPE_DVI:
                    return 6;
                case TvInputInfo.TYPE_COMPONENT:
                    return 5;
                case TvInputInfo.TYPE_SVIDEO:
                    return 4;
                case TvInputInfo.TYPE_COMPOSITE:
                    return 3;
                case TvInputInfo.TYPE_DISPLAY_PORT:
                    return 2;
                case TvInputInfo.TYPE_VGA:
                    return 1;
                case TvInputInfo.TYPE_SCART:
                default:
                    return 0;
            }
        }
    }

    /**
     * A callback interface for the input selection.
     */
    public interface OnInputSelectedCallback {
        /**
         * Called when the tuner input is selected.
         */
        void onTunerInputSelected();

        /**
         * Called when the passthrough input is selected.
         */
        void onPassthroughInputSelected(@NonNull TvInputInfo input);
    }
}
