/*
 * Copyright (C) 2019 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 androidx.constraintlayout.core.state;

import static androidx.constraintlayout.core.widgets.ConstraintWidget.CHAIN_PACKED;
import static androidx.constraintlayout.core.widgets.ConstraintWidget.CHAIN_SPREAD;
import static androidx.constraintlayout.core.widgets.ConstraintWidget.CHAIN_SPREAD_INSIDE;

import androidx.annotation.NonNull;
import androidx.constraintlayout.core.state.helpers.AlignHorizontallyReference;
import androidx.constraintlayout.core.state.helpers.AlignVerticallyReference;
import androidx.constraintlayout.core.state.helpers.BarrierReference;
import androidx.constraintlayout.core.state.helpers.FlowReference;
import androidx.constraintlayout.core.state.helpers.GridReference;
import androidx.constraintlayout.core.state.helpers.GuidelineReference;
import androidx.constraintlayout.core.state.helpers.HorizontalChainReference;
import androidx.constraintlayout.core.state.helpers.VerticalChainReference;
import androidx.constraintlayout.core.widgets.ConstraintWidget;
import androidx.constraintlayout.core.widgets.ConstraintWidgetContainer;
import androidx.constraintlayout.core.widgets.HelperWidget;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;

/**
 * Represents a full state of a ConstraintLayout
 */
public class State {
    private CorePixelDp mDpToPixel;
    private boolean mIsLtr = true;
    protected HashMap<Object, Reference> mReferences = new HashMap<>();
    protected HashMap<Object, HelperReference> mHelperReferences = new HashMap<>();
    HashMap<String, ArrayList<String>> mTags = new HashMap<>();

    static final int UNKNOWN = -1;
    static final int CONSTRAINT_SPREAD = 0;
    static final int CONSTRAINT_WRAP = 1;
    static final int CONSTRAINT_RATIO = 2;

    public static final Integer PARENT = 0;

    public final ConstraintReference mParent = new ConstraintReference(this);

    public enum Constraint {
        LEFT_TO_LEFT,
        LEFT_TO_RIGHT,
        RIGHT_TO_LEFT,
        RIGHT_TO_RIGHT,
        START_TO_START,
        START_TO_END,
        END_TO_START,
        END_TO_END,
        TOP_TO_TOP,
        TOP_TO_BOTTOM,
        TOP_TO_BASELINE,
        BOTTOM_TO_TOP,
        BOTTOM_TO_BOTTOM,
        BOTTOM_TO_BASELINE,
        BASELINE_TO_BASELINE,
        BASELINE_TO_TOP,
        BASELINE_TO_BOTTOM,
        CENTER_HORIZONTALLY,
        CENTER_VERTICALLY,
        CIRCULAR_CONSTRAINT
    }

    public enum Direction {
        LEFT,
        RIGHT,
        START,
        END,
        TOP,
        BOTTOM
    }

    public enum Helper {
        HORIZONTAL_CHAIN,
        VERTICAL_CHAIN,
        ALIGN_HORIZONTALLY,
        ALIGN_VERTICALLY,
        BARRIER,
        LAYER,
        HORIZONTAL_FLOW,
        VERTICAL_FLOW,
        GRID,
        ROW,
        COLUMN,
        FLOW
    }

    public enum Chain {
        SPREAD,
        SPREAD_INSIDE,
        PACKED;

        public static Map<String, Chain> chainMap = new HashMap<>();
        public static Map<String, Integer> valueMap = new HashMap<>();
        static {
            chainMap.put("packed", PACKED);
            chainMap.put("spread_inside", SPREAD_INSIDE);
            chainMap.put("spread", SPREAD);

            valueMap.put("packed", CHAIN_PACKED);
            valueMap.put("spread_inside", CHAIN_SPREAD_INSIDE);
            valueMap.put("spread", CHAIN_SPREAD);
        }

        /**
         * Get the Enum value with a String
         * @param str a String representation of a Enum value
         * @return a Enum value
         */
        public static int getValueByString(String str) {
            if (valueMap.containsKey(str)) {
                return valueMap.get(str);
            }
            return UNKNOWN;
        }

