/*******************************************************************************
 * Copyright (c) 2013, 2020 Dirk Fauth and others.
 *
 * This program and the accompanying materials are made
 * available under the terms of the Eclipse Public License 2.0
 * which is available at https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *    Dirk Fauth <dirk.fauth@googlemail.com> - initial API and implementation
 *******************************************************************************/
package org.eclipse.nebula.widgets.nattable.extension.glazedlists.tree;

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

import java.text.MessageFormat;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;

import org.eclipse.nebula.widgets.nattable.dataset.person.Person;
import org.eclipse.nebula.widgets.nattable.dataset.person.PersonService;
import org.junit.Before;
import org.junit.Test;

import ca.odell.glazedlists.EventList;
import ca.odell.glazedlists.GlazedLists;
import ca.odell.glazedlists.TreeList;

@SuppressWarnings({ "unchecked", "rawtypes" })
public class GlazedListTreeRowModelExpandCollapseTest {

    private TreeList treeList;
    private GlazedListTreeData treeData;
    private GlazedListTreeRowModel treeRowModel;

    @Before
    public void setup() {
        EventList<Person> eventList = GlazedLists.eventList(PersonService.getFixedPersons());

        this.treeList = new TreeList(eventList, new PersonTreeFormat(), TreeList.nodesStartExpanded());

        this.treeData = new GlazedListTreeData(this.treeList);
        this.treeRowModel = new GlazedListTreeRowModel(this.treeData);
    }

    @Test
    public void testInitialExpanded() {
        for (int i = 0; i < this.treeList.size(); i++) {
            assertFalse("Node is not expanded", this.treeRowModel.isCollapsed(i));
        }
    }

    @Test
    public void testCollapseAllStepByStep() {
        for (int i = this.treeList.size() - 1; i >= 0; i--) {
            this.treeRowModel.collapse(i);
        }

        for (int i = 0; i < this.treeList.size(); i++) {
            assertTrue(
                    MessageFormat.format("Node at index {0} is expanded", i),
                    this.treeRowModel.isCollapsed(i));
        }
    }

    @Test
    public void testCollapseAll() {
        this.treeRowModel.collapseAll();

        for (int i = 0; i < this.treeList.size(); i++) {
            assertTrue(
                    MessageFormat.format("Node at index {0} is expanded", i),
                    this.treeRowModel.isCollapsed(i));
        }
    }

    @Test
    public void testCollapseExpandAll() {
        this.treeRowModel.collapseAll();
        this.treeRowModel.expandAll();

        for (int i = 0; i < this.treeList.size(); i++) {
            assertFalse(
                    MessageFormat.format("Node at index {0} is collapsed", i),
                    this.treeRowModel.isCollapsed(i));
        }
    }

    @Test
    public void testCollapseAllNonUnique() {
        this.treeList.addAll(PersonService.getFixedMixedPersons());

        this.treeRowModel.collapseAll();

        for (int i = 0; i < this.treeList.size(); i++) {
            assertTrue(
                    MessageFormat.format("Node at index {0} is expanded", i),
                    this.treeRowModel.isCollapsed(i));
        }
    }

    @Test
    public void testCollapseExpandAllNonUnique() {
        this.treeList.addAll(PersonService.getFixedMixedPersons());

        this.treeRowModel.collapseAll();
        this.treeRowModel.expandAll();

        for (int i = 0; i < this.treeList.size(); i++) {
            assertFalse(
                    MessageFormat.format("Node at index {0} is collapsed", i),
                    this.treeRowModel.isCollapsed(i));
        }
    }

    @Test
    public void testGetChildrenByIndex() {
        List flandersChildren = this.treeData.getChildren(0);

        assertEquals(4, flandersChildren.size());
    }

    @Test
    public void testExpandCollapseByIndex() {
        assertFalse("Flanders is not expanded", this.treeRowModel.isCollapsed(0));

        // collapse Flanders
        this.treeRowModel.collapse(0);

        assertTrue("Flanders is not expanded", this.treeRowModel.isCollapsed(0));

        // expand Flanders
        this.treeRowModel.expand(0);

        assertFalse("Flanders is not expanded", this.treeRowModel.isCollapsed(0));
    }

    @Test
    public void testExpandCollapseByObject() {
        LastNameGroup flanders = new LastNameGroup(2, "Flanders");
        assertFalse("Flanders is not expanded", this.treeRowModel.isCollapsed(flanders));

        // collapse Flanders
        this.treeRowModel.collapse(flanders);

        assertTrue("Flanders is not expanded", this.treeRowModel.isCollapsed(flanders));

        // expand Flanders
        this.treeRowModel.expand(flanders);

        assertFalse("Flanders is not expanded", this.treeRowModel.isCollapsed(flanders));
    }

