// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.chrome.browser.infobar;

import android.content.Context;
import android.content.res.Resources;
import android.graphics.Paint;
import android.support.v7.widget.SwitchCompat;
import android.text.method.LinkMovementMethod;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams;
import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.RatingBar;
import android.widget.Spinner;
import android.widget.TextView;

import org.chromium.base.ApiCompatibilityUtils;
import org.chromium.base.VisibleForTesting;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.widget.DualControlLayout;

/**
 * Lays out a group of controls (e.g. switches, spinners, or additional text) for InfoBars that need
 * more than the normal pair of buttons.
 *
 * This class works with the {@link InfoBarLayout} to define a standard set of controls with
 * standardized spacings and text styling that gets laid out in grid form: https://crbug.com/543205
 *
 * Manually specified margins on the children managed by this layout are EXPLICITLY ignored to
 * enforce a uniform margin between controls across all InfoBar types.  Do NOT circumvent this
 * restriction with creative layout definitions.  If the layout algorithm doesn't work for your new
 * InfoBar, convince Chrome for Android's UX team to amend the master spec and then change the
 * layout algorithm to match.
 *
 * TODO(dfalcantara): The line spacing multiplier is applied to all lines in JB & KK, even if the
 *                    TextView has only one line.  This throws off vertical alignment.  Find a
 *                    solution that hopefully doesn't involve subclassing the TextView.
 */
public final class InfoBarControlLayout extends ViewGroup {
    /**
     * ArrayAdapter that automatically determines what size make its Views to accommodate all of
     * its potential values.
     */
    public static final class InfoBarArrayAdapter<T> extends ArrayAdapter<T> {
        private final String mLabel;
        private int mMinWidthRequiredForValues;

        public InfoBarArrayAdapter(Context context, String label) {
            super(context, R.layout.infobar_control_spinner_drop_down);
            mLabel = label;
        }

        public InfoBarArrayAdapter(Context context, T[] objects) {
            super(context, R.layout.infobar_control_spinner_drop_down, objects);
            mLabel = null;
        }

        @Override
        public View getDropDownView(int position, View convertView, ViewGroup parent) {
            TextView view;
            if (convertView instanceof TextView) {
                view = (TextView) convertView;
            } else {
                view = (TextView) LayoutInflater.from(getContext())
                        .inflate(R.layout.infobar_control_spinner_drop_down, parent, false);
            }

            view.setText(getItem(position).toString());
            return view;
        }

        @Override
        public DualControlLayout getView(int position, View convertView, ViewGroup parent) {
            DualControlLayout view;
            if (convertView instanceof DualControlLayout) {
                view = (DualControlLayout) convertView;
            } else {
                view = (DualControlLayout) LayoutInflater.from(getContext())
                        .inflate(R.layout.infobar_control_spinner_view, parent, false);
            }

            // Set up the spinner label.  The text it displays won't change.
            TextView labelView = (TextView) view.getChildAt(0);
            labelView.setText(mLabel);

            // Because the values can be of different widths, the TextView may expand or shrink.
            // Enforcing a minimum width prevents the layout from doing so as the user swaps values,
            // preventing unwanted layout passes.
            TextView valueView = (TextView) view.getChildAt(1);
            valueView.setText(getItem(position).toString());
            valueView.setMinimumWidth(mMinWidthRequiredForValues);

            return view;
        }

        /**
         * Computes and records the minimum width required to display any of the values without
         * causing another layout pass when switching values.
         */
        int computeMinWidthRequiredForValues() {
            DualControlLayout layout = getView(0, null, null);
            TextView container = (TextView) layout.getChildAt(1);

            Paint textPaint = container.getPaint();
            float longestLanguageWidth = 0;
            for (int i = 0; i < getCount(); i++) {
                float width = textPaint.measureText(getItem(i).toString());
                longestLanguageWidth = Math.max(longestLanguageWidth, width);
            }

            mMinWidthRequiredForValues = (int) Math.ceil(longestLanguageWidth);
            return mMinWidthRequiredForValues;
        }