        /**
         * Get the actual int value with a String
         * @param str a String representation of a Enum value
         * @return an actual int value
         */
        public static Chain getChainByString(String str) {
            if (chainMap.containsKey(str)) {
                return chainMap.get(str);
            }
            return null;
        }

    }

    public enum Wrap {
        NONE,
        CHAIN,
        ALIGNED;
        public static Map<String, Wrap> wrapMap = new HashMap<>();
        public static Map<String, Integer> valueMap = new HashMap<>();
        static {
            wrapMap.put("none", NONE);
            wrapMap.put("chain", CHAIN);
            wrapMap.put("aligned", ALIGNED);

            valueMap.put("none", 0);
            valueMap.put("chain", 3); // Corresponds to CHAIN_NEW
            valueMap.put("aligned", 2);
        }

        /**
         * Get the actual int value with a String
         * @param str a String representation of a Enum value
         * @return a actual int value
         */
        public static int getValueByString(String str) {
            if (valueMap.containsKey(str)) {
                return valueMap.get(str);
            }
            return UNKNOWN;
        }

        /**
         * Get the Enum value with a String
         * @param str a String representation of a Enum value
         * @return a Enum value
         */
        public static Wrap getChainByString(String str) {
            if (wrapMap.containsKey(str)) {
                return wrapMap.get(str);
            }
            return null;
        }
    }

    public State() {
        mParent.setKey(PARENT);
        mReferences.put(PARENT, mParent);
    }

    CorePixelDp getDpToPixel() {
        return mDpToPixel;
    }

    /**
     * Set the function that converts dp to Pixels
     */
    public void setDpToPixel(CorePixelDp dpToPixel) {
        this.mDpToPixel = dpToPixel;
    }

    /**
     * Set whether the layout direction is left to right (Ltr).
     *
     * @deprecated For consistency, use {@link #setRtl(boolean)} instead.
     */
    @Deprecated
    public void setLtr(boolean isLtr) {
        mIsLtr = isLtr;
    }

    /**
     * Returns true if layout direction is left to right. False for right to left.
     *
     * @deprecated For consistency, use {@link #isRtl()} instead.
     */
    @Deprecated
    public boolean isLtr() {
        return mIsLtr;
    }

    /**
     * Set whether the layout direction is right to left (Rtl).
     */
    public void setRtl(boolean isRtl) {
        mIsLtr = !isRtl;
    }

    /**
     * Returns true if layout direction is right to left. False for left to right.
     */
    public boolean isRtl() {
        return !mIsLtr;
    }

    /**
     * Clear the state
     */
    public void reset() {
        for (Object ref : mReferences.keySet()) {
            mReferences.get(ref).getConstraintWidget().reset();
        }
        mReferences.clear();
        mReferences.put(PARENT, mParent);
        mHelperReferences.clear();
        mTags.clear();
        mBaselineNeeded.clear();
        mDirtyBaselineNeededWidgets = true;
    }

    /**
     * Implements a conversion function for values, returning int.
     * This can be used in case values (e.g. margins) are represented
     * via an object, not directly an int.
     *
     * @param value the object to convert from
     */
    public int convertDimension(Object value) {
        if (value instanceof Float) {
            return Math.round((Float) value);
        }
        if (value instanceof Integer) {
            return (Integer) value;
        }
        return 0;
    }

    /**
     * Create a new reference given a key.
     */
    public ConstraintReference createConstraintReference(Object key) {
        return new ConstraintReference(this);
    }

    // @TODO: add description
    public boolean sameFixedWidth(int width) {
        return mParent.getWidth().equalsFixedValue(width);
    }

    // @TODO: add description
    public boolean sameFixedHeight(int height) {
        return mParent.getHeight().equalsFixedValue(height);
    }

    // @TODO: add description
    public State width(Dimension dimension) {
        return setWidth(dimension);
    }

    // @TODO: add description
    public State height(Dimension dimension) {
        return setHeight(dimension);
    }

    // @TODO: add description
    public State setWidth(Dimension dimension) {
        mParent.setWidth(dimension);
        return this;
    }

    // @TODO: add description
    public State setHeight(Dimension dimension) {
        mParent.setHeight(dimension);
        return this;
    }

    Reference reference(Object key) {
        return mReferences.get(key);
    }

