// License: GPL. For details, see LICENSE file.
package org.openstreetmap.josm.plugins.turnrestrictions.editor;

import static org.openstreetmap.josm.tools.I18n.tr;

import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

import javax.swing.DefaultListSelectionModel;
import javax.swing.table.AbstractTableModel;

import org.openstreetmap.josm.data.osm.DefaultNameFormatter;
import org.openstreetmap.josm.data.osm.OsmPrimitive;
import org.openstreetmap.josm.data.osm.PrimitiveId;
import org.openstreetmap.josm.data.osm.Relation;
import org.openstreetmap.josm.data.osm.RelationMember;
import org.openstreetmap.josm.gui.layer.OsmDataLayer;
import org.openstreetmap.josm.tools.CheckParameterUtil;

public class RelationMemberEditorModel extends AbstractTableModel {
    private final ArrayList<RelationMemberModel> members = new ArrayList<>();
    private OsmDataLayer layer;
    private DefaultListSelectionModel rowSelectionModel;
    private DefaultListSelectionModel colSelectionModel;

    /**
     * Creates a new model in the context of an {@link OsmDataLayer}. Internally allocates
     * a row and a column selection model, see {@link #getRowSelectionModel()} and
     * {@link #getColSelectionModel()}.
     *
     * @param layer the data layer. Must not be null.
     * @exception IllegalArgumentException thrown if layer is null
     */
    public RelationMemberEditorModel(OsmDataLayer layer) throws IllegalArgumentException {
        CheckParameterUtil.ensureParameterNotNull(layer, "layer");
        this.layer = layer;
        rowSelectionModel = new DefaultListSelectionModel();
        colSelectionModel = new DefaultListSelectionModel();
    }

    /**
     *  Creates a new model in the context of an {@link OsmDataLayer}
     *
     * @param layer layer the data layer. Must not be null.
     * @param rowSelectionModel the row selection model. Must not be null.
     * @param colSelectionModel the column selection model. Must not be null.
     * @throws IllegalArgumentException thrown if layer is null
     * @throws IllegalArgumentException thrown if rowSelectionModel is null
     * @throws IllegalArgumentException thrown if colSelectionModel is null
     */
    public RelationMemberEditorModel(OsmDataLayer layer, DefaultListSelectionModel rowSelectionModel,
            DefaultListSelectionModel colSelectionModel) throws IllegalArgumentException {
        CheckParameterUtil.ensureParameterNotNull(layer, "layer");
        CheckParameterUtil.ensureParameterNotNull(rowSelectionModel, "rowSelectionModel");
        CheckParameterUtil.ensureParameterNotNull(colSelectionModel, "colSelectionModel");
        this.layer = layer;
        this.rowSelectionModel = rowSelectionModel;
        this.colSelectionModel = colSelectionModel;
    }

    /**
     * Replies the row selection model used in this table model.
     *
     * @return the row selection model
     */
    public DefaultListSelectionModel getRowSelectionModel() {
        return rowSelectionModel;
    }

    /**
     * Replies the column selection model used in this table model.
     *
     * @return the col selection model
     */
    public DefaultListSelectionModel getColSelectionModel() {
        return colSelectionModel;
    }

    /**
     * Replies the set of {@link OsmPrimitive}s with the role {@code role}. If no
     * such primitives exists, the empty set is returned.
     *
     * @return the set of {@link OsmPrimitive}s with the role {@code role}
     */
    protected Set<OsmPrimitive> getPrimitivesWithRole(String role) {
        HashSet<OsmPrimitive> ret = new HashSet<>();
        for (RelationMemberModel rm: members) {
            if (rm.getRole().equals(role)) {
                OsmPrimitive p = layer.data.getPrimitiveById(rm.getTarget());
                if (p != null) {
                    ret.add(p);
                }
            }
        }
        return ret;
    }

