/*
 * Copyright (c) 2016, 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.car.hvac.controllers;

import android.animation.Animator;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.drawable.Drawable;
import android.os.Handler;
import android.view.MotionEvent;
import android.view.View;
import android.view.WindowManager;
import android.widget.ImageView;

import androidx.annotation.IntDef;

import com.android.car.hvac.HvacController;
import com.android.car.hvac.R;
import com.android.car.hvac.ui.FanDirectionButtons;
import com.android.car.hvac.ui.FanSpeedBar;
import com.android.car.hvac.ui.HvacPanelRow;
import com.android.car.hvac.ui.SeatWarmerButton;
import com.android.car.hvac.ui.TemperatureBarOverlay;
import com.android.car.hvac.ui.ToggleButton;

import java.util.ArrayList;
import java.util.List;

/**
 * A state machine to control transition from various HVAC UI layouts.
 */
public class HvacPanelController {
    private static final int PANEL_ANIMATION_TIME_MS = 200;
    private static final int PANEL_COLLAPSE_ANIMATION_TIME_MS = 500;

    private static final int PANEL_ANIMATION_DELAY_MS = 100;
    private static final int PANEL_ANIMATION_LONG_DELAY_MS = 3 * PANEL_ANIMATION_DELAY_MS;

    private static final float DISABLED_BUTTON_ALPHA = 0.20f;
    private static final float ENABLED_BUTTON_ALPHA = 1.0f;

    private static final int STATE_COLLAPSED = 0;
    private static final int STATE_COLLAPSED_DIMMED = 1;
    private static final int STATE_FULL_EXPANDED = 2;
    // Allows for delayed invocation of code. Thus we can control UI events to happen after
    // others. Example: set something visible but do it after we've complete current UI updates.
    private static Handler handler = new Handler();

    // We have both a collapsed and expanded version of the overlays due to a bug
    // that does not correctly rendering a window resize event. Thus we toggle the the visibility
    // of the windows instead. A better solution would be a having separate views collapsed state
    // since the it does not need the other elements but this works for now.
    private TemperatureBarOverlay mDriverTemperatureBarCollapsed;
    private TemperatureBarOverlay mPassengerTemperatureBarCollapsed;
    private TemperatureBarOverlay mDriverTemperatureBarExpanded;
    private TemperatureBarOverlay mPassengerTemperatureBarExpanded;
    private final boolean mShowCollapsed;

    @IntDef({STATE_COLLAPSED,
            STATE_COLLAPSED_DIMMED,
            STATE_FULL_EXPANDED})
    private @interface HvacPanelState {}

    private @HvacPanelState int mCurrentState;

    private int mPanelCollapsedHeight;
    private int mPanelFullExpandedHeight;

    private View mPanel;
    private View mContainer;

    private SeatWarmerButton mDriverSeatWarmer;
    private SeatWarmerButton mPassengerSeatWarmer;

    private ToggleButton mHvacPowerSwitch;

    private ToggleButton mAcButton;
    private ToggleButton mRecycleAirButton;

    private ToggleButton mFrontDefrosterButton;
    private ToggleButton mRearDefrosterButton;

    private Drawable mAutoOnDrawable;
    private Drawable mAutoOffDrawable;

    private ImageView mAutoButton;

    private HvacPanelRow mPanelTopRow;
    private HvacPanelRow mPanelBottomRow;

    private FanSpeedBar mFanSpeedBar;
    private FanDirectionButtons mFanDirectionButtons;

    private float mTopPanelMaxAlpha = 1.0f;

    private WindowManager mWindowManager;

    private HvacPanelStateTransition mTransition;

    private View mHvacFanControlBackground;

    private HvacController mHvacController;
    private FanSpeedBarController mFanSpeedBarController;
    private FanDirectionButtonsController mFanDirectionButtonsController;
    private TemperatureController mTemperatureController;
    private TemperatureController mTemperatureControllerCollapsed;
    private SeatWarmerController mSeatWarmerController;

