/*
 * Copyright 2023 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.wear.protolayout.renderer.helper;

import static java.util.Arrays.stream;
import static java.util.stream.Collectors.toList;

import androidx.annotation.Nullable;
import androidx.wear.protolayout.expression.proto.DynamicProto.DynamicString;
import androidx.wear.protolayout.expression.proto.FixedProto.FixedString;
import androidx.wear.protolayout.proto.AlignmentProto.HorizontalAlignment;
import androidx.wear.protolayout.proto.AlignmentProto.HorizontalAlignmentProp;
import androidx.wear.protolayout.proto.AlignmentProto.VerticalAlignment;
import androidx.wear.protolayout.proto.AlignmentProto.VerticalAlignmentProp;
import androidx.wear.protolayout.proto.ColorProto.ColorProp;
import androidx.wear.protolayout.proto.DimensionProto.ContainerDimension;
import androidx.wear.protolayout.proto.DimensionProto.DegreesProp;
import androidx.wear.protolayout.proto.DimensionProto.DpProp;
import androidx.wear.protolayout.proto.DimensionProto.ImageDimension;
import androidx.wear.protolayout.proto.DimensionProto.SpProp;
import androidx.wear.protolayout.proto.DimensionProto.SpacerDimension;
import androidx.wear.protolayout.proto.FingerprintProto.NodeFingerprint;
import androidx.wear.protolayout.proto.FingerprintProto.TreeFingerprint;
import androidx.wear.protolayout.proto.LayoutElementProto;
import androidx.wear.protolayout.proto.LayoutElementProto.Arc;
import androidx.wear.protolayout.proto.LayoutElementProto.ArcAdapter;
import androidx.wear.protolayout.proto.LayoutElementProto.ArcLayoutElement;
import androidx.wear.protolayout.proto.LayoutElementProto.ArcText;
import androidx.wear.protolayout.proto.LayoutElementProto.Box;
import androidx.wear.protolayout.proto.LayoutElementProto.Column;
import androidx.wear.protolayout.proto.LayoutElementProto.Image;
import androidx.wear.protolayout.proto.LayoutElementProto.Layout;
import androidx.wear.protolayout.proto.LayoutElementProto.LayoutElement;
import androidx.wear.protolayout.proto.LayoutElementProto.Row;
import androidx.wear.protolayout.proto.LayoutElementProto.Spacer;
import androidx.wear.protolayout.proto.LayoutElementProto.Span;
import androidx.wear.protolayout.proto.LayoutElementProto.SpanText;
import androidx.wear.protolayout.proto.LayoutElementProto.Spannable;
import androidx.wear.protolayout.proto.LayoutElementProto.Text;
import androidx.wear.protolayout.proto.ModifiersProto;
import androidx.wear.protolayout.proto.TypesProto.BoolProp;
import androidx.wear.protolayout.proto.TypesProto.Int32Prop;
import androidx.wear.protolayout.proto.TypesProto.StringProp;

import java.util.List;
import java.util.Objects;
import java.util.function.Consumer;

/**
 * A simple DSL for more easily producing layout protos in tests.
 *
 * <p>Example usage:
 *
 * <pre>{@code
 * layout(
 *   column(
 *     props -> {
 *       props.heightDp = 20;
 *       props.modifiers.border.widthDp = 2;
 *     },
 *     row(
 *       text("Foo"),
 *       text("Bar")
 *     )
 *   )
 * )
 * }</pre>
 */
public class TestDsl {

    private TestDsl() {}

    /** An intermediate opaque layout node produced by the builders in this class. */
    public static final class LayoutNode {
        private LayoutElement.Builder mLayoutElement;
        private ArcLayoutElement.Builder mArcLayoutElement;
        private Span.Builder mSpanElement;
        private NodeFingerprint mFingerprint;
    }

    /** Corresponds to {@link ModifiersProto.Border} */
    public static final class Border {
        public int widthDp;
        public int colorArgb;

        private ModifiersProto.Border toProto() {
            return ModifiersProto.Border.newBuilder()
                    .setWidth(DpProp.newBuilder().setValue(widthDp))
                    .setColor(ColorProp.newBuilder().setArgb(colorArgb))
                    .build();
        }
    }

    /** Corresponds to {@link ModifiersProto.Modifiers} */
    public static final class Modifiers {
        public Border border = new Border();

        private ModifiersProto.Modifiers toProto() {
            ModifiersProto.Modifiers.Builder proto = ModifiersProto.Modifiers.newBuilder();
            proto.setBorder(border.toProto());
            return proto.build();
        }
    }

