/*
 * Copyright (C) 2013 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 com.android.ide.common.res2;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import com.android.ide.common.rendering.api.AttrResourceValue;
import com.android.ide.common.rendering.api.ItemResourceValue;
import com.android.ide.common.rendering.api.ResourceValue;
import com.android.ide.common.rendering.api.StyleResourceValue;
import com.android.resources.ResourceFolderType;
import com.android.resources.ResourceType;
import com.android.testutils.TestUtils;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Multimap;

import org.junit.Test;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;

public class ResourceRepositoryTest extends BaseTestCase {

    @Test
    public void testMergeByCount() throws Exception {
        ResourceRepository repo = getResourceRepository();

        Map<ResourceType, ListMultimap<String, ResourceItem>> items = repo.getItems();

        assertEquals(6, items.get(ResourceType.DRAWABLE).size());
        assertEquals(1, items.get(ResourceType.RAW).size());
        assertEquals(4, items.get(ResourceType.LAYOUT).size());
        assertEquals(1, items.get(ResourceType.COLOR).size());
        assertEquals(6, items.get(ResourceType.STRING).size());
        assertEquals(1, items.get(ResourceType.STYLE).size());
        assertEquals(3, items.get(ResourceType.ARRAY).size());
        assertEquals(7, items.get(ResourceType.ATTR).size());
        assertEquals(1, items.get(ResourceType.DECLARE_STYLEABLE).size());
        assertEquals(2, items.get(ResourceType.DIMEN).size());
        assertEquals(1, items.get(ResourceType.ID).size());
        assertEquals(1, items.get(ResourceType.INTEGER).size());
        assertEquals(2, items.get(ResourceType.PLURALS).size());
    }

    @Test
    public void testMergedResourcesByName() throws Exception {
        ResourceRepository repo = getResourceRepository();

        // use ? between type and qualifier because of declare-styleable
        verifyResourceExists(repo,
                "drawable/icon",
                "drawable?ldpi-v4/icon",
                "drawable/icon2",
                "drawable/patch",
                "drawable/color_drawable",
                "drawable/drawable_ref",
                "raw/foo",
                "layout/main",
                "layout/layout_ref",
                "layout/alias_replaced_by_file",
                "layout/file_replaced_by_alias",
                "color/color",
                "string/basic_string",
                "string/xliff_string",
                "string/styled_string",
                "string/two",
                "string/many",
                "style/style",
                "array/string_array",
                "array/integer_array",
                "array/my_colors",
                "attr/dimen_attr",
                "attr/string_attr",
                "attr/enum_attr",
                "attr/flag_attr",
                "attr/blah",
                "attr/blah2",
                "attr/flagAttr",
                "declare-styleable/declare_styleable",
                "dimen/dimen",
                "dimen?sw600dp-v13/offset",
                "id/item_id",
                "integer/integer",
                "plurals/plurals",
                "plurals/plurals_with_bad_quantity"
        );
    }

    @Test
    public void testBaseStringValue() throws Exception {
        ResourceRepository repo = getResourceRepository();

        List<ResourceItem> itemList = repo.getResourceItem(ResourceType.STRING, "basic_string");
        assertNotNull(itemList);
        assertEquals(1, itemList.size());

        ResourceValue value = itemList.get(0).getResourceValue(false);
        assertNotNull(value);

        assertEquals("overlay_string", value.getValue());
    }

    @Test
    public void testBaseStyledStringValue() throws Exception {
        ResourceRepository repo = getResourceRepository();

        List<ResourceItem> itemList = repo.getResourceItem(ResourceType.STRING, "styled_string");
        assertNotNull(itemList);
        assertEquals(1, itemList.size());

        ResourceValue value = itemList.get(0).getResourceValue(false);
        assertNotNull(value);

        assertEquals("Forgot your username or password?\nVisit google.com/accounts/recovery.",
                value.getValue());
    }

    @Test
    public void testBaseColorValue() throws Exception {
        ResourceRepository repo = getResourceRepository();

        List<ResourceItem> itemList = repo.getResourceItem(ResourceType.COLOR, "color");
        assertNotNull(itemList);
        assertEquals(1, itemList.size());

        ResourceValue value = itemList.get(0).getResourceValue(false);
        assertNotNull(value);

        assertEquals("#FFFFFFFF", value.getValue());
    }

    @Test
    public void testBaseLayoutAliasValue() throws Exception {
        ResourceRepository repo = getResourceRepository();

        List<ResourceItem> itemList = repo.getResourceItem(ResourceType.LAYOUT, "layout_ref");
        assertNotNull(itemList);
        assertEquals(1, itemList.size());

        ResourceValue value = itemList.get(0).getResourceValue(false);
        assertNotNull(value);

        assertEquals("@layout/ref", value.getValue());
    }

    @Test
    public void testBaseAttrValue() throws Exception {
        ResourceRepository repo = getResourceRepository();

        List<ResourceItem> itemList = repo.getResourceItem(ResourceType.ATTR, "flag_attr");
        assertNotNull(itemList);
        assertEquals(1, itemList.size());

        ResourceValue value = itemList.get(0).getResourceValue(false);
        assertNotNull(value);
        assertTrue(value instanceof AttrResourceValue);
        AttrResourceValue attrValue = (AttrResourceValue) value;

        Map<String, Integer> attrValues = attrValue.getAttributeValues();
        assertNotNull(attrValues);
        assertEquals(3, attrValues.size());

        Integer i = attrValues.get("normal");
        assertNotNull(i);
        assertEquals(Integer.valueOf(0), i);

        i = attrValues.get("bold");
        assertNotNull(i);
        assertEquals(Integer.valueOf(1), i);

        i = attrValues.get("italic");
        assertNotNull(i);
        assertEquals(Integer.valueOf(2), i);
    }

    @Test
    public void testBaseStyleValue() throws Exception {
        ResourceRepository repo = getResourceRepository();

        List<ResourceItem> itemList = repo.getResourceItem(ResourceType.STYLE, "style");
        assertNotNull(itemList);
        assertEquals(1, itemList.size());

        ResourceValue value = itemList.get(0).getResourceValue(false);
        assertNotNull(value);
        assertTrue(value instanceof StyleResourceValue);
        StyleResourceValue styleResourceValue = (StyleResourceValue) value;

        assertEquals("@android:style/Holo.Light", styleResourceValue.getParentStyle());

        ItemResourceValue styleValue = styleResourceValue.getItem("singleLine", true /*framework*/);
        assertNotNull(styleValue);
        assertEquals("true", styleValue.getValue());

        styleValue = styleResourceValue.getItem("textAppearance", true /*framework*/);
        assertNotNull(styleValue);
        assertEquals("@style/TextAppearance.WindowTitle", styleValue.getValue());

        styleValue = styleResourceValue.getItem("shadowColor", true /*framework*/);
        assertNotNull(styleValue);
        assertEquals("#BB000000", styleValue.getValue());

        styleValue = styleResourceValue.getItem("shadowRadius", true /*framework*/);
        assertNotNull(styleValue);
        assertEquals("2.75", styleValue.getValue());

        styleValue = styleResourceValue.getItem("foo", false /*framework*/);
        assertNotNull(styleValue);
        assertEquals("foo", styleValue.getValue());
    }

    @Test
    public void testUpdateWithBasicFiles() throws Exception {
        File root = getIncMergeRoot("basicFiles");
        File fakeRoot = getMergedBlobFolder(root);
        ResourceMerger resourceMerger = new ResourceMerger(0);
        resourceMerger.loadFromBlob(fakeRoot, false /*incrementalState*/);
        checkSourceFolders(resourceMerger);

        List<ResourceSet> sets = resourceMerger.getDataSets();
        assertEquals(2, sets.size());

        // write the content in a repo.
        ResourceRepository repo = new ResourceRepository(false);
        resourceMerger.mergeData(repo.createMergeConsumer(), true /*doCleanUp*/);

        // checks the initial state of the repo
        Map<ResourceType, ListMultimap<String, ResourceItem>> items = repo.getItems();
        ListMultimap<String, ResourceItem> drawables = items.get(ResourceType.DRAWABLE);
        assertNotNull("Drawable null check", drawables);
        assertEquals("Drawable size check", 6, drawables.size());
        verifyResourceExists(repo,
                "drawable/new_overlay",
                "drawable/removed",
                "drawable?ldpi/removed",
                "drawable/touched",
                "drawable/removed_overlay",
                "drawable/untouched");

        // Apply updates
        RecordingLogger logger =  new RecordingLogger();

        // ----------------
        // first set is the main one, no change here
        ResourceSet mainSet = sets.get(0);
        File mainBase = new File(root, "main");
        File mainDrawable = new File(mainBase, "drawable");
        File mainDrawableLdpi = new File(mainBase, "drawable-ldpi");

        // touched/removed files:
        File mainDrawableTouched = new File(mainDrawable, "touched.png");
        mainSet.updateWith(mainBase, mainDrawableTouched, FileStatus.CHANGED, logger);
        checkLogger(logger);

        File mainDrawableRemoved = new File(mainDrawable, "removed.png");
        mainSet.updateWith(mainBase, mainDrawableRemoved, FileStatus.REMOVED, logger);
        checkLogger(logger);

        File mainDrawableLdpiRemoved = new File(mainDrawableLdpi, "removed.png");
        mainSet.updateWith(mainBase, mainDrawableLdpiRemoved, FileStatus.REMOVED, logger);
        checkLogger(logger);

        // ----------------
        // second set is the overlay one
        ResourceSet overlaySet = sets.get(1);
        File overlayBase = new File(root, "overlay");
        File overlayDrawable = new File(overlayBase, "drawable");
        File overlayDrawableHdpi = new File(overlayBase, "drawable-hdpi");

        // new/removed files:
        File overlayDrawableNewOverlay = new File(overlayDrawable, "new_overlay.png");
        overlaySet.updateWith(overlayBase, overlayDrawableNewOverlay, FileStatus.NEW, logger);
        checkLogger(logger);

        File overlayDrawableRemovedOverlay = new File(overlayDrawable, "removed_overlay.png");
        overlaySet.updateWith(overlayBase, overlayDrawableRemovedOverlay, FileStatus.REMOVED,
                logger);
        checkLogger(logger);

        File overlayDrawableHdpiNewAlternate = new File(overlayDrawableHdpi, "new_alternate.png");
        overlaySet.updateWith(overlayBase, overlayDrawableHdpiNewAlternate, FileStatus.NEW, logger);
        checkLogger(logger);

        // validate for duplicates
        resourceMerger.validateDataSets();

        // check the new content.
        resourceMerger.mergeData(repo.createMergeConsumer(), true /*doCleanUp*/);

        drawables = items.get(ResourceType.DRAWABLE);
        assertNotNull("Drawable null check", drawables);
        assertEquals("Drawable size check", 5, drawables.size());
        verifyResourceExists(repo,
                "drawable/new_overlay",
                "drawable/touched",
                "drawable/removed_overlay",
                "drawable/untouched",
                "drawable?hdpi-v4/new_alternate");
        checkRemovedItems(resourceMerger);
    }

    @Test
    public void testUpdateWithBasicValues() throws Exception {
        File root = getIncMergeRoot("basicValues");
        File fakeRoot = getMergedBlobFolder(root);
        ResourceMerger resourceMerger = new ResourceMerger(0);
        resourceMerger.loadFromBlob(fakeRoot, false /*incrementalState*/);
        checkSourceFolders(resourceMerger);

        List<ResourceSet> sets = resourceMerger.getDataSets();
        assertEquals(2, sets.size());

        // write the content in a repo.
        ResourceRepository repo = new ResourceRepository(false);
        resourceMerger.mergeData(repo.createMergeConsumer(), true /*doCleanUp*/);

        // checks the initial state of the repo
        Map<ResourceType, ListMultimap<String, ResourceItem>> items = repo.getItems();
        ListMultimap<String, ResourceItem> strings = items.get(ResourceType.STRING);
        assertNotNull("String null check", strings);
        assertEquals("String size check", 5, strings.size());
        verifyResourceExists(repo,
                "string/untouched",
                "string/touched",
                "string/removed",
                "string?en/removed",
                "string/new_overlay");

        // apply updates
        RecordingLogger logger = new RecordingLogger();

        // ----------------
        // first set is the main one, no change here
        ResourceSet mainSet = sets.get(0);
        File mainBase = new File(root, "main");
        File mainValues = new File(mainBase, "values");
        File mainValuesEn = new File(mainBase, "values-en");

        // touched file:
        File mainValuesTouched = new File(mainValues, "values.xml");
        mainSet.updateWith(mainBase, mainValuesTouched, FileStatus.CHANGED, logger);
        checkLogger(logger);

        // removed files
        File mainValuesEnRemoved = new File(mainValuesEn, "values.xml");
        mainSet.updateWith(mainBase, mainValuesEnRemoved, FileStatus.REMOVED, logger);
        checkLogger(logger);

        // ----------------
        // second set is the overlay one
        ResourceSet overlaySet = sets.get(1);
        File overlayBase = new File(root, "overlay");
        File overlayValues = new File(overlayBase, "values");
        File overlayValuesFr = new File(overlayBase, "values-fr");

        // new files:
        File overlayValuesNew = new File(overlayValues, "values.xml");
        overlaySet.updateWith(overlayBase, overlayValuesNew, FileStatus.NEW, logger);
        checkLogger(logger);

        File overlayValuesFrNew = new File(overlayValuesFr, "values.xml");
        overlaySet.updateWith(overlayBase, overlayValuesFrNew, FileStatus.NEW, logger);
        checkLogger(logger);

        // validate for duplicates
        resourceMerger.validateDataSets();

        // check the new content.
        resourceMerger.mergeData(repo.createMergeConsumer(), true /*doCleanUp*/);

        strings = items.get(ResourceType.STRING);
        assertNotNull("String null check", strings);
        assertEquals("String size check", 4, strings.size());
        verifyResourceExists(repo,
                "string/untouched",
                "string/touched",
                "string/new_overlay",
                "string?fr/new_alternate");
        checkRemovedItems(resourceMerger);
    }

    @Test
    public void testUpdateWithBasicValues2() throws Exception {
        File root = getIncMergeRoot("basicValues2");
        File fakeRoot = getMergedBlobFolder(root);
        ResourceMerger resourceMerger = new ResourceMerger(0);
        resourceMerger.loadFromBlob(fakeRoot, false /*incrementalState*/);
        checkSourceFolders(resourceMerger);

        List<ResourceSet> sets = resourceMerger.getDataSets();
        assertEquals(2, sets.size());

        // write the content in a repo.
        ResourceRepository repo = new ResourceRepository(false);
        resourceMerger.mergeData(repo.createMergeConsumer(), true /*doCleanUp*/);

        // checks the initial state of the repo
        Map<ResourceType, ListMultimap<String, ResourceItem>> items = repo.getItems();
        ListMultimap<String, ResourceItem> strings = items.get(ResourceType.STRING);
        assertNotNull("String null check", strings);
        assertEquals("String size check", 2, strings.size());
        verifyResourceExists(repo,
                "string/untouched",
                "string/removed_overlay");

        // apply updates
        RecordingLogger logger = new RecordingLogger();

        // ----------------
        // first set is the main one, no change here

        // ----------------
        // second set is the overlay one
        ResourceSet overlaySet = sets.get(1);
        File overlayBase = new File(root, "overlay");
        File overlayValues = new File(overlayBase, "values");

        // new files:
        File overlayValuesNew = new File(overlayValues, "values.xml");
        overlaySet.updateWith(overlayBase, overlayValuesNew, FileStatus.REMOVED, logger);
        checkLogger(logger);

        // validate for duplicates
        resourceMerger.validateDataSets();

        // check the new content.
        resourceMerger.mergeData(repo.createMergeConsumer(), true /*doCleanUp*/);

        strings = items.get(ResourceType.STRING);
        assertNotNull("String null check", strings);
        assertEquals("String size check", 2, strings.size());
        verifyResourceExists(repo,
                "string/untouched",
                "string/removed_overlay");
        checkRemovedItems(resourceMerger);
    }

    @Test
    public void testUpdateWithFilesVsValues() throws Exception {
        File root = getIncMergeRoot("filesVsValues");
        File fakeRoot = getMergedBlobFolder(root);
        ResourceMerger resourceMerger = new ResourceMerger(0);
        resourceMerger.loadFromBlob(fakeRoot, false /*incrementalState*/);
        checkSourceFolders(resourceMerger);

        List<ResourceSet> sets = resourceMerger.getDataSets();
        assertEquals(1, sets.size());

        // write the content in a repo.
        ResourceRepository repo = new ResourceRepository(false);
        resourceMerger.mergeData(repo.createMergeConsumer(), true /*doCleanUp*/);

        // checks the initial state of the repo
        Map<ResourceType, ListMultimap<String, ResourceItem>> items = repo.getItems();
        ListMultimap<String, ResourceItem> layouts = items.get(ResourceType.LAYOUT);
        assertNotNull("String null check", layouts);
        assertEquals("String size check", 3, layouts.size());
        verifyResourceExists(repo,
                "layout/main",
                "layout/file_replaced_by_alias",
                "layout/alias_replaced_by_file");

        // apply updates
        RecordingLogger logger = new RecordingLogger();

        // ----------------
        // Load the main set
        ResourceSet mainSet = sets.get(0);
        File mainBase = new File(root, "main");
        File mainValues = new File(mainBase, ResourceFolderType.VALUES.getName());
        File mainLayout = new File(mainBase, ResourceFolderType.LAYOUT.getName());

        // touched file:
        File mainValuesTouched = new File(mainValues, "values.xml");
        mainSet.updateWith(mainBase, mainValuesTouched, FileStatus.CHANGED, logger);
        checkLogger(logger);

        // new file:
        File mainLayoutNew = new File(mainLayout, "alias_replaced_by_file.xml");
        mainSet.updateWith(mainBase, mainLayoutNew, FileStatus.NEW, logger);
        checkLogger(logger);

        // removed file
        File mainLayoutRemoved = new File(mainLayout, "file_replaced_by_alias.xml");
        mainSet.updateWith(mainBase, mainLayoutRemoved, FileStatus.REMOVED, logger);
        checkLogger(logger);

        // validate for duplicates
        resourceMerger.validateDataSets();

        // check the new content.
        resourceMerger.mergeData(repo.createMergeConsumer(), true /*doCleanUp*/);

        layouts = items.get(ResourceType.LAYOUT);
        assertNotNull("String null check", layouts);
        assertEquals("String size check", 3, layouts.size());
        verifyResourceExists(repo,
                "layout/main",
                "layout/file_replaced_by_alias",
                "layout/alias_replaced_by_file");
        checkRemovedItems(resourceMerger);
    }

    @Test
    public void testUpdateFromOldFile() throws Exception {
        File root = getIncMergeRoot("oldMerge");
        File fakeRoot = getMergedBlobFolder(root);
        ResourceMerger resourceMerger = new ResourceMerger(0);
        assertFalse(resourceMerger.loadFromBlob(fakeRoot, false /*incrementalState*/));
    }

    private static void checkRemovedItems(DataMap<? extends DataItem> dataMap) {
        for (DataItem item : dataMap.getDataMap().values()) {
            if (item.isRemoved()) {
                fail("Removed item found: " + item);
            }
        }
    }

    /**
     * Creates a fake merge with given sets.
     *
     * the data is an array of sets.
     *
     * Each set is [ setName, folder1, folder2, ...]
     *
     */
    private static ResourceMerger createMerger(String[][] data) {
        ResourceMerger merger = new ResourceMerger(0);
        for (String[] setData : data) {
            ResourceSet set = new ResourceSet(setData[0], null);
            merger.addDataSet(set);
            for (int i = 1, n = setData.length; i < n; i++) {
                set.addSource(new File(setData[i]));
            }
        }

        return merger;
    }

    /**
     * Returns a merger with the baseSet and baseMerge content.
     */
    private static ResourceMerger getBaseResourceMerger()
            throws MergingException, IOException {
        File root = TestUtils.getRoot("resources", "baseMerge");

        ResourceSet res = ResourceSetTest.getBaseResourceSet();

        RecordingLogger logger = new RecordingLogger();

        ResourceSet overlay = new ResourceSet("overlay", null);
        overlay.addSource(new File(root, "overlay"));
        overlay.loadFromFiles(logger);

        checkLogger(logger);

        ResourceMerger resourceMerger = new ResourceMerger(0);
        resourceMerger.addDataSet(res);
        resourceMerger.addDataSet(overlay);

        return resourceMerger;
    }

    /**
     * Returns a merger from incMergeData initialized from the files, not from the merger
     * state blog.
     */
    private static ResourceMerger getIncResourceMerger(String rootName, String... sets)
            throws MergingException, IOException {

        File root = getIncMergeRoot(rootName);
        RecordingLogger logger = new RecordingLogger();

        ResourceMerger resourceMerger = new ResourceMerger(0);

        for (String setName : sets) {
            ResourceSet resourceSet = new ResourceSet(setName, null);
            resourceSet.addSource(new File(root, setName));
            resourceSet.loadFromFiles(logger);
            checkLogger(logger);

            resourceMerger.addDataSet(resourceSet);
        }

        return resourceMerger;
    }

    private static ResourceRepository getResourceRepository()
            throws MergingException, IOException {
        ResourceMerger merger = getBaseResourceMerger();

        ResourceRepository repo = new ResourceRepository(false);

        merger.mergeData(repo.createMergeConsumer(), true /*doCleanUp*/);
        return repo;
    }

    private static File getIncMergeRoot(String name) throws IOException {
        File root = TestUtils.getCanonicalRoot("resources", "incMergeData");
        return new File(root, name);
    }

    private static void verifyResourceExists(ResourceRepository repository,
            String... dataItemKeys) {
        Map<ResourceType, ListMultimap<String, ResourceItem>> items = repository.getItems();

        for (String resKey : dataItemKeys) {
            String type, name, qualifier = "";

            int pos = resKey.indexOf('/');
            if (pos != -1) {
                name = resKey.substring(pos + 1);
                type = resKey.substring(0, pos);
            } else {
                throw new IllegalArgumentException("Invalid key " + resKey);
            }

            // use ? as a qualifier delimiter because of
            // declare-styleable
            pos = type.indexOf('?');
            if (pos != -1) {
                qualifier = type.substring(pos + 1);
                type = type.substring(0, pos);
            }

            ResourceType resourceType = ResourceType.getEnum(type);
            assertNotNull("Type check for " + resKey, resourceType);

            Multimap<String, ResourceItem> map = items.get(resourceType);
            assertNotNull("Map check for " + resKey, map);

            Collection<ResourceItem> list = map.get(name);
            int found = 0;
            for (ResourceItem resourceItem : list) {
                if (resourceItem.getName().equals(name)) {
                    String fileQualifier = resourceItem.getSource() != null ?
                            resourceItem.getSource().getQualifiers() : "";

                    if (qualifier.equals(fileQualifier)) {
                        found++;
                    }
                }
            }

            assertEquals("Match for " + resKey, 1, found);
        }
    }

    // This utility method has been pretty handy in tracking down resource repository bugs
    // so keeping it for future potential use, but in unit test code so no runtime overhead
    @SuppressWarnings({"deprecation", "ConstantConditions"})
    public static String dumpRepository(ResourceRepository repository) {
        Map<ResourceType, ListMultimap<String, ResourceItem>> mItems = repository.getMap();
        Comparator<ResourceItem> comparator = new Comparator<ResourceItem>() {
            @Override
            public int compare(ResourceItem item1, ResourceItem item2) {
                assert item1.getType() == item2.getType();
                String qualifiers = item2.getSource().getQualifiers();
                return item1.getSource().getQualifiers().compareTo(qualifiers);
            }
        };

        StringBuilder sb = new StringBuilder(5000);
        sb.append("Resource Map Dump For Repository ").append(repository)
                .append("\n------------------------------------------------\n");
        for (ResourceType type : ResourceType.values()) {
            ListMultimap<String, ResourceItem> map = mItems.get(type);
            if (map == null) {
                continue;
            }
            sb.append(type.getName()).append(':').append('\n');

            List<String> keys = new ArrayList<String>(map.keySet());
            Collections.sort(keys);
            for (String key : keys) {
                List<ResourceItem> items = map.get(key);
                List<ResourceItem> sorted = new ArrayList<ResourceItem>(items);
                Collections.sort(sorted, comparator);
                sb.append("  ").append(type.getName()).append(" ").append(key).append(": ");
                boolean first = true;
                for (ResourceItem item : sorted) {
                    if (first) {
                        first = false;
                    } else {
                        sb.append(", ");
                    }
                    String qualifiers = item.getSource().getQualifiers();
                    if (qualifiers.isEmpty()) {
                        qualifiers = "default";
                    }
                    sb.append(qualifiers);
                }

                if (!sorted.isEmpty()) {
                    ResourceItem item = sorted.get(0);
                    ResourceValue resourceValue = item.getResourceValue(repository.isFramework());
                    if (resourceValue == null) {
                        sb.append(" <no value found>");
                    } else {
                        String value = resourceValue.getValue();
                        if (value == null || value.isEmpty()) {
                            if (resourceValue instanceof StyleResourceValue) {
                                StyleResourceValue srv = (StyleResourceValue) resourceValue;
                                sb.append("  parentStyle=").append(srv.getParentStyle())
                                        .append("\n");
                                for (String name : srv.getNames()) {
                                    ItemResourceValue value1 = srv.getItem(name, false);
                                    ItemResourceValue value2 = srv.getItem(name, true);
                                    if (value1 != null) {
                                        Boolean framework = false;
                                        sb.append("    ");
                                        sb.append(name).append(" ").append(framework).append(" ");
                                        sb.append(" = ");
                                        sb.append('"');
                                        String strValue = value1.getValue();
                                        if (strValue != null) {
                                            sb.append(strValue.replace("\n", "\\n"));
                                        } else {
                                            sb.append("???");
                                        }
                                        sb.append('"');
                                    }
                                    if (value2 != null) {
                                        Boolean framework = true;
                                        sb.append("    ");
                                        sb.append(name).append(" ").append(framework).append(" ");
                                        sb.append(" = ");
                                        sb.append('"');
                                        String strValue = value2.getValue();
                                        if (strValue != null) {
                                            sb.append(strValue.replace("\n", "\\n"));
                                        } else {
                                            sb.append("???");
                                        }
                                        sb.append('"');
                                    }
                                }
                            } else {
                                sb.append(" = \"\"");
                            }
                        } else {
                            sb.append(" = ");
                            sb.append('"');
                            sb.append(value.replace("\n", "\\n"));
                            sb.append('"');
                        }
                    }
                }

                sb.append("\n");
            }

            sb.append("\n\n");
        }

        return sb.toString();
    }
}