    private boolean mInAnimation;

    // TODO: read from shared pref
    private boolean mAutoMode;

    public HvacPanelController(Context context, View container,
            WindowManager windowManager,
            TemperatureBarOverlay driverTemperatureExpanded,
            TemperatureBarOverlay passengerTemperatureExpanded,
            TemperatureBarOverlay driverTemperatureBarCollapsed,
            TemperatureBarOverlay passengerTemperatureBarCollapsed) {
        Resources res = context.getResources();
        mShowCollapsed = res.getBoolean(R.bool.config_showCollapsedBars);

        mDriverTemperatureBarCollapsed = driverTemperatureBarCollapsed;
        mPassengerTemperatureBarCollapsed = passengerTemperatureBarCollapsed;

        mCurrentState = STATE_COLLAPSED;
        mWindowManager = windowManager;

        mPanelCollapsedHeight = res.getDimensionPixelSize(R.dimen.car_hvac_panel_collapsed_height);
        mPanelFullExpandedHeight
                = res.getDimensionPixelSize(R.dimen.car_hvac_panel_full_expanded_height);

        mAutoOffDrawable = res.getDrawable(R.drawable.ic_auto_off);
        mAutoOnDrawable = res.getDrawable(R.drawable.ic_auto_on);

        mDriverTemperatureBarExpanded = driverTemperatureExpanded;
        mPassengerTemperatureBarExpanded = passengerTemperatureExpanded;

        mDriverTemperatureBarExpanded.setCloseButtonOnClickListener(mCollapseHvac);
        mPassengerTemperatureBarExpanded.setCloseButtonOnClickListener(mCollapseHvac);

        // Initially the hvac panel is collapsed, hide the expanded version.
        mDriverTemperatureBarExpanded.setVisibility(View.INVISIBLE);
        mPassengerTemperatureBarExpanded.setVisibility(View.INVISIBLE);

        mPassengerTemperatureBarCollapsed.setBarOnClickListener(mExpandHvac);
        mDriverTemperatureBarCollapsed.setBarOnClickListener(mExpandHvac);

        mContainer = container;
        mContainer.setVisibility(View.INVISIBLE);
        mContainer.setOnClickListener(mCollapseHvac);
        mPanel = mContainer.findViewById(R.id.hvac_center_panel);

        mHvacFanControlBackground = mPanel.findViewById(R.id.fan_control_bg);
        // set clickable so that clicks are not forward to the mContainer. This way a miss click
        // does not close the UI
        mPanel.setClickable(true);

        // Set up top row buttons
        mPanelTopRow = (HvacPanelRow) mContainer.findViewById(R.id.top_row);

        mAcButton = (ToggleButton) mPanelTopRow.findViewById(R.id.ac_button);
        mAcButton.setToggleIcons(res.getDrawable(R.drawable.ic_ac_on),
                res.getDrawable(R.drawable.ic_ac_off));

        mRecycleAirButton = (ToggleButton) mPanelTopRow.findViewById(R.id.recycle_air_button);

        mRecycleAirButton.setToggleIcons(res.getDrawable(R.drawable.ic_recycle_air_on),
                res.getDrawable(R.drawable.ic_recycle_air_off));

        // Setup bottom row buttons
        mPanelBottomRow = (HvacPanelRow) mContainer.findViewById(R.id.bottom_row);

        mAutoButton = (ImageView) mContainer.findViewById(R.id.auto_button);
        mAutoButton.setOnClickListener(mAutoButtonClickListener);

        mFrontDefrosterButton = (ToggleButton) mPanelBottomRow.findViewById(R.id.front_defroster);
        mRearDefrosterButton = (ToggleButton) mPanelBottomRow.findViewById(R.id.rear_defroster);

        mFrontDefrosterButton.setToggleIcons(res.getDrawable(R.drawable.ic_front_defroster_on),
                res.getDrawable(R.drawable.ic_front_defroster_off));

        mRearDefrosterButton.setToggleIcons(res.getDrawable(R.drawable.ic_rear_defroster_on),
                res.getDrawable(R.drawable.ic_rear_defroster_off));

        mFanSpeedBar = (FanSpeedBar) mContainer.findViewById(R.id.fan_speed_bar);
        mFanDirectionButtons = (FanDirectionButtons) mContainer.findViewById(R.id.fan_direction_buttons);

        mDriverSeatWarmer = (SeatWarmerButton) mContainer.findViewById(R.id.left_seat_heater);
        mPassengerSeatWarmer = (SeatWarmerButton) mContainer.findViewById(R.id.right_seat_heater);

        mHvacPowerSwitch = (ToggleButton)mPanelBottomRow.findViewById(R.id.hvac_master_switch);
        // TODO: this is not good UX design - just a placeholder
        mHvacPowerSwitch.setToggleIcons(res.getDrawable(R.drawable.ac_master_switch_on),
            res.getDrawable(R.drawable.ac_master_switch_off));

        if (!mShowCollapsed) {
            mDriverTemperatureBarCollapsed.setVisibility(View.INVISIBLE);
            mPassengerTemperatureBarCollapsed.setVisibility(View.INVISIBLE);
        }
    }

