package ernestoyaquello.com.verticalstepperform;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Rect;

import androidx.annotation.LayoutRes;
import androidx.appcompat.widget.AppCompatImageButton;

import android.os.Bundle;
import android.os.Parcelable;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewTreeObserver;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.ScrollView;

import java.util.Arrays;
import java.util.List;

import androidx.core.content.ContextCompat;
import ernestoyaquello.com.verticalstepperform.listener.StepperFormListener;

/**
 * Custom layout that implements a vertical stepper form.
 */
public class VerticalStepperFormView extends LinearLayout {

    FormStepListener internalListener;
    FormStyle style;

    private StepperFormListener listener;
    private KeyboardTogglingObserver keyboardTogglingObserver;
    private List<StepHelper> stepHelpers;
    private boolean initialized;

    private LinearLayout formContentView;
    private ScrollView stepsScrollView;
    private ProgressBar progressBar;
    private AppCompatImageButton previousStepButton, nextStepButton;
    private View bottomNavigationView;

    private boolean formCompleted;
    private boolean keyboardIsOpen;

    public VerticalStepperFormView(Context context) {
        super(context);

        onConstructed(context, null, 0);
    }

    public VerticalStepperFormView(Context context, AttributeSet attrs) {
        super(context, attrs);

        onConstructed(context, attrs, 0);
    }

