/*******************************************************************************
 * Copyright (c) 2012, 2020 Original authors 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:
 *     Original authors and others - initial API and implementation
 ******************************************************************************/
package org.eclipse.nebula.widgets.nattable.group.model;

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

/**
 * An implementation of {@link IRowGroup}.
 *
 * @author Stefan Bolton
 *
 * @param <T>
 *            the type of the row objects.
 */
public class RowGroup<T> implements IRowGroup<T> {

    private String groupName;
    private boolean collapsed;
    private boolean collapseable;

    // The model is needed to a) notify of any changes made (so it can notify
    // IRowGroupModelListeners) and b) it keeps a 'master-map' of all the row
    // position-to-row objects added into any of the row groups.
    private final RowGroupModel<T> rowGroupModel;

    // The rows and static rows in this group.
    private List<T> rowMembers;
    private List<T> staticRowMembers;

    // A list of child groups.
    private List<IRowGroup<T>> childGroups;

    // Null for top-level groups.
    private IRowGroup<T> parentGroup;

    // Arbitrary data tagged onto a group.
    private Map<String, Object> data;

    private static final String DEFAULT_DATA_KEY = "defaultKey"; //$NON-NLS-1$

    public RowGroup(final RowGroupModel<T> rowGroupModel, final String groupName) {
        this.rowGroupModel = rowGroupModel;
        init(groupName, false);
        this.collapseable = true;
    }

    public RowGroup(final RowGroupModel<T> rowGroupModel,
            final String groupName, final boolean collapsed) {
        this.rowGroupModel = rowGroupModel;
        init(groupName, collapsed);
    }

    private void init(final String groupName, final boolean collapsed) {
        this.groupName = groupName;
        this.collapsed = collapsed;
        this.collapseable = true;
        this.rowMembers = Collections.synchronizedList(new ArrayList<T>());
        this.staticRowMembers = Collections
                .synchronizedList(new ArrayList<T>());
        this.childGroups = Collections
                .synchronizedList(new ArrayList<IRowGroup<T>>());
        this.data = new HashMap<>();
    }

    @Override
    public String getGroupName() {
        return this.groupName;
    }

    @Override
    public Object getData() {
        return this.data.get(DEFAULT_DATA_KEY);
    }

    @Override
    public Object getData(String key) {
        return this.data.get(key);
    }

    @Override
    public void setData(Object data) {
        this.data.put(DEFAULT_DATA_KEY, data);
    }

    @Override
    public void setData(String key, Object data) {
        this.data.put(key, data);
    }

    @Override
    public boolean isCollapsed() {
        return this.collapsed;
    }

    @Override
    public boolean isCollapseable() {
        return this.collapseable;
    }

    public void setCollapseable(boolean collapseable) {
        this.collapseable = collapseable;
    }

    @Override
    public void collapse() {
        if (isCollapseable() && !isCollapsed()) {
            this.collapsed = true;
            this.rowGroupModel.notifyListeners();
        }
    }

    @Override
    public void expand() {
        if (isCollapseable() && isCollapsed()) {
            this.collapsed = false;
            this.rowGroupModel.notifyListeners();
        }
    }

    private void addMemberRowAndCache(final List<T> rows, final T row) {
        rows.add(row);
        this.rowGroupModel.addMemberRow(row, this);
    }

    @Override
    public void addMemberRow(final T row) {
        addMemberRowAndCache(this.rowMembers, row);
        this.rowGroupModel.notifyListeners();
    }

    @Override
    public void addStaticMemberRow(final T row) {
        addMemberRowAndCache(this.staticRowMembers, row);
        this.rowGroupModel.notifyListeners();
    }

    @Override
    public void addMemberRows(final List<T> rows) {
        for (T row : rows) {
            addMemberRowAndCache(this.rowMembers, row);
        }
        this.rowGroupModel.notifyListeners();
    }