    public void updateHvacController(HvacController controller) {
        //TODO: handle disconnected HvacController.
        mHvacController = controller;

        mFanSpeedBarController = new FanSpeedBarController(mFanSpeedBar, mHvacController);
        mFanDirectionButtonsController
                = new FanDirectionButtonsController(mFanDirectionButtons, mHvacController);
        mTemperatureController = new TemperatureController(
                mPassengerTemperatureBarExpanded,
                mDriverTemperatureBarExpanded,
                mPassengerTemperatureBarCollapsed,
                mDriverTemperatureBarCollapsed,
                mHvacController);
        mSeatWarmerController = new SeatWarmerController(mPassengerSeatWarmer,
                mDriverSeatWarmer, mHvacController);

        // Toggle buttons do not need additional logic to map between hardware
        // and UI settings. Simply use a ToggleListener to handle clicks.
        mAcButton.setIsOn(mHvacController.getAcState());
        mAcButton.setToggleListener(new ToggleButton.ToggleListener() {
            @Override
            public void onToggled(boolean isOn) {
                mHvacController.setAcState(isOn);
            }
        });

        mFrontDefrosterButton.setIsOn(mHvacController.getFrontDefrosterState());
        mFrontDefrosterButton.setToggleListener(new ToggleButton.ToggleListener() {
            @Override
            public void onToggled(boolean isOn) {
                mHvacController.setFrontDefrosterState(isOn);
            }
        });

        mRearDefrosterButton.setIsOn(mHvacController.getRearDefrosterState());
        mRearDefrosterButton.setToggleListener(new ToggleButton.ToggleListener() {
            @Override
            public void onToggled(boolean isOn) {
                mHvacController.setRearDefrosterState(isOn);
            }
        });

        mRecycleAirButton.setIsOn(mHvacController.getAirCirculationState());
        mRecycleAirButton.setToggleListener(new ToggleButton.ToggleListener() {
            @Override
            public void onToggled(boolean isOn) {
                mHvacController.setAirCirculation(isOn);
            }
        });

        setAutoMode(mHvacController.getAutoModeState());

        mHvacPowerSwitch.setIsOn(mHvacController.getHvacPowerState());
        mHvacPowerSwitch.setToggleListener(isOn -> mHvacController.setHvacPowerState(isOn));

        mHvacController.registerCallback(mToggleButtonCallbacks);
        mToggleButtonCallbacks.onHvacPowerChange(mHvacController.getHvacPowerState());
    }

