/*
 * Copyright (C) 2010 The Android Open Source Project
 *
 * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.ide.common.layout;

import static com.android.ide.common.layout.LayoutConstants.ANDROID_URI;
import static com.android.ide.common.layout.LayoutConstants.ATTR_ID;
import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_HEIGHT;
import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_WIDTH;
import static com.android.ide.common.layout.LayoutConstants.ATTR_ORIENTATION;
import static com.android.ide.common.layout.LayoutConstants.VALUE_HORIZONTAL;
import static com.android.ide.common.layout.LayoutConstants.VALUE_VERTICAL;

import com.android.ide.common.api.DropFeedback;
import com.android.ide.common.api.IAttributeInfo.Format;
import com.android.ide.common.api.IDragElement;
import com.android.ide.common.api.IMenuCallback;
import com.android.ide.common.api.INode;
import com.android.ide.common.api.IViewRule;
import com.android.ide.common.api.Point;
import com.android.ide.common.api.Rect;
import com.android.ide.common.api.RuleAction;
import com.android.ide.common.api.RuleAction.NestedAction;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Locale;

/** Test the {@link LinearLayoutRule} */
public class LinearLayoutRuleTest extends LayoutTestBase {
    // Utility for other tests
    protected void dragIntoEmpty(Rect dragBounds) {
        boolean haveBounds = dragBounds.isValid();

        IViewRule rule = new LinearLayoutRule();

        INode targetNode = TestNode.create("android.widget.LinearLayout").id(
        "@+id/LinearLayout01").bounds(new Rect(0, 0, 240, 480));
        Point dropPoint = new Point(10, 5);

        IDragElement[] elements = TestDragElement.create(TestDragElement.create(
                "android.widget.Button", dragBounds).id("@+id/Button01"));

        // Enter target
        DropFeedback feedback = rule.onDropEnter(targetNode, null/*targetView*/, elements);
        assertNotNull(feedback);
        assertFalse(feedback.invalidTarget);
        assertNotNull(feedback.painter);

        feedback = rule.onDropMove(targetNode, elements, feedback, dropPoint);
        assertNotNull(feedback);
        assertFalse(feedback.invalidTarget);

        // Paint feedback and make sure it's what we expect
        TestGraphics graphics = new TestGraphics();
        assertNotNull(feedback.painter);
        feedback.painter.paint(graphics, targetNode, feedback);
        assertEquals(
                // Expect to see a recipient rectangle around the bounds of the
                // LinearLayout,
                // as well as a single vertical line as a drop preview located
                // along the left
                // edge (for this horizontal linear layout) showing insert
                // position at index 0,
                // and finally a rectangle for the bounds of the inserted button
                // centered over
                // the middle
                "[useStyle(DROP_RECIPIENT), "
                        +
                        // Bounds rectangle
                        "drawRect(Rect[0,0,240,480]), "
                        + "useStyle(DROP_ZONE), drawLine(1,0,1,480), "
                        + "useStyle(DROP_ZONE_ACTIVE), " + "useStyle(DROP_PREVIEW), " +
                        // Insert position line
                        "drawLine(1,0,1,480)" + (haveBounds ?
                        // Outline of dragged node centered over position line
                        ", useStyle(DROP_PREVIEW), " + "drawRect(1,0,101,80)"
                                // Nothing when we don't have bounds
                                : "") + "]", graphics.getDrawn().toString());

        // Attempt a drop
        assertEquals(0, targetNode.getChildren().length);
        rule.onDropped(targetNode, elements, feedback, dropPoint);
        assertEquals(1, targetNode.getChildren().length);
        assertEquals("@+id/Button01", targetNode.getChildren()[0].getStringAttr(
                ANDROID_URI, ATTR_ID));
    }

