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

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

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Observable;
import java.util.Observer;

import javax.swing.AbstractListModel;
import javax.swing.DefaultListSelectionModel;

import org.openstreetmap.josm.data.osm.DataSet;
import org.openstreetmap.josm.data.osm.OsmPrimitive;
import org.openstreetmap.josm.data.osm.PrimitiveId;
import org.openstreetmap.josm.plugins.turnrestrictions.dnd.PrimitiveIdListProvider;
import org.openstreetmap.josm.tools.CheckParameterUtil;

/**
 * ViaListModel is a model for the list of 'via' objects of a turn restriction.
 *
 */
public class ViaListModel extends AbstractListModel<OsmPrimitive> implements PrimitiveIdListProvider, Observer {

    private DefaultListSelectionModel selectionModel;
    private final ArrayList<OsmPrimitive> vias = new ArrayList<>();
    private TurnRestrictionEditorModel model;

    /**
     * Constructor
     *
     * @param model the turn restriction editor model. Must not be null.
     * @param selectionModel the selection model. Must not be null.
     * @throws IllegalArgumentException thrown if model is null
     * @throws IllegalArgumentException thrown if selectionModel is null
     */
    public ViaListModel(TurnRestrictionEditorModel model, DefaultListSelectionModel selectionModel) {
        CheckParameterUtil.ensureParameterNotNull(model, "model");
        CheckParameterUtil.ensureParameterNotNull(selectionModel, "selectionModel");
        this.model = model;
        this.selectionModel = selectionModel;
        model.addObserver(this);
        refresh();
    }

    /**
     * Replies the list of currently selected vias
     *
     * @return the list of currently selected vias
     */
    public List<OsmPrimitive> getSelectedVias() {
        ArrayList<OsmPrimitive> ret = new ArrayList<>();
        for (int i = 0; i < getSize(); i++) {
            if (selectionModel.isSelectedIndex(i)) {
                ret.add(vias.get(i));
            }
        }
        return ret;
    }

    /**
     * Sets the collection of currently selected vias
     *
     *  @param vias a collection of vias
     */
    public void setSelectedVias(Collection<OsmPrimitive> vias) {
        selectionModel.clearSelection();
        if (vias == null) return;
        for (OsmPrimitive via: vias) {
            int idx = this.vias.indexOf(via);
            if (idx < 0) continue;
            selectionModel.addSelectionInterval(idx, idx);
        }
    }

    /**
     * Replies the list of selected rows
     *
     * @return the list of selected rows
     */
    public List<Integer> getSelectedRows() {
        ArrayList<Integer> ret = new ArrayList<>();
        for (int i = 0; i < getSize(); i++) {
            if (selectionModel.isSelectedIndex(i)) {
                ret.add(i);
            }
        }
        return ret;
    }

    protected List<Integer> moveUp(List<Integer> rows, int targetRow) {
        List<Integer> ret = new ArrayList<>(rows.size());
        int delta = rows.get(0) - targetRow;
        for (int row: rows) {
            OsmPrimitive via = vias.remove(row);
            vias.add(row - delta, via);
            ret.add(row - delta);
        }
        return ret;
    }

    protected List<Integer> moveDown(List<Integer> rows, int targetRow) {
        List<Integer> ret = new ArrayList<>(rows.size());
        int delta = targetRow - rows.get(0);
        for (int i = rows.size()-1; i >= 0; i--) {
            int row = rows.get(i);
            OsmPrimitive via = vias.remove(row);
            vias.add(row + delta, via);
            ret.add(row + delta);
        }
        return ret;
    }