        /**
         * Explicitly sets the minimum width required to display all of the values.
         */
        void setMinWidthRequiredForValues(int requiredWidth) {
            mMinWidthRequiredForValues = requiredWidth;
        }
    }

    /**
     * Extends the regular LayoutParams by determining where a control should be located.
     */
    @VisibleForTesting
    static final class ControlLayoutParams extends LayoutParams {
        public int start;
        public int top;
        public int columnsRequired;
        private boolean mMustBeFullWidth;

        /**
         * Stores values required for laying out this ViewGroup's children.
         *
         * This is set up as a private method to mitigate attempts at adding controls to the layout
         * that aren't provided by the InfoBarControlLayout.
         */
        private ControlLayoutParams() {
            super(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
        }
    }

    private final int mMarginBetweenRows;
    private final int mMarginBetweenColumns;

    /**
     * Do not call this method directly; use {@link InfoBarLayout#addControlLayout()}.
     */
    InfoBarControlLayout(Context context) {
        super(context);

        Resources resources = context.getResources();
        mMarginBetweenRows =
                resources.getDimensionPixelSize(R.dimen.infobar_control_margin_between_rows);
        mMarginBetweenColumns =
                resources.getDimensionPixelSize(R.dimen.infobar_control_margin_between_columns);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        assert getLayoutParams().height == LayoutParams.WRAP_CONTENT
                : "Height of this layout cannot be constrained.";

        int fullWidth = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.UNSPECIFIED
                ? Integer.MAX_VALUE : MeasureSpec.getSize(widthMeasureSpec);
        int columnWidth = Math.max(0, (fullWidth - mMarginBetweenColumns) / 2);

        int atMostFullWidthSpec = MeasureSpec.makeMeasureSpec(fullWidth, MeasureSpec.AT_MOST);
        int exactlyFullWidthSpec = MeasureSpec.makeMeasureSpec(fullWidth, MeasureSpec.EXACTLY);
        int exactlyColumnWidthSpec = MeasureSpec.makeMeasureSpec(columnWidth, MeasureSpec.EXACTLY);
        int unspecifiedSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);

        // Figure out how many columns each child requires.
        for (int i = 0; i < getChildCount(); i++) {
            View child = getChildAt(i);
            measureChild(child, atMostFullWidthSpec, unspecifiedSpec);

            if (child.getMeasuredWidth() <= columnWidth
                    && !getControlLayoutParams(child).mMustBeFullWidth) {
                getControlLayoutParams(child).columnsRequired = 1;
            } else {
                getControlLayoutParams(child).columnsRequired = 2;
            }
        }

        // Pack all the children as tightly into rows as possible without changing their ordering.
        // Stretch out column-width controls if either it is the last control or the next one is
        // a full-width control.
        for (int i = 0; i < getChildCount(); i++) {
            ControlLayoutParams lp = getControlLayoutParams(getChildAt(i));

            if (i == getChildCount() - 1) {
                lp.columnsRequired = 2;
            } else {
                ControlLayoutParams nextLp = getControlLayoutParams(getChildAt(i + 1));
                if (lp.columnsRequired + nextLp.columnsRequired > 2) {
                    // This control is too big to place with the next child.
                    lp.columnsRequired = 2;
                } else {
                    // This and the next control fit on the same line.  Skip placing the next child.
                    i++;
                }
            }
        }

        // Measure all children, assuming they all have to fit within the width of the layout.
        // Height is unconstrained.
        for (int i = 0; i < getChildCount(); i++) {
            View child = getChildAt(i);
            ControlLayoutParams lp = getControlLayoutParams(child);
            int spec = lp.columnsRequired == 1 ? exactlyColumnWidthSpec : exactlyFullWidthSpec;
            measureChild(child, spec, unspecifiedSpec);
        }