    /** Corresponds to {@link LayoutElementProto.FontStyle} */
    public static final class FontStyle {
        public float sizeSp;
        public boolean italic;
        public int colorArgb;

        private LayoutElementProto.FontStyle toProto() {
            return LayoutElementProto.FontStyle.newBuilder()
                    .setSize(sp(sizeSp))
                    .setItalic(bool(italic))
                    .setColor(color(colorArgb))
                    .build();
        }
    }

    /** Properties of a Box, with each field directly accessible for ease of use. */
    public static final class BoxProps {
        public Modifiers modifiers = new Modifiers();
        public int widthDp;
        public int heightDp;
        public HorizontalAlignment horizontalAlignment;
        public VerticalAlignment verticalAlignment;

        private void applyTo(Box.Builder box) {
            box.setModifiers(modifiers.toProto());
            box.setWidth(dpContainerDim(widthDp));
            box.setHeight(dpContainerDim(heightDp));
            box.setHorizontalAlignment(
                    HorizontalAlignmentProp.newBuilder().setValue(horizontalAlignment).build());
            box.setVerticalAlignment(
                    VerticalAlignmentProp.newBuilder().setValue(verticalAlignment).build());
        }

        private int fingerprint() {
            return Objects.hash(
                    modifiers.toProto(), widthDp, heightDp, horizontalAlignment, verticalAlignment);
        }
    }

    /** Properties of a Row, with each field directly accessible for ease of use. */
    public static final class RowProps {
        public Modifiers modifiers = new Modifiers();
        public int widthDp;
        public int heightDp;

        private void applyTo(Row.Builder row) {
            row.setModifiers(modifiers.toProto());
            row.setWidth(dpContainerDim(widthDp));
            row.setHeight(dpContainerDim(heightDp));
        }

        private int fingerprint() {
            return Objects.hash(modifiers.toProto(), widthDp, heightDp);
        }
    }

    /** Properties of a Column, with each field directly accessible for ease of use. */
    public static final class ColumnProps {
        public Modifiers modifiers = new Modifiers();
        public int widthDp;
        public int heightDp;

        private void applyTo(Column.Builder column) {
            column.setModifiers(modifiers.toProto());
            column.setWidth(dpContainerDim(widthDp));
            column.setHeight(dpContainerDim(heightDp));
        }

        private int fingerprint() {
            return Objects.hash(modifiers.toProto(), widthDp, heightDp);
        }
    }

    /** Properties of a Text object, with each field directly accessible for ease of use. */
    public static final class TextProps {
        public Modifiers modifiers = new Modifiers();
        public int maxLines = 1;
        public float lineHeightSp = 1;
        public FontStyle fontStyle = new FontStyle();

        private void applyTo(Text.Builder text) {
            text.setModifiers(modifiers.toProto());
            text.setMaxLines(int32(maxLines));
            text.setLineHeight(sp(lineHeightSp));
            text.setFontStyle(fontStyle.toProto());
        }

        private int fingerprint() {
            return Objects.hash(modifiers.toProto(), maxLines, fontStyle.toProto());
        }
    }

    /** Properties of a Image object, with each field directly accessible for ease of use. */
    public static final class ImageProps {
        public Modifiers modifiers = new Modifiers();
        public int widthDp;
        public int heightDp;

        private void applyTo(Image.Builder image) {
            image.setModifiers(modifiers.toProto());
            image.setWidth(dpImageDim(widthDp));
            image.setHeight(dpImageDim(heightDp));
        }

        private int fingerprint() {
            return Objects.hash(modifiers.toProto(), widthDp, heightDp);
        }
    }

    /** Properties of a Spacer object, with each field directly accessible for ease of use. */
    public static final class SpacerProps {
        public Modifiers modifiers = new Modifiers();
        public int widthDp;
        public int heightDp;

        private void applyTo(Spacer.Builder spacer) {
            spacer.setModifiers(modifiers.toProto());
            spacer.setWidth(dpSpacerDim(widthDp));
            spacer.setHeight(dpSpacerDim(heightDp));
        }

        private int fingerprint() {
            return Objects.hash(modifiers.toProto(), widthDp, heightDp);
        }
    }

    /** Properties of an Arc, with each field directly accessible for ease of use. */
    public static final class ArcProps {
        public float anchorAngleDegrees;

        private void applyTo(Arc.Builder arc) {
            arc.setAnchorAngle(degrees(anchorAngleDegrees));
        }