    // Utility for other tests
    protected INode dragInto(boolean vertical, Rect dragBounds, Point dragPoint,
            int insertIndex, int currentIndex,
            String... graphicsFragments) {
        INode linearLayout = TestNode.create("android.widget.LinearLayout").id(
                "@+id/LinearLayout01").bounds(new Rect(0, 0, 240, 480)).set(ANDROID_URI,
                ATTR_ORIENTATION,
                vertical ? VALUE_VERTICAL : VALUE_HORIZONTAL)
                .add(
                        TestNode.create("android.widget.Button").id("@+id/Button01").bounds(
                                new Rect(0, 0, 100, 80)),
                        TestNode.create("android.widget.Button").id("@+id/Button02").bounds(
                                new Rect(0, 100, 100, 80)),
                        TestNode.create("android.widget.Button").id("@+id/Button03").bounds(
                                new Rect(0, 200, 100, 80)),
                        TestNode.create("android.widget.Button").id("@+id/Button04").bounds(
                                new Rect(0, 300, 100, 80)));

        return super.dragInto(new LinearLayoutRule(), linearLayout, dragBounds, dragPoint, null,
                insertIndex, currentIndex, graphicsFragments);
    }

    // Check that the context menu registers the expected menu items
    public void testContextMenu() {
        LinearLayoutRule rule = new LinearLayoutRule();
        initialize(rule, "android.widget.LinearLayout");
        INode node = TestNode.create("android.widget.Button").id("@+id/Button012");

        List<RuleAction> contextMenu = new ArrayList<RuleAction>();
        rule.addContextMenuActions(contextMenu, node);
        assertEquals(6, contextMenu.size());
        assertEquals("Edit ID...", contextMenu.get(0).getTitle());
        assertTrue(contextMenu.get(1) instanceof RuleAction.Separator);
        assertEquals("Layout Width", contextMenu.get(2).getTitle());
        assertEquals("Layout Height", contextMenu.get(3).getTitle());
        assertTrue(contextMenu.get(4) instanceof RuleAction.Separator);
        assertEquals("Other Properties", contextMenu.get(5).getTitle());

        RuleAction propertiesMenu = contextMenu.get(5);
        assertTrue(propertiesMenu.getClass().getName(),
                propertiesMenu instanceof NestedAction);
    }

    public void testContextMenuCustom() {
        LinearLayoutRule rule = new LinearLayoutRule();
        initialize(rule, "android.widget.LinearLayout");
        INode node = TestNode.create("android.widget.LinearLayout").id("@+id/LinearLayout")
            .set(ANDROID_URI, ATTR_LAYOUT_WIDTH, "42dip")
            .set(ANDROID_URI, ATTR_LAYOUT_HEIGHT, "50sp");

        List<RuleAction> contextMenu = new ArrayList<RuleAction>();
        rule.addContextMenuActions(contextMenu, node);
        assertEquals(6, contextMenu.size());
        assertEquals("Layout Width", contextMenu.get(2).getTitle());
        RuleAction menuAction = contextMenu.get(2);
        assertTrue(menuAction instanceof RuleAction.Choices);
        RuleAction.Choices choices = (RuleAction.Choices) menuAction;
        List<String> titles = choices.getTitles();
        List<String> ids = choices.getIds();
        assertEquals("Wrap Content", titles.get(0));
        assertEquals("wrap_content", ids.get(0));
        assertEquals("Match Parent", titles.get(1));
        assertEquals("match_parent", ids.get(1));
        assertEquals("42dip", titles.get(2));
        assertEquals("42dip", ids.get(2));
        assertEquals("42dip", choices.getCurrent());
    }

    // Check that the context menu manipulates the orientation attribute
    public void testOrientation() {
        LinearLayoutRule rule = new LinearLayoutRule();
        initialize(rule, "android.widget.LinearLayout");
        TestNode node = TestNode.create("android.widget.LinearLayout").id("@+id/LinearLayout012");
        node.putAttributeInfo(ANDROID_URI, "orientation",
                new TestAttributeInfo(ATTR_ORIENTATION, new Format[] { Format.ENUM },
                        "android.widget.LinearLayout",
                        new String[] {"horizontal", "vertical"}, null, null));

        assertNull(node.getStringAttr(ANDROID_URI, ATTR_ORIENTATION));

        List<RuleAction> contextMenu = new ArrayList<RuleAction>();
        rule.addContextMenuActions(contextMenu, node);
        assertEquals(7, contextMenu.size());
        RuleAction orientationAction = contextMenu.get(1);
        assertEquals("Orientation", orientationAction.getTitle());

        assertTrue(orientationAction.getClass().getName(),
                orientationAction instanceof RuleAction.Choices);

        RuleAction.Choices choices = (RuleAction.Choices) orientationAction;
        IMenuCallback callback = choices.getCallback();
        callback.action(orientationAction, Collections.singletonList(node), VALUE_VERTICAL, true);

        String orientation = node.getStringAttr(ANDROID_URI,
                ATTR_ORIENTATION);
        assertEquals(VALUE_VERTICAL, orientation);
        callback.action(orientationAction, Collections.singletonList(node), VALUE_HORIZONTAL,
                true);
        orientation = node.getStringAttr(ANDROID_URI, ATTR_ORIENTATION);
        assertEquals(VALUE_HORIZONTAL, orientation);
    }