    /**
     * Replies the list of {@link RelationMemberModel}s with the role {@code role}. If no
     * such primitives exists, the empty set is returned.
     *
     * @return the set of {@link RelationMemberModel}s with the role {@code role}
     */
    protected List<RelationMemberModel> getRelationMembersWithRole(String role) {
        ArrayList<RelationMemberModel> ret = new ArrayList<>();
        for (RelationMemberModel rm: members) {
            if (rm.getRole().equals(role)) {
                ret.add(rm);
            }
        }
        return ret;
    }

    /**
     * Removes all members with role {@code role}.
     *
     * @param role the role. Ignored if null.
     * @return true if the list of members was modified; false, otherwise
     */
    protected boolean removeMembersWithRole(String role) {
        if (role == null) return false;
        boolean isChanged = false;
        for (Iterator<RelationMemberModel> it = members.iterator(); it.hasNext();) {
            RelationMemberModel rm = it.next();
            if (rm.getRole().equals(role)) {
                it.remove();
                isChanged = true;
            }
        }
        return isChanged;
    }

    /**
     * Replies the set of {@link OsmPrimitive}s with the role 'from'. If no
     * such primitives exists, the empty set is returned.
     *
     * @return the set of {@link OsmPrimitive}s with the role 'from'
     */
    public Set<OsmPrimitive> getFromPrimitives() {
        return getPrimitivesWithRole("from");
    }

    /**
     * Replies the set of {@link OsmPrimitive}s with the role 'to'. If no
     * such primitives exists, the empty set is returned.
     *
     * @return the set of {@link OsmPrimitive}s with the role 'from'
     */
    public Set<OsmPrimitive> getToPrimitives() {
        return getPrimitivesWithRole("to");
    }

    /**
     * Replies the list of 'via' objects in the order they occur in the
     * member list. Replies an empty list if no vias exist
     */
    public List<OsmPrimitive> getVias() {
        ArrayList<OsmPrimitive> ret = new ArrayList<>();
        for (RelationMemberModel rm: getRelationMembersWithRole("via")) {
            ret.add(layer.data.getPrimitiveById(rm.getTarget()));
        }
        return ret;
    }

    /**
     * Sets the list of vias. Removes all 'vias' if {@code vias} is null.
     *
     * null vias are skipped. A via must belong to the dataset of the layer in whose context
     * this editor is working, otherwise an {@link IllegalArgumentException} is thrown.
     *
     * @param vias the vias.
     * @exception IllegalArgumentException thrown if a via doesn't belong to the dataset of the layer
     * in whose context this editor is working
     */
    public void setVias(List<OsmPrimitive> vias) throws IllegalArgumentException {
        boolean viasDeleted = removeMembersWithRole("via");
        if (vias == null || vias.isEmpty()) {
            if (viasDeleted) {
                fireTableDataChanged();
            }
            return;
        }
        // check vias
        for (OsmPrimitive via: vias) {
            if (via == null) continue;
            if (via.getDataSet() == null || via.getDataSet() != layer.data) {
                throw new IllegalArgumentException(MessageFormat.format("via object ''{0}'' must belong to dataset of layer ''{1}''",
                        via.getDisplayName(DefaultNameFormatter.getInstance()), layer.getName()));
            }
        }
        // add vias
        for (OsmPrimitive via: vias) {
            if (via == null) continue;
            RelationMemberModel model = new RelationMemberModel("via", via);
            members.add(model);
        }
        fireTableDataChanged();
    }

    /**
     * Sets the turn restriction member with role {@code role}. Removes all
     * members with role {@code role} if {@code id} is null.
     *
     * @param id the id
     * @return true if the model was modified; false, otherwise
     */
    protected boolean setPrimitiveWithRole(PrimitiveId id, String role) {
        if (id == null) {
            return removeMembersWithRole(role);
        }

        List<RelationMemberModel> fromMembers = getRelationMembersWithRole(role);
        if (fromMembers.isEmpty()) {
            RelationMemberModel rm = new RelationMemberModel(role, id);
            members.add(rm);
            return true;
        } else if (fromMembers.size() == 1) {
            RelationMemberModel rm = fromMembers.get(0);
            if (!rm.getTarget().equals(id)) {
                rm.setTarget(id);
                return true;
            }
            return false;
        } else {
            removeMembersWithRole(role);
            RelationMemberModel rm = new RelationMemberModel(role, id);
            members.add(rm);
            return true;
        }
    }