        private int fingerprint() {
            return Float.hashCode(anchorAngleDegrees);
        }
    }

    /** Properties of an Spannable, with each field directly accessible for ease of use. */
    public static final class SpannableProps {
        public int maxLines;

        private void applyTo(Spannable.Builder spannable) {
            spannable.setMaxLines(int32(maxLines));
        }

        private int fingerprint() {
            return Float.hashCode(maxLines);
        }
    }

    public static Layout layout(LayoutNode root) {
        return Layout.newBuilder()
                .setRoot(root.mLayoutElement)
                .setFingerprint(TreeFingerprint.newBuilder().setRoot(root.mFingerprint))
                .build();
    }

    public static LayoutNode box(Consumer<BoxProps> propsConsumer, LayoutNode... nodes) {
        return boxInternal(propsConsumer, nodes);
    }

    public static LayoutNode box(LayoutNode... nodes) {
        return boxInternal(/* propsConsumer= */ null, nodes);
    }

    private static LayoutNode boxInternal(
            @Nullable Consumer<BoxProps> propsConsumer, LayoutNode... nodes) {
        LayoutNode element = new LayoutNode();
        Box.Builder builder = Box.newBuilder().addAllContents(linearContents(nodes));
        int selfPropsFingerprint = 0;
        if (propsConsumer != null) {
            BoxProps props = new BoxProps();
            propsConsumer.accept(props);
            props.applyTo(builder);
            selfPropsFingerprint = combine(selfPropsFingerprint, props.fingerprint());
        }
        element.mLayoutElement = LayoutElement.newBuilder().setBox(builder.build());
        element.mFingerprint = fingerprint("box", selfPropsFingerprint, nodes);
        return element;
    }

    public static LayoutNode row(Consumer<RowProps> propsConsumer, LayoutNode... nodes) {
        return rowInternal(propsConsumer, nodes);
    }

    public static LayoutNode row(LayoutNode... nodes) {
        return rowInternal(/* propsConsumer= */ null, nodes);
    }

    private static LayoutNode rowInternal(
            @Nullable Consumer<RowProps> propsConsumer, LayoutNode... nodes) {
        LayoutNode element = new LayoutNode();
        Row.Builder builder = Row.newBuilder().addAllContents(linearContents(nodes));
        int selfPropsFingerprint = 0;
        if (propsConsumer != null) {
            RowProps props = new RowProps();
            propsConsumer.accept(props);
            props.applyTo(builder);
            selfPropsFingerprint = combine(selfPropsFingerprint, props.fingerprint());
        }
        element.mLayoutElement = LayoutElement.newBuilder().setRow(builder.build());
        element.mFingerprint = fingerprint("row", selfPropsFingerprint, nodes);
        return element;
    }

    public static LayoutNode column(Consumer<ColumnProps> propsConsumer, LayoutNode... nodes) {
        return columnInternal(propsConsumer, nodes);
    }

    public static LayoutNode column(LayoutNode... nodes) {
        return columnInternal(/* propsConsumer= */ null, nodes);
    }

    private static LayoutNode columnInternal(
            @Nullable Consumer<ColumnProps> propsConsumer, LayoutNode... nodes) {
        LayoutNode element = new LayoutNode();
        Column.Builder builder = Column.newBuilder().addAllContents(linearContents(nodes));
        int selfPropsFingerprint = 0;
        if (propsConsumer != null) {
            ColumnProps props = new ColumnProps();
            propsConsumer.accept(props);
            props.applyTo(builder);
            selfPropsFingerprint = combine(selfPropsFingerprint, props.fingerprint());
        }
        element.mLayoutElement = LayoutElement.newBuilder().setColumn(builder.build());
        element.mFingerprint = fingerprint("column", selfPropsFingerprint, nodes);
        return element;
    }

    public static LayoutNode dynamicFixedText(String fixedText) {
        return dynamicFixedText(/* propsConsumer= */ null, fixedText);
    }

    public static LayoutNode dynamicFixedText(Consumer<TextProps> propsConsumer, String fixedText) {
        return textInternal(propsConsumer, dynamicStr(fixedText));
    }

    public static LayoutNode text(String text) {
        return text(/* propsConsumer= */ null, text);
    }

    public static LayoutNode text(Consumer<TextProps> propsConsumer, String text) {
        return textInternal(propsConsumer, str(text));
    }