    // Check that the context menu manipulates the orientation attribute
    public void testProperties() {
        LinearLayoutRule rule = new LinearLayoutRule();
        initialize(rule, "android.widget.LinearLayout");
        TestNode node = TestNode.create("android.widget.LinearLayout").id("@+id/LinearLayout012");
        node.putAttributeInfo(ANDROID_URI, "orientation",
                new TestAttributeInfo(ATTR_ORIENTATION, new Format[] { Format.ENUM },
                        "android.widget.LinearLayout",
                        new String[] {"horizontal", "vertical"}, null, null));
        node.setAttributeSources(Arrays.asList("android.widget.LinearLayout",
                "android.view.ViewGroup", "android.view.View"));
        node.putAttributeInfo(ANDROID_URI, "gravity",
                new TestAttributeInfo("gravity", new Format[] { Format.INTEGER },
                        "android.widget.LinearLayout", null, null, null));


        assertNull(node.getStringAttr(ANDROID_URI, ATTR_ORIENTATION));

        List<RuleAction> contextMenu = new ArrayList<RuleAction>();
        rule.addContextMenuActions(contextMenu, node);
        assertEquals(8, contextMenu.size());

        assertEquals("Orientation", contextMenu.get(1).getTitle());
        assertEquals("Edit Gravity...", contextMenu.get(2).getTitle());

        assertEquals("Other Properties", contextMenu.get(7).getTitle());

        RuleAction propertiesMenu = contextMenu.get(7);
        assertTrue(propertiesMenu.getClass().getName(),
                propertiesMenu instanceof NestedAction);
        NestedAction nested = (NestedAction) propertiesMenu;
        List<RuleAction> nestedActions = nested.getNestedActions(node);
        assertEquals(9, nestedActions.size());
        assertEquals("Recent", nestedActions.get(0).getTitle());
        assertTrue(nestedActions.get(1) instanceof RuleAction.Separator);
        assertEquals("Defined by LinearLayout", nestedActions.get(2).getTitle());
        assertEquals("Inherited from ViewGroup", nestedActions.get(3).getTitle());
        assertEquals("Inherited from View", nestedActions.get(4).getTitle());
        assertTrue(nestedActions.get(5) instanceof RuleAction.Separator);
        assertEquals("Layout Parameters", nestedActions.get(6).getTitle());
        assertTrue(nestedActions.get(7) instanceof RuleAction.Separator);
        assertEquals("All By Name", nestedActions.get(8).getTitle());

        BaseViewRule.editedProperty(ATTR_ORIENTATION);

        RuleAction recentAction = nestedActions.get(0);
        assertTrue(recentAction instanceof NestedAction);
        NestedAction recentChoices = (NestedAction) recentAction;
        List<RuleAction> recentItems = recentChoices.getNestedActions(node);

        assertEquals(1, recentItems.size());
        assertEquals("Orientation", recentItems.get(0).getTitle());

        BaseViewRule.editedProperty("gravity");
        recentItems = recentChoices.getNestedActions(node);
        assertEquals(2, recentItems.size());
        assertEquals("Gravity...", recentItems.get(0).getTitle());
        assertEquals("Orientation", recentItems.get(1).getTitle());

        BaseViewRule.editedProperty(ATTR_ORIENTATION);
        recentItems = recentChoices.getNestedActions(node);
        assertEquals(2, recentItems.size());
        assertEquals("Orientation", recentItems.get(0).getTitle());
        assertEquals("Gravity...", recentItems.get(1).getTitle());

        // Lots of other properties -- flushes out properties that apply to this view
        for (int i = 0; i < 30; i++) {
            BaseViewRule.editedProperty("dummy_" + i);
        }
        recentItems = recentChoices.getNestedActions(node);
        assertEquals(0, recentItems.size());

        BaseViewRule.editedProperty("gravity");
        recentItems = recentChoices.getNestedActions(node);
        assertEquals(1, recentItems.size());
        assertEquals("Gravity...", recentItems.get(0).getTitle());
    }