    public VerticalStepperFormView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        onConstructed(context, attrs, defStyleAttr);
    }

    /**
     * Gets an instance of the builder that will be used to set up and initialize the form.
     *
     * @param stepperFormListener The listener for the stepper form events.
     * @param steps An array with the steps that will be displayed in the form.
     * @return An instance of the stepper form builder. Use it to configure and initialize the form.
     */
    public Builder setup(StepperFormListener stepperFormListener, Step... steps) {
        return new Builder(this, stepperFormListener, steps);
    }

    /**
     * Gets an instance of the builder that will be used to set up and initialize the form.
     *
     * @param stepperFormListener The listener for the stepper form events.
     * @param steps A list with the steps that will be displayed in the form.
     * @return An instance of the stepper form builder. Use it to configure and initialize the form.
     */
    public Builder setup(StepperFormListener stepperFormListener, List<Step> steps) {
        Step[] stepsArray = steps.toArray(new Step[0]);
        return new Builder(this, stepperFormListener, stepsArray);
    }

    /**
     * Marks the currently open step as completed or uncompleted depending on whether the step data
     * is valid or not.
     *
     * @param useAnimations True to animate the changes in the views, false to not.
     * @return True if the step was found and marked as completed; false otherwise.
     */
    public synchronized boolean markOpenStepAsCompletedOrUncompleted(boolean useAnimations) {
        return markStepAsCompletedOrUncompleted(getOpenStepPosition(), useAnimations);
    }

    /**
     * Marks the specified step as completed or uncompleted depending on whether the step data is 
     * valid or not.
     *
     * @param stepPosition The step position.
     * @param useAnimations True to animate the changes in the views, false to not.
     * @return True if the step was found and marked as completed; false otherwise.
     */
    public boolean markStepAsCompletedOrUncompleted(int stepPosition, boolean useAnimations) {
        if (stepPosition >= 0 && stepPosition < stepHelpers.size()) {
            StepHelper stepHelper = stepHelpers.get(stepPosition);
            return stepHelper.getStepInstance().markAsCompletedOrUncompleted(useAnimations);
        }

        return false;
    }

    /**
     * Marks the currently open step as completed.
     *
     * @param useAnimations True to animate the changes in the views, false to not.
     */
    public synchronized void markOpenStepAsCompleted(boolean useAnimations) {
        markStepAsCompleted(getOpenStepPosition(), useAnimations);
    }

    /**
     * Marks the specified step as completed.
     *
     * @param stepPosition The step position.
     * @param useAnimations True to animate the changes in the views, false to not.
     */
    public void markStepAsCompleted(int stepPosition, boolean useAnimations) {
        if (stepPosition >= 0 && stepPosition < stepHelpers.size()) {
            StepHelper stepHelper = stepHelpers.get(stepPosition);
            stepHelper.getStepInstance().markAsCompleted(useAnimations);
        }
    }

    /**
     * Marks the currently open step as uncompleted.
     *
     * @param errorMessage The error message.
     * @param useAnimations True to animate the changes in the views, false to not.
     */
    public synchronized void markOpenStepAsUncompleted(boolean useAnimations, String errorMessage) {
        markStepAsUncompleted(getOpenStepPosition(), errorMessage, useAnimations);
    }

    /**
     * Marks the specified step as uncompleted.
     *
     * @param stepPosition The step position.
     * @param errorMessage The error message.
     * @param useAnimations True to animate the changes in the views, false to not.
     */
    public void markStepAsUncompleted(int stepPosition, String errorMessage, boolean useAnimations) {
        if (stepPosition >= 0 && stepPosition < stepHelpers.size()) {
            StepHelper stepHelper = stepHelpers.get(stepPosition);
            stepHelper.getStepInstance().markAsUncompleted(errorMessage, useAnimations);
        }
    }

    /**
     * Determines whether the open step is marked as completed or not.
     *
     * @return True if the open step is currently marked as completed; false otherwise.
     */
    public synchronized boolean isOpenStepCompleted() {
        return isStepCompleted(getOpenStepPosition());
    }

    /**
     * Determines whether the specified step is marked as completed or not.
     *
     * @param stepPosition The step position.
     * @return True if the step is currently marked as completed; false otherwise.
     */
    public boolean isStepCompleted(int stepPosition) {
        if (stepPosition >= 0 && stepPosition < stepHelpers.size()) {
            return stepHelpers.get(stepPosition).getStepInstance().isCompleted();
        }

        return false;
    }

    /**
     * Determines whether there is at least one step marked as completed.
     *
     * @return True if at least one step has been marked as completed; false otherwise.
     */
    public boolean isAnyStepCompleted() {
        for (int i = 0; i < stepHelpers.size(); i++) {
            if (stepHelpers.get(i).getStepInstance().isCompleted()) {
                return true;
            }
        }

        return false;
    }

    /**
     * Determines whether all the steps previous to the specified one are currently marked as completed.
     *
     * @param stepPosition The step position.
     * @return True if all the steps previous to the specified one are marked as completed; false otherwise.
     */
    public boolean areAllPreviousStepsCompleted(int stepPosition) {
        boolean previousStepsAreCompleted = true;
        for (int i = stepPosition - 1; i >= 0; i--) {
            previousStepsAreCompleted &= stepHelpers.get(i).getStepInstance().isCompleted();
        }

        return previousStepsAreCompleted;
    }

    /**
     * Determines whether all the steps are currently marked as completed.
     *
     * @return True if all the steps are marked as completed; false otherwise.
     */
    public boolean areAllStepsCompleted() {
        return areAllPreviousStepsCompleted(stepHelpers.size());
    }

    /**
     * Determines whether the form has already been completed or cancelled.
     * Please note that this could return false even if all the steps are completed (for example,
     * if the user has filled in all the required data but hasn't submitted the form yet).
     *
     * @return True if the form has been completed or cancelled; false otherwise.
     */
    public boolean isFormCompleted() {
        return formCompleted;
    }

    /**
     * If possible, goes to the step that is positioned after the currently open one, closing the
     * current one and opening the next one.
     * Please note that, unless allowNonLinearNavigation is set to true, it will only be possible to
     * navigate to a certain step if all the previous ones are marked as completed.
     *
     * @param useAnimations Indicates whether or not the affected steps will be opened/closed using
     *                      animations.
     * @return True if the navigation to the step was performed; false otherwise.
     */
    public synchronized boolean goToNextStep(boolean useAnimations) {
        return goToStep(getOpenStepPosition() + 1, useAnimations);
    }

    /**
     * If possible, goes to the step that is positioned before the currently open one, closing the
     * current one and opening the previous one.
     * Please note that, unless allowNonLinearNavigation is set to true, it will only be possible to
     * navigate to a certain step if all the previous ones are marked as completed.
     *
     * @param useAnimations Indicates whether or not the affected steps will be opened/closed using
     *                      animations.
     * @return True if the navigation to the step was performed; false otherwise.
     */
    public synchronized boolean goToPreviousStep(boolean useAnimations) {
        return goToStep(getOpenStepPosition() - 1, useAnimations);
    }

    /**
     * If possible, goes to the specified step, closing the currently open one and opening the
     * target one.
     * Please note that, unless allowNonLinearNavigation is set to true, it will only be possible to
     * navigate to a certain step if all the previous ones are marked as completed.
     * In case the navigation is possible and the specified position to go to is the last one + 1,
     * the form will attempt to complete.
     *
     * @param stepPosition The step position to go to. If it is the next one to the actual last one,
     *                     the form will attempt to complete.
     * @param useAnimations Indicates whether or not the affected steps will be opened/closed using
     *                      animations.
     * @return True if the navigation to the step was performed; false otherwise.
     */
    public synchronized boolean goToStep(int stepPosition, boolean useAnimations) {
        if (formCompleted) {
            return false;
        }

        int openStepPosition = getOpenStepPosition();
        if (openStepPosition != stepPosition && stepPosition >= 0 && stepPosition <= stepHelpers.size()) {
            boolean previousStepsAreCompleted = areAllPreviousStepsCompleted(stepPosition);
            if ((style.allowNonLinearNavigation && stepPosition < stepHelpers.size()) || previousStepsAreCompleted) {
                openStep(stepPosition, useAnimations);

                return true;
            }
        }

        return false;
    }

    /**
     * Gets the position of the currently open step.
     *
     * @return The position of the currently open step, counting from 0. -1 if not found.
     */
    public synchronized int getOpenStepPosition() {
        for (int i = 0; i < stepHelpers.size(); i++) {
            StepHelper stepHelper = stepHelpers.get(i);
            if (stepHelper.getStepInstance().isOpen()) {
                return i;
            }
        }

        return -1;
    }

    /**
     * Gets the currently open step.
     *
     * @return The currently open step, or null if not found.
     */
    public synchronized Step getOpenStep() {
        for (int i = 0; i < stepHelpers.size(); i++) {
            StepHelper stepHelper = stepHelpers.get(i);
            if (stepHelper.getStepInstance().isOpen()) {
                return stepHelper.getStepInstance();
            }
        }

        return null;
    }

    /**
     * Gets the content layout of the specified step (i.e., the layout which was provided at start
     * to setup the step).
     *
     * @param stepPosition The step position.
     * @return If found, the layout. If not, null.
     */
    public View getStepContentLayout(int stepPosition) {
        if (stepPosition >= 0 && stepPosition < stepHelpers.size()) {
            return stepHelpers.get(stepPosition).getStepInstance().getContentLayout();
        }

        return null;
    }

    /**
     * Shows the bottom navigation bar.
     */
    public void showBottomNavigation() {
        bottomNavigationView.setVisibility(View.VISIBLE);
    }

    /**
     * Hides the bottom navigation bar.
     */
    public void hideBottomNavigation() {
        bottomNavigationView.setVisibility(View.GONE);
    }

    /**
     * Scrolls to the top of the specified step, but only in case its content is not visible.
     *
     * @param stepPosition The step position.
     * @param smoothScroll Determines whether the scrolling should be smooth or abrupt.
     */
    public void scrollToStepIfNecessary(final int stepPosition, final boolean smoothScroll) {
        if (stepPosition >= 0 && stepPosition < stepHelpers.size()) {
            stepsScrollView.post(new Runnable() {
                public void run() {
                    Step stepInstance = stepHelpers.get(stepPosition).getStepInstance();
                    View stepEntireLayout = stepInstance.getEntireStepLayout();
                    View stepContentLayout = stepInstance.getContentLayout();
                    Rect scrollBounds = new Rect();
                    stepsScrollView.getDrawingRect(scrollBounds);
                    if (stepContentLayout == null || scrollBounds.top > stepContentLayout.getY()) {
                        if (smoothScroll) {
                            stepsScrollView.smoothScrollTo(0, stepEntireLayout.getTop());
                        } else {
                            stepsScrollView.scrollTo(0, stepEntireLayout.getTop());
                        }
                    }
                }
            });
        }
    }

    /**
     * Scrolls to the top of the currently open step, but only in case its content is not visible.
     *
     * @param smoothScroll Determines whether the scrolling should be smooth or abrupt.
     */
    public synchronized void scrollToOpenStepIfNecessary(boolean smoothScroll) {
        scrollToStepIfNecessary(getOpenStepPosition(), smoothScroll);
    }

    /**
     * If all the steps are currently marked as completed, completes the form, disabling the step
     * navigation and the button(s) of the last step, and invoking onCompletedForm() on the listener.
     * To revert these changes (for example, because saving or sending the data has failed and you
     * want the form to go back to normal so the user can use it), call
     * cancelFormCompletionOrCancellationAttempt().
     */
    public void completeForm() {
        attemptToCompleteForm(false);
    }

    /**
     * Cancels the form, disabling the step navigation and the button(s) of the currently open step,
     * and invoking onCancelledForm() on the listener.
     * To revert these changes (for example, because the user has dismissed the cancellation and you
     * want the form to go back to normal), call cancelFormCompletionOrCancellationAttempt().
     */
    public void cancelForm() {
        attemptToCompleteForm(true);
    }

    /**
     * To be used after a failed form completion attempt or after a dismissed cancellation attempt,
     * this method re-activates the navigation to other steps and re-enables the button(s) of the
     * currently open step.
     * Useful when saving the form data fails and you want to allow the user to use the form again
     * in order to re-send the data.
     */
    public synchronized void cancelFormCompletionOrCancellationAttempt() {
        if (!formCompleted) {
            return;
        }

        int openedStepPosition = getOpenStepPosition();
        openedStepPosition = openedStepPosition == -1 ? stepHelpers.size() - 1 : openedStepPosition;
        StepHelper stepHelper = stepHelpers.get(openedStepPosition);

        if (style.closeLastStepOnCompletion) {
            Step step = stepHelper.getStepInstance();
            if (!step.isOpen()) {
                step.openInternal(true);
            }
        }

        if ((openedStepPosition + 1) < stepHelpers.size() || areAllStepsCompleted()) {
            stepHelper.enableAllButtons();
        } else {
            stepHelper.enableCancelButton();
        }

        formCompleted = false;
        updateBottomNavigationButtons();
    }

    /**
     * Refreshes the progress bar of the bottom navigation depending on the number of steps marked
     * as completed, returning the number of completed steps.
     *
     * @return The number of steps that are currently marked as completed.
     */
    public int refreshFormProgress() {
        int numberOfCompletedSteps = 0;
        for (int i = 0; i < stepHelpers.size(); i++) {
            if (stepHelpers.get(i).getStepInstance().isCompleted()) {
                ++numberOfCompletedSteps;
            }
        }
        setProgress(numberOfCompletedSteps);

        return numberOfCompletedSteps;
    }

    /**
     * Gets the total number of steps of the form.
     *
     * @return The total number of steps, including the confirmation step, if any.
     */
    public int getTotalNumberOfSteps() {
        return stepHelpers.size();
    }

    private void onConstructed(Context context, AttributeSet attrs, int defStyleAttr) {
        LayoutInflater inflater = LayoutInflater.from(context);
        inflater.inflate(R.layout.vertical_stepper_form_layout, this, true);

        keyboardTogglingObserver = new KeyboardTogglingObserver();

        style = new FormStyle();

        // Set the default values for all the style properties
        style.stepNextButtonText =
                getResources().getString(R.string.vertical_stepper_form_continue_button);
        style.lastStepNextButtonText =
                getResources().getString(R.string.vertical_stepper_form_confirm_button);
        style.lastStepCancelButtonText =
                getResources().getString(R.string.vertical_stepper_form_cancel_button);
        style.confirmationStepTitle =
                getResources().getString(R.string.vertical_stepper_form_confirmation_step_title);
        style.confirmationStepSubtitle = "";
        style.leftCircleSizeInPx =
                getResources().getDimensionPixelSize(R.dimen.vertical_stepper_form_width_circle);
        style.leftCircleTextSizeInPx =
                getResources().getDimensionPixelSize(R.dimen.vertical_stepper_form_text_size_circle);
        style.stepTitleTextSizeInPx =
                getResources().getDimensionPixelSize(R.dimen.vertical_stepper_form_text_size_title);
        style.stepSubtitleTextSizeInPx =
                getResources().getDimensionPixelSize(R.dimen.vertical_stepper_form_text_size_subtitle);
        style.stepErrorMessageTextSizeInPx =
                getResources().getDimensionPixelSize(R.dimen.vertical_stepper_form_text_size_error_message);
        style.leftVerticalLineThicknessSizeInPx =
                getResources().getDimensionPixelSize(R.dimen.vertical_stepper_form_width_vertical_line);
        style.marginFromStepNumbersToContentInPx =
                getResources().getDimensionPixelSize(R.dimen.vertical_stepper_form_space_between_numbers_and_content);
        style.backgroundColorOfDisabledElements =
                ContextCompat.getColor(context, R.color.vertical_stepper_form_background_color_disabled_elements);
        style.stepNumberBackgroundColor =
                ContextCompat.getColor(context, R.color.vertical_stepper_form_background_color_circle);
        style.nextButtonBackgroundColor =
                ContextCompat.getColor(context, R.color.vertical_stepper_form_background_color_next_button);
        style.nextButtonPressedBackgroundColor =
                ContextCompat.getColor(context, R.color.vertical_stepper_form_background_color_next_button_pressed);
        style.lastStepCancelButtonBackgroundColor =
                ContextCompat.getColor(context, R.color.vertical_stepper_form_background_color_cancel_button);
        style.lastStepCancelButtonPressedBackgroundColor =
                ContextCompat.getColor(context, R.color.vertical_stepper_form_background_color_cancel_button_pressed);
        style.stepNumberTextColor =
                ContextCompat.getColor(context, R.color.vertical_stepper_form_text_color_circle);
        style.stepTitleTextColor =
                ContextCompat.getColor(context, R.color.vertical_stepper_form_text_color_title);
        style.stepSubtitleTextColor =
                ContextCompat.getColor(context, R.color.vertical_stepper_form_text_color_subtitle);
        style.nextButtonTextColor =
                ContextCompat.getColor(context, R.color.vertical_stepper_form_text_color_next_button);
        style.nextButtonPressedTextColor =
                ContextCompat.getColor(context, R.color.vertical_stepper_form_text_color_next_button_pressed);
        style.lastStepCancelButtonTextColor =
                ContextCompat.getColor(context, R.color.vertical_stepper_form_text_color_cancel_button);
        style.lastStepCancelButtonPressedTextColor =
                ContextCompat.getColor(context, R.color.vertical_stepper_form_text_color_cancel_button_pressed);
        style.errorMessageTextColor =
                ContextCompat.getColor(context, R.color.vertical_stepper_form_text_color_error_message);
        style.bottomNavigationBackgroundColor =
                ContextCompat.getColor(context, R.color.vertical_stepper_form_background_color_bottom_navigation);
        style.displayBottomNavigation = true;
        style.displayStepButtons = true;
        style.displayCancelButtonInLastStep = false;
        style.displayStepDataInSubtitleOfClosedSteps = true;
        style.displayDifferentBackgroundColorOnDisabledElements = false;
        style.includeConfirmationStep = true;
        style.allowNonLinearNavigation = false;
        style.allowStepOpeningOnHeaderClick = true;
        style.closeLastStepOnCompletion = false;
        style.alphaOfDisabledElements = 0.3f;

        // Try to get the user values for the style properties to replace the default ones
        TypedArray vars;
        if (attrs != null) {
            vars = context.getTheme().obtainStyledAttributes(
                    attrs,
                    R.styleable.VerticalStepperFormView,
                    defStyleAttr,
                    0);

            if (vars.hasValue(R.styleable.VerticalStepperFormView_form_next_button_text)) {
                style.stepNextButtonText = vars.getString(
                        R.styleable.VerticalStepperFormView_form_next_button_text);
            }
            if (vars.hasValue(R.styleable.VerticalStepperFormView_form_last_button_text)) {
                style.lastStepNextButtonText = vars.getString(
                        R.styleable.VerticalStepperFormView_form_last_button_text);
            }
            if (vars.hasValue(R.styleable.VerticalStepperFormView_form_cancel_button_text)) {
                style.lastStepCancelButtonText = vars.getString(
                        R.styleable.VerticalStepperFormView_form_cancel_button_text);
            }
            if (vars.hasValue(R.styleable.VerticalStepperFormView_form_confirmation_step_title_text)) {
                style.confirmationStepTitle = vars.getString(
                        R.styleable.VerticalStepperFormView_form_confirmation_step_title_text);
            }
            if (vars.hasValue(R.styleable.VerticalStepperFormView_form_confirmation_step_subtitle_text)) {
                style.confirmationStepSubtitle = vars.getString(
                        R.styleable.VerticalStepperFormView_form_confirmation_step_subtitle_text);
            }
            style.leftCircleSizeInPx = vars.getDimensionPixelSize(
                    R.styleable.VerticalStepperFormView_form_circle_size,
                    style.leftCircleSizeInPx);
            style.leftCircleTextSizeInPx = vars.getDimensionPixelSize(
                    R.styleable.VerticalStepperFormView_form_circle_text_size,
                    style.leftCircleTextSizeInPx);
            style.stepTitleTextSizeInPx = vars.getDimensionPixelSize(
                    R.styleable.VerticalStepperFormView_form_title_text_size,
                    style.stepTitleTextSizeInPx);
            style.stepSubtitleTextSizeInPx = vars.getDimensionPixelSize(
                    R.styleable.VerticalStepperFormView_form_subtitle_text_size,
                    style.stepSubtitleTextSizeInPx);
            style.stepErrorMessageTextSizeInPx = vars.getDimensionPixelSize(
                    R.styleable.VerticalStepperFormView_form_error_message_text_size,
                    style.stepErrorMessageTextSizeInPx);
            style.leftVerticalLineThicknessSizeInPx = vars.getDimensionPixelSize(
                    R.styleable.VerticalStepperFormView_form_vertical_line_width,
                    style.leftVerticalLineThicknessSizeInPx);
            style.marginFromStepNumbersToContentInPx = vars.getDimensionPixelSize(
                    R.styleable.VerticalStepperFormView_form_horizontal_margin_from_step_numbers_to_content,
                    style.marginFromStepNumbersToContentInPx);
            style.backgroundColorOfDisabledElements = vars.getColor(
                    R.styleable.VerticalStepperFormView_form_disabled_elements_background_color,
                    style.backgroundColorOfDisabledElements);
            style.stepNumberBackgroundColor = vars.getColor(
                    R.styleable.VerticalStepperFormView_form_circle_background_color,
                    style.stepNumberBackgroundColor);
            style.nextButtonBackgroundColor = vars.getColor(
                    R.styleable.VerticalStepperFormView_form_next_button_background_color,
                    style.nextButtonBackgroundColor);
            style.nextButtonPressedBackgroundColor = vars.getColor(
                    R.styleable.VerticalStepperFormView_form_next_button_pressed_background_color,
                    style.nextButtonPressedBackgroundColor);
            style.lastStepCancelButtonBackgroundColor = vars.getColor(
                    R.styleable.VerticalStepperFormView_form_cancel_button_background_color,
                    style.lastStepCancelButtonBackgroundColor);
            style.lastStepCancelButtonPressedBackgroundColor = vars.getColor(
                    R.styleable.VerticalStepperFormView_form_cancel_button_pressed_background_color,
                    style.lastStepCancelButtonPressedBackgroundColor);
            style.stepNumberTextColor = vars.getColor(
                    R.styleable.VerticalStepperFormView_form_circle_text_color,
                    style.stepNumberTextColor);
            style.stepTitleTextColor = vars.getColor(
                    R.styleable.VerticalStepperFormView_form_title_text_color,
                    style.stepTitleTextColor);
            style.stepSubtitleTextColor = vars.getColor(
                    R.styleable.VerticalStepperFormView_form_subtitle_text_color,
                    style.stepSubtitleTextColor);
            style.nextButtonTextColor = vars.getColor(
                    R.styleable.VerticalStepperFormView_form_next_button_text_color,
                    style.nextButtonTextColor);
            style.nextButtonPressedTextColor = vars.getColor(
                    R.styleable.VerticalStepperFormView_form_next_button_pressed_text_color,
                    style.nextButtonPressedTextColor);
            style.lastStepCancelButtonTextColor = vars.getColor(
                    R.styleable.VerticalStepperFormView_form_cancel_button_text_color,
                    style.lastStepCancelButtonTextColor);
            style.lastStepCancelButtonPressedTextColor = vars.getColor(
                    R.styleable.VerticalStepperFormView_form_cancel_button_pressed_text_color,
                    style.lastStepCancelButtonPressedTextColor);
            style.errorMessageTextColor = vars.getColor(
                    R.styleable.VerticalStepperFormView_form_error_message_text_color,
                    style.errorMessageTextColor);
            style.bottomNavigationBackgroundColor = vars.getColor(
                    R.styleable.VerticalStepperFormView_form_bottom_navigation_background_color,
                    style.bottomNavigationBackgroundColor);
            style.displayBottomNavigation = vars.getBoolean(
                    R.styleable.VerticalStepperFormView_form_display_bottom_navigation,
                    style.displayBottomNavigation);
            style.displayStepButtons = vars.getBoolean(
                    R.styleable.VerticalStepperFormView_form_display_step_buttons,
                    style.displayStepButtons);
            style.displayCancelButtonInLastStep = vars.getBoolean(
                    R.styleable.VerticalStepperFormView_form_display_cancel_button_in_last_step,
                    style.displayCancelButtonInLastStep);
            style.displayStepDataInSubtitleOfClosedSteps = vars.getBoolean(
                    R.styleable.VerticalStepperFormView_form_display_step_data_in_subtitle_of_closed_steps,
                    style.displayStepDataInSubtitleOfClosedSteps);
            style.displayDifferentBackgroundColorOnDisabledElements = vars.getBoolean(
                    R.styleable.VerticalStepperFormView_form_display_different_background_color_on_disabled_elements,
                    style.displayDifferentBackgroundColorOnDisabledElements);
            style.includeConfirmationStep = vars.getBoolean(
                    R.styleable.VerticalStepperFormView_form_include_confirmation_step,
                    style.includeConfirmationStep);
            style.allowNonLinearNavigation = vars.getBoolean(
                    R.styleable.VerticalStepperFormView_form_allow_non_linear_navigation,
                    style.allowNonLinearNavigation);
            style.allowStepOpeningOnHeaderClick = vars.getBoolean(
                    R.styleable.VerticalStepperFormView_form_allow_step_opening_on_header_click,
                    style.allowStepOpeningOnHeaderClick);
            style.closeLastStepOnCompletion = vars.getBoolean(
                    R.styleable.VerticalStepperFormView_form_close_last_step_on_completion,
                    style.closeLastStepOnCompletion);
            style.alphaOfDisabledElements = vars.getFloat(
                    R.styleable.VerticalStepperFormView_form_alpha_of_disabled_elements,
                    style.alphaOfDisabledElements);

            vars.recycle();
        }

        internalListener = new FormStepListener();
    }

    void initializeForm(StepperFormListener listener, StepHelper[] stepsArray) {
        this.listener = listener;
        this.stepHelpers = Arrays.asList(stepsArray);

        progressBar.setMax(stepHelpers.size());

        bottomNavigationView.setBackgroundColor(style.bottomNavigationBackgroundColor);
        if (!style.displayBottomNavigation) {
            hideBottomNavigation();
        }

        for (int i = 0; i < stepHelpers.size(); i++) {
            View stepLayout = initializeStepHelper(i);
            formContentView.addView(stepLayout);
        }

        goToStep(0, false);

        initialized = true;
    }

    private View initializeStepHelper(int position) {
        StepHelper stepHelper = stepHelpers.get(position);
        boolean isLast = (position + 1) == stepHelpers.size();
        int stepLayoutResourceId = getStepLayoutResourceId(position, isLast);

        return stepHelper.initialize(this, formContentView, stepLayoutResourceId, position, isLast);
    }

    @LayoutRes
    protected int getStepLayoutResourceId(int position, boolean isLast) {
        // This could be overridden to use a custom step layout
        return R.layout.step_layout;
    }

    private synchronized void openStep(int stepToOpenPosition, boolean useAnimations) {
        if (stepToOpenPosition >= 0 && stepToOpenPosition < stepHelpers.size()) {

            int stepToClosePosition = getOpenStepPosition();
            if (stepToClosePosition != -1) {
                StepHelper stepToClose = stepHelpers.get(stepToClosePosition);
                stepToClose.getStepInstance().closeInternal(useAnimations);
            }

            StepHelper stepToOpen = stepHelpers.get(stepToOpenPosition);
            stepToOpen.getStepInstance().openInternal(useAnimations);
        } else if (stepToOpenPosition == stepHelpers.size()) {
            attemptToCompleteForm(false);
        }
    }

    protected synchronized void updateBottomNavigationButtons() {
        int stepPosition = getOpenStepPosition();
        if (stepPosition >= 0 && stepPosition < stepHelpers.size()) {
            StepHelper stepHelper = stepHelpers.get(stepPosition);

            if (!formCompleted && stepPosition > 0) {
                enablePreviousButtonInBottomNavigation();
            } else {
                disablePreviousButtonInBottomNavigation();
            }

            if (!formCompleted
                    && (stepPosition + 1) < stepHelpers.size()
                    && (style.allowNonLinearNavigation || stepHelper.getStepInstance().isCompleted())) {
                enableNextButtonInBottomNavigation();
            } else {
                disableNextButtonInBottomNavigation();
            }
        }
    }

    protected void disablePreviousButtonInBottomNavigation() {
        disableBottomButtonNavigation(previousStepButton);
    }

    protected void enablePreviousButtonInBottomNavigation() {
        enableBottomButtonNavigation(previousStepButton);
    }

    protected void disableNextButtonInBottomNavigation() {
        disableBottomButtonNavigation(nextStepButton);
    }

    protected void enableNextButtonInBottomNavigation() {
        enableBottomButtonNavigation(nextStepButton);
    }

    private void enableBottomButtonNavigation(View button) {
        button.setAlpha(1f);
        button.setEnabled(true);
    }

    private void disableBottomButtonNavigation(View button) {
        button.setAlpha(style.alphaOfDisabledElements);
        button.setEnabled(false);
    }

    private void setProgress(int numberOfCompletedSteps) {
        if (numberOfCompletedSteps >= 0 && numberOfCompletedSteps <= stepHelpers.size()) {
            progressBar.setProgress(numberOfCompletedSteps);
        }
    }

    private void enableOrDisableLastStepNextButton() {
        if (!areAllStepsCompleted()) {
            stepHelpers.get(stepHelpers.size() - 1).disableNextButton();
        } else {
            stepHelpers.get(stepHelpers.size() - 1).enableNextButton();
        }
    }

    private synchronized void attemptToCompleteForm(boolean isCancellation) {
        if (formCompleted) {
            return;
        }

        // If the last step is a confirmation step that happens to be marked as uncompleted,
        // here we attempt to mark it as completed so the form can be completed
        boolean markedConfirmationStepAsCompleted = false;
        String confirmationStepErrorMessage = "";
        StepHelper lastStepHelper = stepHelpers.get(stepHelpers.size() - 1);
        Step lastStep = lastStepHelper.getStepInstance();
        if (!isCancellation) {
            if (!lastStep.isCompleted() && lastStepHelper.isConfirmationStep()) {
                confirmationStepErrorMessage = lastStep.getErrorMessage();
                lastStep.markAsCompletedOrUncompleted(true);
                if (lastStep.isCompleted()) {
                    markedConfirmationStepAsCompleted = true;
                }
            }
        }

        int openStepPosition = getOpenStepPosition();
        if (openStepPosition >= 0 && openStepPosition < stepHelpers.size() && (isCancellation || areAllStepsCompleted())) {
            formCompleted = true;
            stepHelpers.get(openStepPosition).disableAllButtons();
            updateBottomNavigationButtons();

            if (listener != null) {
                if (!isCancellation) {
                    listener.onCompletedForm();
                } else {
                    listener.onCancelledForm();
                }
            }
        } else if (markedConfirmationStepAsCompleted) {
            // If the completion attempt fails, we restore the confirmation step to its previous state
            lastStep.markAsUncompleted(confirmationStepErrorMessage, true);
        }

        if (!isCancellation && style.closeLastStepOnCompletion) {
            lastStep.closeInternal(true);
        }
    }

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

        findViews();
        registerListeners();
    }

    private void findViews() {
        formContentView = findViewById(R.id.content);
        stepsScrollView = findViewById(R.id.steps_scroll);
        progressBar = findViewById(R.id.progress_bar);
        previousStepButton = findViewById(R.id.down_previous);
        nextStepButton = findViewById(R.id.down_next);
        bottomNavigationView = findViewById(R.id.bottom_navigation);
    }

    private void registerListeners() {
        previousStepButton.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View view) {
                goToPreviousStep(true);
            }
        });
        nextStepButton.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View view) {
                goToNextStep(true);
            }
        });

        addObserverForKeyboard();
    }

    @Override
    protected void onDetachedFromWindow() {
        removeObserverForKeyboard();

        super.onDetachedFromWindow();
    }

    private void addObserverForKeyboard() {
        keyboardIsOpen = isKeyboardOpen();
        getRootView().getViewTreeObserver().addOnGlobalLayoutListener(keyboardTogglingObserver);
    }

    private void removeObserverForKeyboard() {
        getRootView().getViewTreeObserver().removeOnGlobalLayoutListener(keyboardTogglingObserver);
    }

    private boolean isKeyboardOpen() {
        Rect r = new Rect();
        formContentView.getWindowVisibleDisplayFrame(r);
        int screenHeight = formContentView.getRootView().getHeight();
        int keyboardHeight = screenHeight - r.bottom;

        return keyboardHeight > screenHeight * 0.2;
    }

    private void restoreFromState(
            int positionToOpen,
            boolean[] completedSteps,
            String[] titles,
            String[] subtitles,
            String[] buttonTexts,
            String[] errorMessages,
            boolean formCompleted) {

        for (int i = 0; i < completedSteps.length; i++) {
            StepHelper stepHelper = stepHelpers.get(i);

            stepHelper.getStepInstance().updateTitle(titles[i], false);
            stepHelper.getStepInstance().updateSubtitle(subtitles[i], false);
            stepHelper.getStepInstance().updateNextButtonText(buttonTexts[i], false);
            if (completedSteps[i]) {
                stepHelper.getStepInstance().markAsCompleted(false);
            } else {
                stepHelper.getStepInstance().markAsUncompleted(errorMessages[i], false);
            }
        }

        goToStep(positionToOpen, false);

        if (formCompleted) {
            this.formCompleted = true;
            stepHelpers.get(getOpenStepPosition()).disableAllButtons();
            updateBottomNavigationButtons();
        }

        refreshFormProgress();
    }

    @Override
    public Parcelable onSaveInstanceState() {
        Bundle bundle = new Bundle();

        boolean[] completedSteps = new boolean[stepHelpers.size()];
        String[] titles = new String[stepHelpers.size()];
        String[] subtitles = new String[stepHelpers.size()];
        String[] buttonTexts = new String[stepHelpers.size()];
        String[] errorMessages = new String[stepHelpers.size()];
        for (int i = 0; i < completedSteps.length; i++) {
            StepHelper stepHelper = stepHelpers.get(i);
            completedSteps[i] = stepHelper.getStepInstance().isCompleted();
            titles[i] = stepHelper.getStepInstance().getTitle();
            subtitles[i] = stepHelper.getStepInstance().getSubtitle();
            buttonTexts[i] = stepHelper.getStepInstance().getNextButtonText();
            if (!stepHelper.getStepInstance().isCompleted()) {
                errorMessages[i] = stepHelper.getStepInstance().getErrorMessage();
            }
        }

        bundle.putParcelable("superState", super.onSaveInstanceState());
        bundle.putInt("openStep", this.getOpenStepPosition());
        bundle.putBooleanArray("completedSteps", completedSteps);
        bundle.putStringArray("titles", titles);
        bundle.putStringArray("subtitles", subtitles);
        bundle.putStringArray("buttonTexts", buttonTexts);
        bundle.putStringArray("errorMessages", errorMessages);
        bundle.putBoolean("formCompleted", formCompleted);

        return bundle;
    }

    @Override
    public void onRestoreInstanceState(Parcelable state) {
        if (state instanceof Bundle) {
            Bundle bundle = (Bundle) state;

            boolean formCompleted = bundle.getBoolean("formCompleted");
            String[] errorMessages = bundle.getStringArray("errorMessages");
            String[] buttonTexts = bundle.getStringArray("buttonTexts");
            String[] subtitles = bundle.getStringArray("subtitles");
            String[] titles = bundle.getStringArray("titles");
            boolean[] completedSteps = bundle.getBooleanArray("completedSteps");
            int positionToOpen = bundle.getInt("openStep");
            state = bundle.getParcelable("superState");

            restoreFromState(
                    positionToOpen,
                    completedSteps,
                    titles,
                    subtitles,
                    buttonTexts,
                    errorMessages,
                    formCompleted);
        }
        super.onRestoreInstanceState(state);
    }

    class FormStepListener implements Step.InternalFormStepListener {

        @Override
        public void onUpdatedTitle(int stepPosition, boolean useAnimations) {
            // No need to do anything here
        }

        @Override
        public void onUpdatedSubtitle(int stepPosition, boolean useAnimations) {
            // No need to do anything here
        }

        @Override
        public void onUpdatedButtonText(int stepPosition, boolean useAnimations) {
            // No need to do anything here
        }

        @Override
        public void onUpdatedErrorMessage(int stepPosition, boolean useAnimations) {
            // No need to do anything here
        }

        @Override
        public void onUpdatedStepCompletionState(int stepPosition, boolean useAnimations) {
            updateBottomNavigationButtons();
            refreshFormProgress();
            enableOrDisableLastStepNextButton();
        }

        @Override
        public void onUpdatedStepVisibility(int stepPosition, boolean useAnimations) {
            updateBottomNavigationButtons();
            scrollToOpenStepIfNecessary(useAnimations);
            enableOrDisableLastStepNextButton();
        }
    }

    class FormStyle {
        String stepNextButtonText;
        String lastStepNextButtonText;
        String lastStepCancelButtonText;
        String confirmationStepTitle;
        String confirmationStepSubtitle;
        int leftCircleSizeInPx;
        int leftCircleTextSizeInPx;
        int stepTitleTextSizeInPx;
        int stepSubtitleTextSizeInPx;
        int stepErrorMessageTextSizeInPx;
        int leftVerticalLineThicknessSizeInPx;
        int marginFromStepNumbersToContentInPx;
        int backgroundColorOfDisabledElements;
        int stepNumberBackgroundColor;
        int nextButtonBackgroundColor;
        int nextButtonPressedBackgroundColor;
        int lastStepCancelButtonBackgroundColor;
        int lastStepCancelButtonPressedBackgroundColor;
        int stepNumberTextColor;
        int stepTitleTextColor;
        int stepSubtitleTextColor;
        int nextButtonTextColor;
        int nextButtonPressedTextColor;
        int lastStepCancelButtonTextColor;
        int lastStepCancelButtonPressedTextColor;
        int errorMessageTextColor;
        int bottomNavigationBackgroundColor;
        boolean displayBottomNavigation;
        boolean displayStepButtons;
        boolean displayCancelButtonInLastStep;
        boolean displayStepDataInSubtitleOfClosedSteps;
        boolean displayDifferentBackgroundColorOnDisabledElements;
        boolean includeConfirmationStep;
        boolean allowNonLinearNavigation;
        boolean allowStepOpeningOnHeaderClick;
        boolean closeLastStepOnCompletion;
        float alphaOfDisabledElements;
    }

    private class KeyboardTogglingObserver implements ViewTreeObserver.OnGlobalLayoutListener {

        @Override
        public void onGlobalLayout() {
            boolean keyboardWasOpen = keyboardIsOpen;
            keyboardIsOpen = isKeyboardOpen();
            if (initialized && keyboardIsOpen != keyboardWasOpen) {
                scrollToOpenStepIfNecessary(true);
            }
        }
    }
}