    private static LayoutNode textInternal(
            @Nullable Consumer<TextProps> propsConsumer, StringProp text) {
        LayoutNode element = new LayoutNode();
        Text.Builder builder = Text.newBuilder().setText(text);
        int selfPropsFingerprint = text.hashCode();
        if (propsConsumer != null) {
            TextProps props = new TextProps();
            propsConsumer.accept(props);
            props.applyTo(builder);
            selfPropsFingerprint = combine(selfPropsFingerprint, props.fingerprint());
        }
        element.mLayoutElement = LayoutElement.newBuilder().setText(builder.build());
        element.mFingerprint = fingerprint("text", selfPropsFingerprint);
        return element;
    }

    public static LayoutNode image(Consumer<ImageProps> propsConsumer, String resourceId) {
        return imageInternal(propsConsumer, resourceId);
    }

    public static LayoutNode image(String resourceId) {
        return imageInternal(/* propsConsumer= */ null, resourceId);
    }

    private static LayoutNode imageInternal(
            @Nullable Consumer<ImageProps> propsConsumer, String resourceId) {
        LayoutNode element = new LayoutNode();
        Image.Builder builder = Image.newBuilder().setResourceId(str(resourceId));
        int selfPropsFingerprint = resourceId.hashCode();
        if (propsConsumer != null) {
            ImageProps props = new ImageProps();
            propsConsumer.accept(props);
            props.applyTo(builder);
            selfPropsFingerprint = combine(selfPropsFingerprint, props.fingerprint());
        }
        element.mLayoutElement = LayoutElement.newBuilder().setImage(builder.build());
        element.mFingerprint = fingerprint("image", selfPropsFingerprint);
        return element;
    }

    public static LayoutNode spacer(Consumer<SpacerProps> propsConsumer) {
        return spacerInternal(propsConsumer);
    }

    public static LayoutNode spacer() {
        return spacerInternal(/* propsConsumer= */ null);
    }

    private static LayoutNode spacerInternal(@Nullable Consumer<SpacerProps> propsConsumer) {
        LayoutNode element = new LayoutNode();
        Spacer.Builder builder = Spacer.newBuilder();
        int selfPropsFingerprint = 0;
        if (propsConsumer != null) {
            SpacerProps props = new SpacerProps();
            propsConsumer.accept(props);
            props.applyTo(builder);
            selfPropsFingerprint = props.fingerprint();
        }
        element.mLayoutElement = LayoutElement.newBuilder().setSpacer(builder.build());
        element.mFingerprint = fingerprint("spacer", selfPropsFingerprint);
        return element;
    }

    public static LayoutNode arc(Consumer<ArcProps> propsConsumer, LayoutNode... nodes) {
        return arcInternal(propsConsumer, nodes);
    }

    public static LayoutNode arc(LayoutNode... nodes) {
        return arcInternal(/* propsConsumer= */ null, nodes);
    }

    public static LayoutNode arcAdapter(LayoutNode layoutNode) {
        return arcAdapterInternal(layoutNode);
    }

    private static LayoutNode arcInternal(
            @Nullable Consumer<ArcProps> propsConsumer, LayoutNode... nodes) {
        LayoutNode element = new LayoutNode();
        Arc.Builder builder = Arc.newBuilder().addAllContents(radialContents(nodes));
        int selfPropsFingerprint = 0;
        if (propsConsumer != null) {
            ArcProps props = new ArcProps();
            propsConsumer.accept(props);
            props.applyTo(builder);
            selfPropsFingerprint = combine(selfPropsFingerprint, props.fingerprint());
        }
        element.mLayoutElement = LayoutElement.newBuilder().setArc(builder.build());
        element.mFingerprint = fingerprint("arc", selfPropsFingerprint, nodes);
        return element;
    }

    private static LayoutNode arcAdapterInternal(LayoutNode node) {
        LayoutNode element = new LayoutNode();
        ArcAdapter.Builder builder = ArcAdapter.newBuilder().setContent(node.mLayoutElement);
        int selfPropsFingerprint = 0;
        element.mArcLayoutElement = ArcLayoutElement.newBuilder().setAdapter(builder.build());
        element.mFingerprint = fingerprint("arcAdapter", selfPropsFingerprint, node);
        return element;
    }

    public static LayoutNode arcText(String text) {
        LayoutNode element = new LayoutNode();
        element.mArcLayoutElement =
                ArcLayoutElement.newBuilder().setText(ArcText.newBuilder().setText(str(text)));
        element.mFingerprint = fingerprint("arcText", text.hashCode());
        return element;
    }