    private boolean removeMemberRowFromCache(final T row) {
        boolean removed = false;

        // Remove the row from our group.
        if (row != null) {
            removed = this.rowMembers.remove(row);

            // Try removing a static row instead.
            if (!removed) {
                removed = this.staticRowMembers.remove(row);
            }

            if (removed) {
                // Bump row positions to compensate.
                this.rowGroupModel.removeMemberRow(row);

                if ((getOwnMemberRows(false).isEmpty())
                        && (getRowGroups().isEmpty())) {
                    // If there are no more member rows, then clean-up any
                    // static rows and remove the group from the model.
                    for (T staticRow : this.getOwnStaticMemberRows()) {
                        this.rowGroupModel.removeMemberRow(staticRow);
                    }

                    this.staticRowMembers.clear();
                    this.rowGroupModel.removeRowGroup(this);

                    if (this.parentGroup != null) {
                        this.parentGroup.removeRowGroup(this);
                    }
                }

            } else {
                // Try sub-groups.
                synchronized (this.childGroups) {
                    for (final IRowGroup<T> rowGroup : this.childGroups) {
                        removed = ((RowGroup<T>) rowGroup).removeMemberRow(row);

                        if (removed) {
                            // Remove empty child groups from the model.
                            if (rowGroup.getOwnMemberRows(false).isEmpty()) {
                                this.childGroups.remove(rowGroup);
                            }
                            break;
                        }
                    }
                }
            }
        }

        return removed;
    }

    @Override
    public boolean removeMemberRow(final T row) {

        boolean removed = removeMemberRowFromCache(row);

        if (removed) {
            this.rowGroupModel.notifyListeners();
        }

        return removed;
    }

    @Override
    public void removeMemberRows(final List<T> rows) {
        boolean removed = false;

        for (T row : rows) {
            removed |= removeMemberRowFromCache(row);
        }

        if (removed) {
            this.rowGroupModel.notifyListeners();
        }
    }

    @Override
    public IRowGroup<T> getParentGroup() {
        return this.parentGroup;
    }

    @Override
    public void setParentGroup(IRowGroup<T> parentGroup) {
        this.parentGroup = parentGroup;
    }

    /*
     * (non-Javadoc)
     *
     * @see com.gresham.darwin.ui.widgets.grid.data.rowgroups.IRowGroup#
     * addMemberRowGroup
     * (com.gresham.darwin.ui.widgets.grid.data.rowgroups.IRowGroup)
     */
    @Override
    public void addRowGroup(final IRowGroup<T> rowGroup) {
        rowGroup.setParentGroup(this);
        this.childGroups.add(rowGroup);
        this.rowGroupModel.notifyListeners();
    }

    /*
     * (non-Javadoc)
     *
     * @see com.gresham.darwin.ui.widgets.grid.data.rowgroups.IRowGroup#
     * removeMemberRowGroup
     * (com.gresham.darwin.ui.widgets.grid.data.rowgroups.IRowGroup)
     */
    @Override
    public boolean removeRowGroup(final IRowGroup<T> rowGroup) {
        // Remove all members in the group.
        rowGroup.setParentGroup(null);
        rowGroup.clear();
        boolean removed = this.childGroups.remove(rowGroup);
        this.rowGroupModel.notifyListeners();
        return removed;
    }

    /*
     * (non-Javadoc)
     *
     * @see com.gresham.darwin.ui.widgets.grid.data.rowgroups.IRowGroup#
     * getMemnerRowGroups()
     */
    @Override
    public List<IRowGroup<T>> getRowGroups() {
        return Collections.unmodifiableList(this.childGroups);
    }

    /*
     * (non-Javadoc)
     *
     * @see com.gresham.darwin.ui.widgets.grid.data.rowgroups.IRowGroup#
     * getOwnMemberRows ()
     */
    @Override
    public List<T> getOwnMemberRows(final boolean includeStaticRows) {
        List<T> rows = new ArrayList<>(this.rowMembers);

        if (includeStaticRows) {
            rows.addAll(this.staticRowMembers);
        }

        return Collections.unmodifiableList(rows);
    }