    private HvacController.Callback mToggleButtonCallbacks
            = new HvacController.Callback() {
        @Override
        public void onAirCirculationChange(boolean isOn) {
            mRecycleAirButton.setIsOn(isOn);
        }

        @Override
        public void onFrontDefrosterChange(boolean isOn) {
            mFrontDefrosterButton.setIsOn(isOn);
        }

        @Override
        public void onRearDefrosterChange(boolean isOn) {
            mRearDefrosterButton.setIsOn(isOn);
        }

        @Override
        public void onAcStateChange(boolean isOn) {
            mAcButton.setIsOn(isOn);
        }

        @Override
        public void onAutoModeChange(boolean isOn) {
            mAutoMode = isOn;
            setAutoMode(mAutoMode);
        }
    };

    /**
     * Take the listeners and animators from a {@link AnimatorSet} and merge them to the
     * input {@link Animator} and {@link android.animation.Animator.AnimatorListener} lists.
     */
    private void combineAnimationSet(List<Animator> animatorList,
            List<Animator.AnimatorListener> listenerList, AnimatorSet set) {

        ArrayList<Animator> list = set.getChildAnimations();
        if (list != null) {
            int size = list.size();
            for (int i = 0; i < size; i++) {
                animatorList.add(list.get(i));
            }
        }

        ArrayList<Animator.AnimatorListener> listeners = set.getListeners();
        if (listeners != null) {
            int size = listeners.size();
            for (int i = 0; i < size; i++) {
                listenerList.add(listeners.get(i));
            }
        }
    }

    /**
     * Play necessary animations between {@link HvacPanelState} transitions
     */
    private void transitionState(@HvacPanelState int startState, @HvacPanelState int endState) {
        if (startState == endState || mInAnimation) {
            return;
        }

        List<Animator> animationList = new ArrayList<>();
        List<Animator.AnimatorListener> listenerList = new ArrayList<>();
        ValueAnimator heightAnimator = getPanelHeightAnimator(startState, endState);
        mTransition = new HvacPanelStateTransition(startState, endState);
        ValueAnimator fanBgAlphaAnimator;
        switch (endState) {
            case STATE_COLLAPSED:
                // Transition to collapsed state:
                // 1. Collapse the temperature bars.
                // 2. Collapse the top and bottom panel, staggered with a different delay.
                // 3. Decrease height of the hvac center panel, but maintain container height.
                // 4. Fade the background of the fan controls seperately to create staggered effect.
                animationList.add(heightAnimator);
                heightAnimator.setDuration(PANEL_COLLAPSE_ANIMATION_TIME_MS);
                fanBgAlphaAnimator
                        = ObjectAnimator.ofFloat(mHvacFanControlBackground, View.ALPHA, 1, 0)
                        .setDuration(PANEL_COLLAPSE_ANIMATION_TIME_MS);
                fanBgAlphaAnimator.setStartDelay(PANEL_ANIMATION_DELAY_MS);
                animationList.add(fanBgAlphaAnimator);

                ValueAnimator panelAlphaAnimator
                        = ObjectAnimator.ofFloat(mContainer, View.ALPHA, 1, 0);
                panelAlphaAnimator.setDuration(200);
                panelAlphaAnimator.setStartDelay(300);

                animationList.add(panelAlphaAnimator);

                combineAnimationSet(animationList, listenerList,
                        mDriverTemperatureBarExpanded.getCollapseAnimations());
                combineAnimationSet(animationList, listenerList,
                        mPassengerTemperatureBarExpanded.getCollapseAnimations());

                combineAnimationSet(animationList, listenerList,
                        mPanelTopRow.getCollapseAnimation(PANEL_ANIMATION_DELAY_MS,
                                mTopPanelMaxAlpha));
                combineAnimationSet(animationList, listenerList,
                        mPanelBottomRow.getCollapseAnimation(PANEL_ANIMATION_DELAY_MS,
                                mTopPanelMaxAlpha));
                break;
            case STATE_COLLAPSED_DIMMED:
                // Hide the temperature numbers, open arrows and auto state button.
                // TODO: determine if this section is still needed.
                break;
            case STATE_FULL_EXPANDED:
                // Transition to expaneded state:
                // 1. Expand the temperature bars.
                // 2. Expand the top and bottom panel, staggered with a different delay.
                // 3. Increase height of the hvac center panel, but maintain container height.
                // 4. Fade in fan control background in a staggered manner.
                fanBgAlphaAnimator
                        = ObjectAnimator.ofFloat(mHvacFanControlBackground, View.ALPHA, 0, 1)
                        .setDuration(PANEL_ANIMATION_TIME_MS);
                fanBgAlphaAnimator.setStartDelay(PANEL_ANIMATION_DELAY_MS);
                animationList.add(fanBgAlphaAnimator);

                animationList.add(heightAnimator);
                combineAnimationSet(animationList, listenerList,
                        mDriverTemperatureBarExpanded.getExpandAnimatons());
                combineAnimationSet(animationList, listenerList,
                        mPassengerTemperatureBarExpanded.getExpandAnimatons());

                // During expansion, the bottom panel animation should be delayed
                combineAnimationSet(animationList, listenerList,
                        mPanelTopRow.getExpandAnimation(PANEL_ANIMATION_DELAY_MS,
                                mTopPanelMaxAlpha));
                combineAnimationSet(animationList, listenerList,
                        mPanelBottomRow.getExpandAnimation(PANEL_ANIMATION_LONG_DELAY_MS, 1f));
                break;
            default:
        }

        // If there are animations for the state change, play them all together and ensure
        // the animation listeners are attached.
        if (animationList.size() > 0) {
            AnimatorSet animatorSet = new AnimatorSet();
            animatorSet.playTogether(animationList);
            for (Animator.AnimatorListener listener : listenerList) {
                animatorSet.addListener(listener);
            }
            animatorSet.addListener(mAnimatorListener);
            animatorSet.start();
        }
    }