    public void testDragInEmptyWithBounds() {
        dragIntoEmpty(new Rect(0, 0, 100, 80));
    }

    public void testDragInEmptyWithoutBounds() {
        dragIntoEmpty(new Rect(0, 0, 0, 0));
    }

    public void testDragInVerticalTop() {
        dragInto(true,
                // Bounds of the dragged item
                new Rect(0, 0, 105, 80),
                // Drag point
                new Point(30, -10),
                // Expected insert location
                0,
                // Not dragging one of the existing children
                -1,
                // Bounds rectangle
                "useStyle(DROP_RECIPIENT), drawRect(Rect[0,0,240,480])",

                // Drop zones
                "useStyle(DROP_ZONE), drawLine(0,0,240,0), drawLine(0,90,240,90), "
                        + "drawLine(0,190,240,190), drawLine(0,290,240,290), "
                        + "drawLine(0,381,240,381)",

                // Active nearest line
                "useStyle(DROP_ZONE_ACTIVE), useStyle(DROP_PREVIEW), drawLine(0,0,240,0)",

                // Preview of the dropped rectangle
                "useStyle(DROP_PREVIEW), drawRect(0,-40,105,40)");

        // Without drag bounds it should be identical except no preview
        // rectangle
        dragInto(true,
                new Rect(0, 0, 0, 0), // Invalid
                new Point(30, -10), 0, -1,
                "useStyle(DROP_ZONE_ACTIVE), useStyle(DROP_PREVIEW), drawLine(0,0,240,0)");
    }

    public void testDragInVerticalBottom() {
        dragInto(true,
                // Bounds of the dragged item
                new Rect(0, 0, 105, 80),
                // Drag point
                new Point(30, 500),
                // Expected insert location
                4,
                // Not dragging one of the existing children
                -1,
                // Bounds rectangle
                "useStyle(DROP_RECIPIENT), drawRect(Rect[0,0,240,480])",

                // Drop zones
                "useStyle(DROP_ZONE), drawLine(0,0,240,0), drawLine(0,90,240,90), "
                        + "drawLine(0,190,240,190), drawLine(0,290,240,290), drawLine(0,381,240,381), ",

                // Active nearest line
                "useStyle(DROP_ZONE_ACTIVE), useStyle(DROP_PREVIEW), drawLine(0,381,240,381)",

                // Preview of the dropped rectangle
                "useStyle(DROP_PREVIEW), drawRect(0,381,105,461)");

        // Check without bounds too
        dragInto(true, new Rect(0, 0, 105, 80), new Point(30, 500), 4, -1,
                "useStyle(DROP_PREVIEW), drawRect(0,381,105,461)");
    }

    public void testDragInVerticalMiddle() {
        dragInto(true,
                // Bounds of the dragged item
                new Rect(0, 0, 105, 80),
                // Drag point
                new Point(0, 170),
                // Expected insert location
                2,
                // Not dragging one of the existing children
                -1,
                // Bounds rectangle
                "useStyle(DROP_RECIPIENT), drawRect(Rect[0,0,240,480])",

                // Drop zones
                "useStyle(DROP_ZONE), drawLine(0,0,240,0), drawLine(0,90,240,90), "
                        + "drawLine(0,190,240,190), drawLine(0,290,240,290)",

                // Active nearest line
                "useStyle(DROP_ZONE_ACTIVE), useStyle(DROP_PREVIEW), drawLine(0,190,240,190)",

                // Preview of the dropped rectangle
                "useStyle(DROP_PREVIEW), drawRect(0,150,105,230)");

        // Check without bounds too
        dragInto(true, new Rect(0, 0, 105, 80), new Point(0, 170), 2, -1,
                "useStyle(DROP_PREVIEW), drawRect(0,150,105,230)");
    }