    /**
     * Sets the turn restriction member with role 'from'. Removes all
     * members with role 'from' if {@code id} is null.
     *
     * @param id the id
     */
    public void setFromPrimitive(PrimitiveId id) {
        if (setPrimitiveWithRole(id, "from")) {
            fireTableDataChanged();
        }
    }

    /**
     * Sets the turn restriction member with role 'to'. Removes all
     * members with role 'to' if {@code id} is null.
     *
     * @param id the id
     */
    public void setToPrimitive(PrimitiveId id) {
        if (setPrimitiveWithRole(id, "to")) {
            fireTableDataChanged();
        }
    }

    /**
     * Replies the set of {@link OsmPrimitive}s referred to by members in
     * this model.
     *
     * @return the set of {@link OsmPrimitive}s referred to by members in
     * this model.
     */
    public Set<OsmPrimitive> getMemberPrimitives() {
        Set<OsmPrimitive> ret = new HashSet<>();
        for (RelationMemberModel rm: members) {
            OsmPrimitive p = layer.data.getPrimitiveById(rm.getTarget());
            if (p != null) ret.add(p);
        }
        return ret;
    }

    /**
     * Populates the model with the relation member of a turn restriction. Clears
     * the model if {@code tr} is null.
     *
     * @param tr the turn restriction
     */
    public void populate(Relation tr) {
        members.clear();
        if (tr == null) {
            fireTableDataChanged();
            return;
        }
        for (RelationMember rm: tr.getMembers()) {
            members.add(new RelationMemberModel(rm));
        }
        fireTableDataChanged();
    }

    /**
     * Replaces the member of turn restriction {@code tr} by the relation members currently
     * edited in this model.
     *
     * @param tr the turn restriction. Ignored if null.
     */
    public void applyTo(Relation tr) {
        if (tr == null) return;
        List<RelationMember> newMembers = new ArrayList<>();
        for (RelationMemberModel model: members) {
            RelationMember rm = new RelationMember(model.getRole(), layer.data.getPrimitiveById(model.getTarget()));
            newMembers.add(rm);
        }
        tr.setMembers(newMembers);
    }

    /**
     * Clears the roles of all relation members currently selected in the
     * table.
     */
    protected void clearSelectedRoles() {
        for (int i = 0; i < getRowCount(); i++) {
            if (rowSelectionModel.isSelectedIndex(i)) {
                members.get(i).setRole("");
            }
        }
    }

    /**
     * Removes the currently selected rows from the model
     */
    protected void removedSelectedMembers() {
        for (int i = getRowCount()-1; i >= 0; i--) {
            if (rowSelectionModel.isSelectedIndex(i)) {
                members.remove(i);
            }
        }
    }

    /**
     * Deletes the current selection.
     *
     * If only cells in the first column are selected, the roles of the selected
     * members are reset to the empty string. Otherwise the selected members are
     * removed from the model.
     *
     */
    public void deleteSelected() {
        if (colSelectionModel.isSelectedIndex(0) && !colSelectionModel.isSelectedIndex(1)) {
            clearSelectedRoles();
        } else if (rowSelectionModel.getMinSelectionIndex() >= 0) {
            removedSelectedMembers();
        }
        fireTableDataChanged();
    }

    protected List<Integer> getSelectedIndices() {
        ArrayList<Integer> ret = new ArrayList<>();
        for (int i = 0; i < members.size(); i++) {
            if (rowSelectionModel.isSelectedIndex(i))
                ret.add(i);
        }
        return ret;
    }

    public boolean canMoveUp() {
        List<Integer> sel = getSelectedIndices();
        if (sel.isEmpty()) return false;
        return sel.get(0) > 0;
    }