    private AnimatorSet.AnimatorListener mAnimatorListener = new AnimatorSet.AnimatorListener() {
        @Override
        public void onAnimationStart(Animator animation) {
            mTransition.onTransitionStart();
        }

        @Override
        public void onAnimationEnd(Animator animation) {
            mTransition.onTransitionComplete();
        }

        @Override
        public void onAnimationCancel(Animator animation) {}

        @Override
        public void onAnimationRepeat(Animator animation) {}
    };

    private ValueAnimator getPanelHeightAnimator(@HvacPanelState int startState,
            @HvacPanelState int endState) {
        int startHeight = getStateHeight(startState);
        int endHeight = getStateHeight(endState);
        if (startHeight == endHeight) {
            return null;
        }

        ValueAnimator heightAnimator = new ValueAnimator().ofInt(startHeight, endHeight)
                .setDuration(PANEL_ANIMATION_TIME_MS);
        heightAnimator.addUpdateListener(mHeightUpdateListener);
        return heightAnimator;
    }

    private int getStateHeight(@HvacPanelState int state) {
        switch (state) {
            case STATE_COLLAPSED:
            case STATE_COLLAPSED_DIMMED:
                return mPanelCollapsedHeight;
            case STATE_FULL_EXPANDED:
                return mPanelFullExpandedHeight;
            default:
                throw new IllegalArgumentException("No height mapped to HVAC State: " + state);
        }
    }

    private void setAutoMode(boolean isOn) {
        if (isOn) {
            mPanelTopRow.setOnTouchListener(new View.OnTouchListener() {
                @Override
                public boolean onTouch(View v, MotionEvent event) {
                    return true;
                }
            });
            mAutoMode = true;
            mPanelTopRow.disablePanel(true);
            mTopPanelMaxAlpha = DISABLED_BUTTON_ALPHA;
            mAutoButton.setImageDrawable(mAutoOnDrawable);
        } else {
            mPanelTopRow.disablePanel(false);
            mTopPanelMaxAlpha = ENABLED_BUTTON_ALPHA;
            mAutoButton.setImageDrawable(mAutoOffDrawable);
        }
        mHvacFanControlBackground.setAlpha(mTopPanelMaxAlpha);
        mPanelTopRow.setAlpha(mTopPanelMaxAlpha);
    }