    /*
     * (non-Javadoc)
     *
     * @see com.gresham.darwin.ui.widgets.grid.data.rowgroups.IRowGroup#
     * getOwnStaticMemberRows()
     */
    @Override
    public List<T> getOwnStaticMemberRows() {
        return Collections.unmodifiableList(this.staticRowMembers);
    }

    @Override
    public void clear() {
        synchronized (this.childGroups) {
            for (final IRowGroup<T> rowGroup : this.childGroups) {
                rowGroup.clear();
            }
        }

        synchronized (this.rowMembers) {
            for (T row : new ArrayList<T>(this.rowMembers)) {
                removeMemberRow(row);
            }
        }

        synchronized (this.staticRowMembers) {
            for (T row : new ArrayList<T>(this.staticRowMembers)) {
                removeMemberRow(row);
            }
        }
    }

    @Override
    public List<T> getMemberRows(final boolean includeStaticRows) {
        final List<T> memberRows = new ArrayList<>();

        // Return all the member rows from nested groups.
        synchronized (this.childGroups) {
            for (final IRowGroup<T> rowGroup : this.childGroups) {
                memberRows.addAll(rowGroup.getMemberRows(includeStaticRows));
            }
        }

        // Add all of our immediate child rows.
        memberRows.addAll(getOwnMemberRows(includeStaticRows));

        return Collections.unmodifiableList(memberRows);
    }

    @Override
    public List<T> getStaticMemberRows() {
        final List<T> staticMemberRows = new ArrayList<>();

        // Return all the member rows from nested groups.
        synchronized (this.childGroups) {
            for (final IRowGroup<T> rowGroup : this.childGroups) {
                staticMemberRows.addAll(rowGroup.getStaticMemberRows());
            }
        }

        // Add all of our immediate child rows.
        staticMemberRows.addAll(getOwnStaticMemberRows());

        return Collections.unmodifiableList(staticMemberRows);
    }

    @Override
    public IRowGroup<T> getRowGroupForRow(final T row) {
        IRowGroup<T> group = null;

        if (getOwnMemberRows(true).contains(row)) {
            group = this;

        } else {
            synchronized (this.childGroups) {
                for (final IRowGroup<T> rowGroup : this.childGroups) {
                    group = rowGroup.getRowGroupForRow(row);

                    if (group != null) {
                        break;
                    }
                }
            }
        }

        return group;
    }

    @Override
    public boolean isEmpty() {

        boolean empty = true;

        synchronized (this.childGroups) {
            for (final IRowGroup<T> rowGroup : this.childGroups) {
                empty &= rowGroup.isEmpty();
            }
        }

        return (empty && ((this.rowMembers.size() + this.staticRowMembers.size()) == 0));
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append(String.format("Name      : %s\n", getGroupName())); //$NON-NLS-1$
        sb.append(String.format("Collapsed : %s\n", isCollapsed())); //$NON-NLS-1$
        sb.append("Members   : \n"); //$NON-NLS-1$

        for (T row : getOwnMemberRows(false)) {
            sb.append(String.format("%s", row.toString())); //$NON-NLS-1$
        }

        for (T row : getOwnStaticMemberRows()) {
            sb.append(String.format("*%s", row.toString())); //$NON-NLS-1$
        }

        if (!this.childGroups.isEmpty()) {
            sb.append(String.format("Start Child Groups for [%s] :- \n", getGroupName())); //$NON-NLS-1$
            for (final IRowGroup<T> rowGroup : this.childGroups) {
                sb.append(rowGroup.toString());
            }
            sb.append(String.format("End Child Groups for [%s]\n", getGroupName())); //$NON-NLS-1$
        }

        return sb.toString();
    }
}