    @Test
    public void testExpandToLevel() {
        // collapse all and expand to level 1
        // lastnames should be expanded, firstnames still collapsed
        this.treeRowModel.collapseAll();

        for (int i = 0; i < this.treeList.size(); i++) {
            assertTrue(
                    MessageFormat.format("Node at index {0} is expanded", i),
                    this.treeRowModel.isCollapsed(i));
        }

        this.treeRowModel.expandToLevel(1);
        for (int i = 0; i < this.treeList.size(); i++) {
            if (this.treeList.get(i) instanceof LastNameGroup) {
                assertFalse(
                        MessageFormat.format("Node at index {0} is collapsed", i),
                        this.treeRowModel.isCollapsed(i));
            } else if (this.treeList.get(i) instanceof FirstNameGroup) {
                assertTrue(
                        MessageFormat.format("Node at index {0} is expanded", i),
                        this.treeRowModel.isCollapsed(i));
            } else {
                // there should be no other values visible right now
                fail("Another object than LastNameGroup and FirstNameGroup is visible");
            }
        }
    }

    private class PersonTreeFormat implements TreeList.Format<Object> {

        AtomicInteger counter = new AtomicInteger();
        Map<String, LastNameGroup> lastNames = new HashMap<String, LastNameGroup>();

        {
            this.lastNames.put("Simpsons", new LastNameGroup(1, "Simpsons"));
            this.lastNames.put("Flanders", new LastNameGroup(2, "Flanders"));
        }

        Map<String, FirstNameGroup> firstNames = new HashMap<String, FirstNameGroup>();

        @Override
        public void getPath(List<Object> path, Object element) {
            if (element instanceof Person) {
                Person ele = (Person) element;
                if (!this.lastNames.containsKey(ele.getLastName())) {
                    this.lastNames.put(ele.getLastName(), new LastNameGroup(this.counter.incrementAndGet(), ele.getLastName()));
                }
                path.add(this.lastNames.get(ele.getLastName()));

                String firstNameKey = ele.getLastName() + "_" + ele.getFirstName();
                if (!this.firstNames.containsKey(firstNameKey)) {
                    this.firstNames.put(firstNameKey, new FirstNameGroup(ele.getLastName(), ele.getFirstName()));
                }
                path.add(this.firstNames.get(firstNameKey));
            }
            path.add(element);
        }

        @Override
        public boolean allowsChildren(Object element) {
            return true;
        }

        @Override
        public Comparator<? super Object> getComparator(final int depth) {
            return new Comparator<Object>() {

                @Override
                public int compare(Object o1, Object o2) {
                    String e1 = (o1 instanceof Person) ? (depth == 0 ? ((Person) o1)
                            .getLastName() : ((Person) o1).getFirstName())
                            : o1.toString();
                    String e2 = (o2 instanceof Person) ? (depth == 0 ? ((Person) o2)
                            .getLastName() : ((Person) o2).getFirstName())
                            : o2.toString();
                    return e1.compareTo(e2);
                }

            };
        }
    }

    class LastNameGroup implements Comparable<LastNameGroup> {
        int id;
        String lastName;

        public LastNameGroup(int id, String lastName) {
            this.id = id;
            this.lastName = lastName;
        }

        @Override
        public int hashCode() {
            final int prime = 31;
            int result = 1;
            result = prime * result + this.id;
            result = prime * result + ((this.lastName == null) ? 0 : this.lastName.hashCode());
            return result;
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj)
                return true;
            if (obj == null)
                return false;
            if (getClass() != obj.getClass())
                return false;
            LastNameGroup other = (LastNameGroup) obj;
            if (this.id != other.id)
                return false;
            if (this.lastName == null) {
                if (other.lastName != null)
                    return false;
            } else if (!this.lastName.equals(other.lastName))
                return false;
            return true;
        }

        @Override
        public String toString() {
            return this.lastName;
        }

        @Override
        public int compareTo(LastNameGroup o) {
            return this.lastName.compareTo(o.lastName);
        }
    }

    // firstname group is unique within a lastname group
    class FirstNameGroup implements Comparable<FirstNameGroup> {
        String lastName;
        String firstName;

        public FirstNameGroup(String lastName, String firstName) {
            this.lastName = lastName;
            this.firstName = firstName;
        }

        @Override
        public int hashCode() {
            final int prime = 31;
            int result = 1;
            result = prime * result + ((this.firstName == null) ? 0 : this.firstName.hashCode());
            result = prime * result + ((this.lastName == null) ? 0 : this.lastName.hashCode());
            return result;
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj)
                return true;
            if (obj == null)
                return false;
            if (getClass() != obj.getClass())
                return false;
            FirstNameGroup other = (FirstNameGroup) obj;
            if (this.firstName == null) {
                if (other.firstName != null)
                    return false;
            } else if (!this.firstName.equals(other.firstName))
                return false;
            if (this.lastName == null) {
                if (other.lastName != null)
                    return false;
            } else if (!this.lastName.equals(other.lastName))
                return false;
            return true;
        }

        @Override
        public String toString() {
            return this.firstName;
        }

        @Override
        public int compareTo(FirstNameGroup o) {
            return this.firstName.compareTo(o.firstName);
        }
    }
}