    private View.OnClickListener mAutoButtonClickListener = new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            if (mAutoMode) {
                mAutoMode = false;
            } else {
                mAutoMode = true;
            }
            mHvacController.setAutoMode(mAutoMode);
            setAutoMode(mAutoMode);
        }
    };

     private View.OnClickListener mCollapseHvac = new View.OnClickListener() {
         @Override
         public void onClick(View v) {
             if (mInAnimation) {
                 return;
             }
             if (mCurrentState != STATE_COLLAPSED) {
                 transitionState(mCurrentState, STATE_COLLAPSED);
             }
         }
     };

    public void toggleHvacUi() {
        if (mCurrentState != STATE_COLLAPSED) {
            mCollapseHvac.onClick(null);
        } else {
            mExpandHvac.onClick(null);
        }
    }

    public void collapseHvacUi() {
        if (mCurrentState != STATE_COLLAPSED) {
            mCollapseHvac.onClick(null);
        }
    }

    public View.OnClickListener mExpandHvac = new View.OnClickListener() {
        @Override
        public void onClick(View v) {
             if (mInAnimation) {
                return;
            }
            if (mCurrentState != STATE_FULL_EXPANDED) {
                transitionState(mCurrentState, STATE_FULL_EXPANDED);
            }
        }
    };


    private ValueAnimator.AnimatorUpdateListener mHeightUpdateListener
            = new ValueAnimator.AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            int height = (Integer) animation.getAnimatedValue();
            int currentHeight = mPanel.getLayoutParams().height;
            mPanel.getLayoutParams().height = height;
            mPanel.setTop(mPanel.getTop() + height - currentHeight);
            mPanel.requestLayout();
        }
    };

    /**
     * Handles the necessary setup/clean up before and after a state transition.
     */
    private class HvacPanelStateTransition {
        private @HvacPanelState int mEndState;
        private @HvacPanelState int mStartState;

        public HvacPanelStateTransition(@HvacPanelState int startState,
                @HvacPanelState int endState) {
            mStartState = startState;
            mEndState = endState;
        }

        public void onTransitionStart() {
            mInAnimation = true;
            if (mEndState == STATE_FULL_EXPANDED) {
                mPassengerTemperatureBarExpanded.setVisibility(View.VISIBLE);
                mDriverTemperatureBarExpanded.setVisibility(View.VISIBLE);
                if (mShowCollapsed) {
                    mDriverTemperatureBarCollapsed.setVisibility(View.INVISIBLE);
                    mPassengerTemperatureBarCollapsed.setVisibility(View.INVISIBLE);
                }
                mContainer.setAlpha(1);
                mContainer.setVisibility(View.VISIBLE);
            }
        }

        public void onTransitionComplete() {
            if (mEndState == STATE_COLLAPSED) {
                if (mShowCollapsed) {
                    mDriverTemperatureBarCollapsed.setVisibility(View.VISIBLE);
                    mPassengerTemperatureBarCollapsed.setVisibility(View.VISIBLE);
                }
                handler.postAtFrontOfQueue(() -> {
                    mDriverTemperatureBarExpanded.setVisibility(View.INVISIBLE);
                    mPassengerTemperatureBarExpanded.setVisibility(View.INVISIBLE);
                });
            }
            if (mStartState == STATE_FULL_EXPANDED) {
                mContainer.setVisibility(View.INVISIBLE);
            }

            // set new states
            mCurrentState = mEndState;
            mInAnimation = false;
        }
    }
}