    // @TODO: add description
    public ConstraintReference constraints(Object key) {
        Reference reference = mReferences.get(key);

        if (reference == null) {
            reference = createConstraintReference(key);
            mReferences.put(key, reference);
            reference.setKey(key);
        }
        if (reference instanceof ConstraintReference) {
            return (ConstraintReference) reference;
        }
        return null;
    }

    private int mNumHelpers = 0;

    private String createHelperKey() {
        return "__HELPER_KEY_" + mNumHelpers++ + "__";
    }

    // @TODO: add description
    public HelperReference helper(Object key, State.Helper type) {
        if (key == null) {
            key = createHelperKey();
        }
        HelperReference reference = mHelperReferences.get(key);
        if (reference == null) {
            switch (type) {
                case HORIZONTAL_CHAIN: {
                    reference = new HorizontalChainReference(this);
                }
                break;
                case VERTICAL_CHAIN: {
                    reference = new VerticalChainReference(this);
                }
                break;
                case ALIGN_HORIZONTALLY: {
                    reference = new AlignHorizontallyReference(this);
                }
                break;
                case ALIGN_VERTICALLY: {
                    reference = new AlignVerticallyReference(this);
                }
                break;
                case BARRIER: {
                    reference = new BarrierReference(this);
                }
                break;
                case VERTICAL_FLOW:
                case HORIZONTAL_FLOW: {
                    reference = new FlowReference(this, type);
                }
                break;
                case GRID:
                case ROW:
                case COLUMN: {
                    reference = new GridReference(this, type);
                }
                    break;
                default: {
                    reference = new HelperReference(this, type);
                }
            }
            reference.setKey(key);
            mHelperReferences.put(key, reference);
        }
        return reference;
    }

    // @TODO: add description
    public GuidelineReference horizontalGuideline(Object key) {
        return guideline(key, ConstraintWidget.HORIZONTAL);
    }

    // @TODO: add description
    public GuidelineReference verticalGuideline(Object key) {
        return guideline(key, ConstraintWidget.VERTICAL);
    }

    // @TODO: add description
    public GuidelineReference guideline(Object key, int orientation) {
        ConstraintReference reference = constraints(key);
        if (reference.getFacade() == null
                || !(reference.getFacade() instanceof GuidelineReference)) {
            GuidelineReference guidelineReference = new GuidelineReference(this);
            guidelineReference.setOrientation(orientation);
            guidelineReference.setKey(key);
            reference.setFacade(guidelineReference);
        }
        return (GuidelineReference) reference.getFacade();
    }

    // @TODO: add description
    public BarrierReference barrier(Object key, Direction direction) {
        ConstraintReference reference = constraints(key);
        if (reference.getFacade() == null || !(reference.getFacade() instanceof BarrierReference)) {
            BarrierReference barrierReference = new BarrierReference(this);
            barrierReference.setBarrierDirection(direction);
            reference.setFacade(barrierReference);
        }
        return (BarrierReference) reference.getFacade();
    }

    /**
     * Get a Grid reference
     *
     * @param key name of the reference object
     * @param gridType type of Grid pattern - Grid, Row, or Column
     * @return a GridReference object
     */
    @NonNull
    public GridReference getGrid(@NonNull Object key, @NonNull String gridType) {
        ConstraintReference reference = constraints(key);
        if (reference.getFacade() == null || !(reference.getFacade() instanceof GridReference)) {
            State.Helper Type = Helper.GRID;
            if (gridType.charAt(0) == 'r') {
                Type = Helper.ROW;
            } else if (gridType.charAt(0) == 'c') {
                Type = Helper.COLUMN;
            }
            GridReference gridReference = new GridReference(this, Type);
            reference.setFacade(gridReference);
        }
        return (GridReference) reference.getFacade();
    }

    /**
     * Gets a reference to a Flow object. Creating it if needed.
     * @param key id of the reference
     * @param vertical is it a vertical or horizontal flow
     * @return a FlowReference
     */
    public FlowReference getFlow(Object key, boolean vertical) {
        ConstraintReference reference = constraints(key);
        if (reference.getFacade() == null || !(reference.getFacade() instanceof FlowReference)) {
            FlowReference flowReference =
                    vertical ? new FlowReference(this, Helper.VERTICAL_FLOW)
                            : new FlowReference(this, Helper.HORIZONTAL_FLOW);

            reference.setFacade(flowReference);
        }
        return (FlowReference) reference.getFacade();
    }