    public void moveVias(List<Integer> selectedRows, int targetRow) {
        if (selectedRows == null) return;
        if (selectedRows.size() == 1) {
            int sourceRow = selectedRows.get(0);
            if (sourceRow == targetRow) return;
            OsmPrimitive via = vias.remove(sourceRow);
            vias.add(targetRow, via);
            fireContentsChanged(this, 0, getSize());
            selectionModel.setSelectionInterval(targetRow, targetRow);
            return;
        }
        int min = selectedRows.get(0);
        int max = selectedRows.get(selectedRows.size()-1);
        if (targetRow < min) {
            selectedRows = moveUp(selectedRows, targetRow);
        } else if (targetRow == min) {
            // do nothing
        } else if (targetRow - min < getSize() - max) {
            int delta = Math.min(targetRow - min, getSize()-1 - max);
            targetRow = min + delta;
            if (targetRow > min) {
                selectedRows = moveDown(selectedRows, targetRow);
            }
        }
        fireContentsChanged(this, 0, getSize());
        selectionModel.clearSelection();
        for (int row: selectedRows) {
            selectionModel.addSelectionInterval(row, row);
        }
    }

    /**
     * Move the currently selected vias up by one position
     */
    public void moveUp() {
        List<Integer> sel = getSelectedRows();
        if (sel.isEmpty() || sel.get(0) == 0) return;
        moveVias(sel, sel.get(0)-1);
    }

    /**
     * Move the currently selected vias down by one position
     */
    public void moveDown() {
        List<Integer> sel = getSelectedRows();
        if (sel.isEmpty() || sel.get(sel.size()-1) == getSize()-1) return;
        moveVias(sel, sel.get(sel.size()-1)+1);
    }

    /**
     * Inserts a list of OSM objects given by OSM primitive ids.
     *
     * @param idsToInsert the ids of the objects to insert
     */
    public void insertVias(List<PrimitiveId> idsToInsert) {
        if (idsToInsert == null) return;
        List<OsmPrimitive> primitives = new ArrayList<>(idsToInsert.size());
        DataSet ds = model.getLayer().getDataSet();
        for (PrimitiveId id: idsToInsert) {
            OsmPrimitive p = ds.getPrimitiveById(id);
            if (p == null) {
                System.out.println(tr("Failed to retrieve OSM object with id {0} from dataset {1}. Cannot add it as ''via''.", id, ds));
                continue;
            }
            primitives.add(p);
        }
        int targetRow = Math.max(selectionModel.getMinSelectionIndex(), 0);
        List<OsmPrimitive> newVias = new ArrayList<>(vias);
        newVias.addAll(targetRow, primitives);
        model.setVias(newVias);
        fireContentsChanged(this, 0, getSize());
        selectionModel.clearSelection();
        for (int i = targetRow; i < targetRow + primitives.size(); i++) {
            selectionModel.addSelectionInterval(i, i);
        }
    }

    /**
     * Removes the currently selected vias
     */
    public void removeSelectedVias() {
        ArrayList<OsmPrimitive> newVias = new ArrayList<>(vias);
        int j = 0;
        for (int i = 0; i < getSize(); i++) {
            if (!selectionModel.isSelectedIndex(i)) continue;
            newVias.remove(i-j);
            j++;
        }
        if (j == 0) return; // nothing selected, nothing deleted
        model.setVias(newVias);
    }

    /**
     * Refreshes the list of 'vias' in this model with the current list of
     * vias from the turn restriction model.
     */
    protected void refresh() {
        List<OsmPrimitive> sel = getSelectedVias();
        vias.clear();
        vias.addAll(model.getVias());
        fireContentsChanged(this, 0, getSize());
        setSelectedVias(sel);
    }

    @Override
    public OsmPrimitive getElementAt(int index) {
        return vias.get(index);
    }

    @Override
    public int getSize() {
        return vias.size();
    }

    /* ----------------------------------------------------------------------- */
    /* interface PrimitiveIdListProvider                                       */
    /* ----------------------------------------------------------------------- */
    @Override
    public List<PrimitiveId> getSelectedPrimitiveIds() {
        ArrayList<PrimitiveId> ids = new ArrayList<>();
        for (OsmPrimitive p: getSelectedVias()) {
            ids.add(p.getPrimitiveId());
        }
        return ids;
    }

    /* ----------------------------------------------------------------------- */
    /* interface Observer                                                      */
    /* ----------------------------------------------------------------------- */
    @Override
    public void update(Observable o, Object arg) {
        refresh();
    }
}