    public boolean canMoveDown() {
        List<Integer> sel = getSelectedIndices();
        if (sel.isEmpty()) return false;
        return sel.get(sel.size()-1) < members.size()-1;
    }

    public void moveUpSelected() {
        if (!canMoveUp()) return;
        List<Integer> sel = getSelectedIndices();
        for (int idx: sel) {
            RelationMemberModel m = members.remove(idx);
            members.add(idx-1, m);
        }
        fireTableDataChanged();
        rowSelectionModel.clearSelection();
        colSelectionModel.setSelectionInterval(0, 1);
        for (int idx: sel) {
            rowSelectionModel.addSelectionInterval(idx-1, idx-1);
        }
    }

    public void moveDownSelected() {
        if (!canMoveDown()) return;
        List<Integer> sel = getSelectedIndices();
        for (int i = sel.size()-1; i >= 0; i--) {
            int idx = sel.get(i);
            RelationMemberModel m = members.remove(idx);
            members.add(idx+1, m);
        }
        fireTableDataChanged();
        rowSelectionModel.clearSelection();
        colSelectionModel.setSelectionInterval(0, 1);
        for (int idx: sel) {
            rowSelectionModel.addSelectionInterval(idx+1, idx+1);
        }
    }

    /**
     * <p>Inserts a list of new relation members with the empty role for the primitives
     * with id in {@code ids}. Inserts the new primitives at the position of the first
     * selected row. If no row is selected, at the end of the list.</p>
     *
     * <p> null values are skipped. If there is an id for which there is no primitive in the context
     *  layer, if the primitive is deleted or invisible, an {@link IllegalArgumentException}
     *  is thrown and nothing is inserted.</p>
     *
     * @param ids the list of ids. Ignored if null.
     * @throws IllegalArgumentException thrown if one of the ids can't be inserted
     */
    public void insertMembers(Collection<PrimitiveId> ids) throws IllegalArgumentException {
        if (ids == null) return;
        ArrayList<RelationMemberModel> newMembers = new ArrayList<>();
        for (PrimitiveId id: ids) {
            OsmPrimitive p = layer.data.getPrimitiveById(id);
            if (p == null) {
                throw new IllegalArgumentException(tr("Cannot find object with id ''{0}'' in layer ''{1}''", id.toString(), layer.getName()));
            }
            if (p.isDeleted() || !p.isVisible()) {
                throw new IllegalArgumentException(
                        tr("Cannot add object ''{0}'' as relation member because it is deleted or invisible in layer ''{1}''",
                        p.getDisplayName(DefaultNameFormatter.getInstance()), layer.getName()));
            }
            newMembers.add(new RelationMemberModel("", id));
        }
        if (newMembers.isEmpty()) return;
        int insertPos = rowSelectionModel.getMinSelectionIndex();
        if (insertPos >= 0) {
            members.addAll(insertPos, newMembers);
        } else {
            members.addAll(newMembers);
        }
        fireTableDataChanged();
        if (insertPos < 0) insertPos = 0;
        colSelectionModel.setSelectionInterval(0, 1); // select both columns
        rowSelectionModel.setSelectionInterval(insertPos, insertPos + newMembers.size()-1);
    }

    @Override
    public int getColumnCount() {
        return 2;
    }

    @Override
    public int getRowCount() {
        return members.size();
    }

    @Override
    public Object getValueAt(int rowIndex, int columnIndex) {
        switch(columnIndex) {
        case 0: return members.get(rowIndex).getRole();
        case 1: return layer.data.getPrimitiveById(members.get(rowIndex).getTarget());
        }
        return null;
    }

    @Override
    public boolean isCellEditable(int rowIndex, int columnIndex) {
        // only the column with the member roles is editable
        return columnIndex == 0;
    }

    @Override
    public void setValueAt(Object aValue, int rowIndex, int columnIndex) {
        if (columnIndex != 0) return;
        String role = (String) aValue;
        RelationMemberModel model = members.get(rowIndex);
        model.setRole(role);
        fireTableCellUpdated(rowIndex, columnIndex);
    }
}