    public void testDragInVerticalMiddleSelfPos() {
        // Drag the 2nd button, down to the position between 3rd and 4th
        dragInto(true,
                // Bounds of the dragged item
                new Rect(0, 100, 100, 80),
                // Drag point
                new Point(0, 250),
                // Expected insert location
                2,
                // Dragging 1st item
                1,
                // Bounds rectangle

                "useStyle(DROP_RECIPIENT), drawRect(Rect[0,0,240,480])",

                // Drop zones - these are different because we exclude drop
                // zones around the
                // dragged item itself (it doesn't make sense to insert directly
                // before or after
                // myself
                "useStyle(DROP_ZONE), drawLine(0,0,240,0), drawLine(0,290,240,290), "
                        + "drawLine(0,381,240,381)",

                // Preview line along insert axis
                "useStyle(DROP_ZONE_ACTIVE), useStyle(DROP_PREVIEW), drawLine(0,290,240,290)",

                // Preview of dropped rectangle
                "useStyle(DROP_PREVIEW), drawRect(0,250,100,330)");

        // Test dropping on self (no position change):
        dragInto(true,
                // Bounds of the dragged item
                new Rect(0, 100, 100, 80),
                // Drag point
                new Point(0, 210),
                // Expected insert location
                1,
                // Dragging from same pos
                1,
                // Bounds rectangle
                "useStyle(DROP_RECIPIENT), drawRect(Rect[0,0,240,480])",

                // Drop zones - these are different because we exclude drop
                // zones around the
                // dragged item itself (it doesn't make sense to insert directly
                // before or after
                // myself
                "useStyle(DROP_ZONE), drawLine(0,0,240,0), drawLine(0,290,240,290), "
                        + "drawLine(0,381,240,381)",

                // No active nearest line when you're over the self pos!

                // Preview of the dropped rectangle
                "useStyle(DROP_ZONE_ACTIVE), useStyle(DROP_PREVIEW), drawRect(0,100,100,180)");
    }

    public void testDragToLastPosition() {
        // Drag a button to the last position -- and confirm that the preview rectangle
        // is now shown midway between the second to last and last positions, but fully
        // below the drop zone line:
        dragInto(true,
                // Bounds of the dragged item
                new Rect(0, 100, 100, 80),
                // Drag point
                new Point(0, 400),
                // Expected insert location
                3,
                // Dragging 1st item
                1,

                // Bounds rectangle
                "useStyle(DROP_RECIPIENT), drawRect(Rect[0,0,240,480])",

                // Drop Zones
                "useStyle(DROP_ZONE), drawLine(0,0,240,0), drawLine(0,290,240,290), " +
                "drawLine(0,381,240,381), ",

                // Active Drop Zone
                "useStyle(DROP_ZONE_ACTIVE), useStyle(DROP_PREVIEW), drawLine(0,381,240,381)",

                // Drop Preview
                "useStyle(DROP_PREVIEW), drawRect(0,381,100,461)");
    }

    public void testFormatFloatValue() throws Exception {
        assertEquals("1", LinearLayoutRule.formatFloatAttribute(1.0f));
        assertEquals("2", LinearLayoutRule.formatFloatAttribute(2.0f));
        assertEquals("1.50", LinearLayoutRule.formatFloatAttribute(1.5f));
        assertEquals("1.50", LinearLayoutRule.formatFloatAttribute(1.50f));
        assertEquals("1.51", LinearLayoutRule.formatFloatAttribute(1.51f));
        assertEquals("1.51", LinearLayoutRule.formatFloatAttribute(1.514542f));
        assertEquals("1.52", LinearLayoutRule.formatFloatAttribute(1.516542f));
        assertEquals("-1.51", LinearLayoutRule.formatFloatAttribute(-1.51f));
        assertEquals("-1", LinearLayoutRule.formatFloatAttribute(-1f));
    }

    public void testFormatFloatValueLocale() throws Exception {
        // Ensure that the layout float values aren't affected by
        // locale settings, like using commas instead of of periods
        Locale originalDefaultLocale = Locale.getDefault();

        try {
            Locale.setDefault(Locale.FRENCH);

            // Ensure that this is a locale which uses a comma instead of a period:
            assertEquals("5,24", String.format("%.2f", 5.236f));

            // Ensure that the formatFloatAttribute is immune
            assertEquals("1.50", LinearLayoutRule.formatFloatAttribute(1.5f));
        } finally {
            Locale.setDefault(originalDefaultLocale);
        }
    }

    // Left to test:
    // Check inserting at last pos with multiple children
    // Check inserting with no bounds rectangle for dragged element
}