        // Pack all the children as tightly into rows as possible without changing their ordering.
        int layoutHeight = 0;
        int nextChildStart = 0;
        int nextChildTop = 0;
        int currentRowHeight = 0;
        int columnsAvailable = 2;
        for (int i = 0; i < getChildCount(); i++) {
            View child = getChildAt(i);
            ControlLayoutParams lp = getControlLayoutParams(child);

            // If there isn't enough room left for the control, move to the next row.
            if (columnsAvailable < lp.columnsRequired) {
                layoutHeight += currentRowHeight + mMarginBetweenRows;
                nextChildStart = 0;
                nextChildTop = layoutHeight;
                currentRowHeight = 0;
                columnsAvailable = 2;
            }

            lp.top = nextChildTop;
            lp.start = nextChildStart;
            currentRowHeight = Math.max(currentRowHeight, child.getMeasuredHeight());
            columnsAvailable -= lp.columnsRequired;
            nextChildStart += lp.columnsRequired * (columnWidth + mMarginBetweenColumns);
        }

        // Compute the ViewGroup's height, accounting for the final row's height.
        layoutHeight += currentRowHeight;
        setMeasuredDimension(resolveSize(fullWidth, widthMeasureSpec),
                resolveSize(layoutHeight, heightMeasureSpec));
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        int width = right - left;
        boolean isRtl = ApiCompatibilityUtils.isLayoutRtl(this);

        // Child positions were already determined during the measurement pass.
        for (int childIndex = 0; childIndex < getChildCount(); childIndex++) {
            View child = getChildAt(childIndex);
            int childLeft = getControlLayoutParams(child).start;
            if (isRtl) childLeft = width - childLeft - child.getMeasuredWidth();

            int childTop = getControlLayoutParams(child).top;
            int childRight = childLeft + child.getMeasuredWidth();
            int childBottom = childTop + child.getMeasuredHeight();
            child.layout(childLeft, childTop, childRight, childBottom);
        }
    }

    /**
     * Adds an icon with a descriptive message to the layout.
     *
     * -----------------------------------------------------
     * | ICON | PRIMARY MESSAGE SECONDARY MESSAGE          |
     * -----------------------------------------------------
     * If an icon is not provided, the ImageView that would normally show it is hidden.
     *
     * @param iconResourceId   ID of the drawable to use for the icon.
     * @param iconColorId      ID of the tint color for the icon, or 0 for default.
     * @param primaryMessage   Message to display for the toggle.
     * @param secondaryMessage Additional descriptive text for the toggle.  May be null.
     */
    public View addIcon(int iconResourceId, int iconColorId, CharSequence primaryMessage,
            CharSequence secondaryMessage) {
        LinearLayout layout = (LinearLayout) LayoutInflater.from(getContext()).inflate(
                R.layout.infobar_control_icon_with_description, this, false);
        addView(layout, new ControlLayoutParams());

        ImageView iconView = (ImageView) layout.findViewById(R.id.control_icon);
        iconView.setImageResource(iconResourceId);
        if (iconColorId != 0) {
            iconView.setColorFilter(ApiCompatibilityUtils.getColor(getResources(), iconColorId));
        }

        // The primary message text is always displayed.
        TextView primaryView = (TextView) layout.findViewById(R.id.control_message);
        primaryView.setText(primaryMessage);

        // The secondary message text is optional.
        TextView secondaryView =
                (TextView) layout.findViewById(R.id.control_secondary_message);
        if (secondaryMessage == null) {
            layout.removeView(secondaryView);
        } else {
            secondaryView.setText(secondaryMessage);
        }

        return layout;
    }