    // @TODO: add description
    public VerticalChainReference verticalChain() {
        return (VerticalChainReference) helper(null, Helper.VERTICAL_CHAIN);
    }

    // @TODO: add description
    public VerticalChainReference verticalChain(Object... references) {
        VerticalChainReference reference =
                (VerticalChainReference) helper(null, State.Helper.VERTICAL_CHAIN);
        reference.add(references);
        return reference;
    }

    // @TODO: add description
    public HorizontalChainReference horizontalChain() {
        return (HorizontalChainReference) helper(null, Helper.HORIZONTAL_CHAIN);
    }

    // @TODO: add description
    public HorizontalChainReference horizontalChain(Object... references) {
        HorizontalChainReference reference =
                (HorizontalChainReference) helper(null, Helper.HORIZONTAL_CHAIN);
        reference.add(references);
        return reference;
    }

    /**
     * Get a VerticalFlowReference
     *
     * @return a VerticalFlowReference
     */
    public FlowReference getVerticalFlow() {
        return (FlowReference) helper(null, Helper.VERTICAL_FLOW);
    }

    /**
     * Get a VerticalFlowReference and add it to references
     *
     * @param references where we add the VerticalFlowReference
     * @return a VerticalFlowReference
     */
    public FlowReference getVerticalFlow(Object... references) {
        FlowReference reference =
                (FlowReference) helper(null, State.Helper.VERTICAL_FLOW);
        reference.add(references);
        return reference;
    }

    /**
     * Get a HorizontalFlowReference
     *
     * @return a HorizontalFlowReference
     */
    public FlowReference getHorizontalFlow() {
        return (FlowReference) helper(null, Helper.HORIZONTAL_FLOW);
    }

    /**
     * Get a HorizontalFlowReference and add it to references
     *
     * @param references references where we the HorizontalFlowReference
     * @return a HorizontalFlowReference
     */
    public FlowReference getHorizontalFlow(Object... references) {
        FlowReference reference =
                (FlowReference) helper(null, Helper.HORIZONTAL_FLOW);
        reference.add(references);
        return reference;
    }

    // @TODO: add description
    public AlignHorizontallyReference centerHorizontally(Object... references) {
        AlignHorizontallyReference reference =
                (AlignHorizontallyReference) helper(null, Helper.ALIGN_HORIZONTALLY);
        reference.add(references);
        return reference;
    }

    // @TODO: add description
    public AlignVerticallyReference centerVertically(Object... references) {
        AlignVerticallyReference reference =
                (AlignVerticallyReference) helper(null, Helper.ALIGN_VERTICALLY);
        reference.add(references);
        return reference;
    }

    // @TODO: add description
    public void directMapping() {
        for (Object key : mReferences.keySet()) {
            Reference ref = constraints(key);
            if (!(ref instanceof ConstraintReference)) {
                continue;
            }
            ConstraintReference reference = (ConstraintReference) ref;
            reference.setView(key);
        }
    }

    // @TODO: add description
    public void map(Object key, Object view) {
        ConstraintReference ref = constraints(key);
        if (ref != null) {
            ref.setView(view);
        }
    }

    // @TODO: add description
    public void setTag(String key, String tag) {
        Reference ref = constraints(key);
        if (ref instanceof ConstraintReference) {
            ConstraintReference reference = (ConstraintReference) ref;
            reference.setTag(tag);
            ArrayList<String> list = null;
            if (!mTags.containsKey(tag)) {
                list = new ArrayList<>();
                mTags.put(tag, list);
            } else {
                list = mTags.get(tag);
            }
            list.add(key);
        }
    }

    // @TODO: add description
    public ArrayList<String> getIdsForTag(String tag) {
        if (mTags.containsKey(tag)) {
            return mTags.get(tag);
        }
        return null;
    }