    public static LayoutNode spannable(
            Consumer<SpannableProps> propsConsumer, LayoutNode... nodes) {
        return spannableInternal(propsConsumer, nodes);
    }

    public static LayoutNode spannable(LayoutNode... nodes) {
        return spannableInternal(/* propsConsumer= */ null, nodes);
    }

    private static LayoutNode spannableInternal(
            @Nullable Consumer<SpannableProps> propsConsumer, LayoutNode... nodes) {
        LayoutNode element = new LayoutNode();
        Spannable.Builder builder = Spannable.newBuilder().addAllSpans(spanContents(nodes));
        int selfPropsFingerprint = 0;
        if (propsConsumer != null) {
            SpannableProps props = new SpannableProps();
            propsConsumer.accept(props);
            props.applyTo(builder);
            selfPropsFingerprint = combine(selfPropsFingerprint, props.fingerprint());
        }
        // A Spannable is *not* considered a container for diffing purposes (i.e. any updated
        // children will cause the entire Spannable to be updated). So we include the fingerprint of
        // the nodes in the Spannable's self fingerprint. This mirrors behaviour from
        // Spannable::Builder::addSpan at http://shortn/_cUyrG0M1N2
        selfPropsFingerprint = combine(selfPropsFingerprint, fingerprints(nodes).hashCode());
        element.mLayoutElement = LayoutElement.newBuilder().setSpannable(builder.build());
        element.mFingerprint = fingerprint("spannable", selfPropsFingerprint);
        return element;
    }

    public static LayoutNode spanText(String text) {
        LayoutNode element = new LayoutNode();
        element.mSpanElement = Span.newBuilder().setText(SpanText.newBuilder().setText(str(text)));
        element.mFingerprint = fingerprint("spanText", text.hashCode());
        return element;
    }

    private static NodeFingerprint fingerprint(
            String selfTypeName, int selfPropsValue, LayoutNode... nodes) {
        return NodeFingerprint.newBuilder()
                .setSelfTypeValue(selfTypeName.hashCode())
                .setSelfPropsValue(selfPropsValue)
                .setChildNodesValue(fingerprints(nodes).hashCode())
                .addAllChildNodes(fingerprints(nodes))
                .build();
    }

    private static int combine(int fingerprint1, int fingerprint2) {
        return 31 * fingerprint1 + fingerprint2;
    }

    private static List<LayoutElement> linearContents(LayoutNode[] nodes) {
        return stream(nodes).map(n -> n.mLayoutElement.build()).collect(toList());
    }

    private static List<NodeFingerprint> fingerprints(LayoutNode[] nodes) {
        return stream(nodes).map(n -> n.mFingerprint).collect(toList());
    }

    private static List<ArcLayoutElement> radialContents(LayoutNode[] nodes) {
        return stream(nodes).map(n -> n.mArcLayoutElement.build()).collect(toList());
    }

    private static List<Span> spanContents(LayoutNode[] nodes) {
        return stream(nodes).map(n -> n.mSpanElement.build()).collect(toList());
    }

    private static ContainerDimension dpContainerDim(float dp) {
        return ContainerDimension.newBuilder().setLinearDimension(dp(dp)).build();
    }

    private static ImageDimension dpImageDim(float dp) {
        return ImageDimension.newBuilder().setLinearDimension(dp(dp)).build();
    }

    private static SpacerDimension dpSpacerDim(float dp) {
        return SpacerDimension.newBuilder().setLinearDimension(dp(dp)).build();
    }

    private static ColorProp color(int value) {
        return ColorProp.newBuilder().setArgb(value).build();
    }

    private static BoolProp bool(boolean value) {
        return BoolProp.newBuilder().setValue(value).build();
    }

    private static DpProp dp(float value) {
        return DpProp.newBuilder().setValue(value).build();
    }

    private static SpProp sp(float value) {
        return SpProp.newBuilder().setValue(value).build();
    }

    private static DegreesProp degrees(float degrees) {
        return DegreesProp.newBuilder().setValue(degrees).build();
    }

    private static Int32Prop int32(int value) {
        return Int32Prop.newBuilder().setValue(value).build();
    }

    private static StringProp str(String value) {
        return StringProp.newBuilder().setValue(value).build();
    }

    private static StringProp dynamicStr(String fixedValue) {
        return StringProp.newBuilder()
                .setDynamicValue(
                        DynamicString.newBuilder()
                                .setFixed(FixedString.newBuilder().setValue(fixedValue)))
                .build();
    }
}