    /**
     * Creates a standard toggle switch and adds it to the layout.
     *
     * -------------------------------------------------
     * | ICON | MESSAGE                       | TOGGLE |
     * -------------------------------------------------
     * If an icon is not provided, the ImageView that would normally show it is hidden.
     *
     * @param iconResourceId ID of the drawable to use for the icon, or 0 to hide the ImageView.
     * @param iconColorId    ID of the tint color for the icon, or 0 for default.
     * @param toggleMessage  Message to display for the toggle.
     * @param toggleId       ID to use for the toggle.
     * @param isChecked      Whether the toggle should start off checked.
     */
    public View addSwitch(int iconResourceId, int iconColorId, CharSequence toggleMessage,
            int toggleId, boolean isChecked) {
        LinearLayout switchLayout = (LinearLayout) LayoutInflater.from(getContext()).inflate(
                R.layout.infobar_control_toggle, this, false);
        addView(switchLayout, new ControlLayoutParams());

        ImageView iconView = (ImageView) switchLayout.findViewById(R.id.control_icon);
        if (iconResourceId == 0) {
            switchLayout.removeView(iconView);
        } else {
            iconView.setImageResource(iconResourceId);
            if (iconColorId != 0) {
                iconView.setColorFilter(
                        ApiCompatibilityUtils.getColor(getResources(), iconColorId));
            }
        }

        TextView messageView = (TextView) switchLayout.findViewById(R.id.control_message);
        messageView.setText(toggleMessage);

        SwitchCompat switchView =
                (SwitchCompat) switchLayout.findViewById(R.id.control_toggle_switch);
        switchView.setId(toggleId);
        switchView.setChecked(isChecked);

        return switchLayout;
    }

    /**
     * Creates a standard spinner and adds it to the layout.
     */
    public <T> Spinner addSpinner(int spinnerId, ArrayAdapter<T> arrayAdapter) {
        Spinner spinner = (Spinner) LayoutInflater.from(getContext()).inflate(
                R.layout.infobar_control_spinner, this, false);
        spinner.setAdapter(arrayAdapter);
        addView(spinner, new ControlLayoutParams());
        spinner.setId(spinnerId);
        return spinner;
    }

    /**
     * Creates and adds a full-width control with additional text describing what an InfoBar is for.
     */
    public View addDescription(CharSequence message) {
        ControlLayoutParams params = new ControlLayoutParams();
        params.mMustBeFullWidth = true;

        TextView descriptionView = (TextView) LayoutInflater.from(getContext()).inflate(
                R.layout.infobar_control_description, this, false);
        addView(descriptionView, params);

        descriptionView.setText(message);
        descriptionView.setMovementMethod(LinkMovementMethod.getInstance());
        return descriptionView;
    }

    /**
     * Creates and adds a control that shows a review rating score.
     *
     * @param rating Fractional rating out of 5 stars.
     */
    public View addRatingBar(float rating) {
        View ratingLayout = LayoutInflater.from(getContext()).inflate(
                R.layout.infobar_control_rating, this, false);
        addView(ratingLayout, new ControlLayoutParams());

        RatingBar ratingView = (RatingBar) ratingLayout.findViewById(R.id.control_rating);
        ratingView.setRating(rating);
        return ratingView;
    }

    /**
     * Do NOT call this method directly from outside {@link InfoBarLayout#InfoBarLayout()}.
     *
     * Adds a full-width control showing the main InfoBar message.  For other text, you should call
     * {@link InfoBarControlLayout#addDescription(CharSequence)} instead.
     */
    TextView addMainMessage(CharSequence mainMessage) {
        ControlLayoutParams params = new ControlLayoutParams();
        params.mMustBeFullWidth = true;

        TextView messageView = (TextView) LayoutInflater.from(getContext()).inflate(
                R.layout.infobar_control_message, this, false);
        addView(messageView, params);

        messageView.setText(mainMessage);
        messageView.setMovementMethod(LinkMovementMethod.getInstance());
        return messageView;
    }

    /**
     * @return The {@link ControlLayoutParams} for the given child.
     */
    @VisibleForTesting
    static ControlLayoutParams getControlLayoutParams(View child) {
        return (ControlLayoutParams) child.getLayoutParams();
    }

}