    // @TODO: add description
    public void apply(ConstraintWidgetContainer container) {
        container.removeAllChildren();
        mParent.getWidth().apply(this, container, ConstraintWidget.HORIZONTAL);
        mParent.getHeight().apply(this, container, ConstraintWidget.VERTICAL);
        // add helper references
        for (Object key : mHelperReferences.keySet()) {
            HelperReference reference = mHelperReferences.get(key);
            HelperWidget helperWidget = reference.getHelperWidget();
            if (helperWidget != null) {
                Reference constraintReference = mReferences.get(key);
                if (constraintReference == null) {
                    constraintReference = constraints(key);
                }
                constraintReference.setConstraintWidget(helperWidget);
            }
        }
        for (Object key : mReferences.keySet()) {
            Reference reference = mReferences.get(key);
            if (reference != mParent && reference.getFacade() instanceof HelperReference) {
                HelperWidget helperWidget =
                        ((HelperReference) reference.getFacade()).getHelperWidget();
                if (helperWidget != null) {
                    Reference constraintReference = mReferences.get(key);
                    if (constraintReference == null) {
                        constraintReference = constraints(key);
                    }
                    constraintReference.setConstraintWidget(helperWidget);
                }
            }
        }
        for (Object key : mReferences.keySet()) {
            Reference reference = mReferences.get(key);
            if (reference != mParent) {
                ConstraintWidget widget = reference.getConstraintWidget();
                widget.setDebugName(reference.getKey().toString());
                widget.setParent(null);
                if (reference.getFacade() instanceof GuidelineReference) {
                    // we apply Guidelines first to correctly setup their ConstraintWidget.
                    reference.apply();
                }
                container.add(widget);
            } else {
                reference.setConstraintWidget(container);
            }
        }
        for (Object key : mHelperReferences.keySet()) {
            // We need this pass to apply chains properly
            HelperReference reference = mHelperReferences.get(key);
            HelperWidget helperWidget = reference.getHelperWidget();
            if (helperWidget != null) {
                for (Object keyRef : reference.mReferences) {
                    Reference constraintReference = mReferences.get(keyRef);
                    reference.getHelperWidget().add(constraintReference.getConstraintWidget());
                }
                reference.apply();
            } else {
                reference.apply();
            }
        }
        for (Object key : mReferences.keySet()) {
            Reference reference = mReferences.get(key);
            if (reference != mParent && reference.getFacade() instanceof HelperReference) {
                HelperReference helperReference = (HelperReference) reference.getFacade();
                HelperWidget helperWidget = helperReference.getHelperWidget();
                if (helperWidget != null) {
                    for (Object keyRef : helperReference.mReferences) {
                        Reference constraintReference = mReferences.get(keyRef);
                        if (constraintReference != null) {
                            helperWidget.add(constraintReference.getConstraintWidget());
                        } else if (keyRef instanceof Reference) {
                            helperWidget.add(((Reference) keyRef).getConstraintWidget());
                        } else {
                            System.out.println("couldn't find reference for " + keyRef);
                        }
                    }
                    reference.apply();
                }
            }
        }
        for (Object key : mReferences.keySet()) {
            Reference reference = mReferences.get(key);
            reference.apply();
            ConstraintWidget widget = reference.getConstraintWidget();
            if (widget != null && key != null) {
                widget.stringId = key.toString();
            }
        }
    }

    // ================= add baseline code================================
    ArrayList<Object> mBaselineNeeded = new ArrayList<>();
    ArrayList<ConstraintWidget> mBaselineNeededWidgets = new ArrayList<>();
    boolean mDirtyBaselineNeededWidgets = true;

    /**
     * Baseline is needed for this object
     */
    public void baselineNeededFor(Object id) {
        mBaselineNeeded.add(id);
        mDirtyBaselineNeededWidgets = true;
    }

    /**
     * Does this constraintWidget need a baseline
     *
     * @return true if the constraintWidget needs a baseline
     */
    public boolean isBaselineNeeded(ConstraintWidget constraintWidget) {
        if (mDirtyBaselineNeededWidgets) {
            mBaselineNeededWidgets.clear();
            for (Object id : mBaselineNeeded) {
                ConstraintWidget widget = mReferences.get(id).getConstraintWidget();
                if (widget != null) mBaselineNeededWidgets.add(widget);
            }

            mDirtyBaselineNeededWidgets = false;
        }
        return mBaselineNeededWidgets.contains(constraintWidget);
    }
}
