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

import java.util.ArrayList;
import java.util.List;

import org.openstreetmap.josm.data.gpx.GpxData;
import org.openstreetmap.josm.data.gpx.GpxRoute;
import org.openstreetmap.josm.data.gpx.IGpxTrack;
import org.openstreetmap.josm.data.gpx.IGpxTrackSegment;
import org.openstreetmap.josm.data.gpx.IWithAttributes;
import org.openstreetmap.josm.data.gpx.WayPoint;
import org.openstreetmap.josm.plugins.elevation.IElevationModel;
import org.openstreetmap.josm.plugins.elevation.IElevationModelListener;
import org.openstreetmap.josm.plugins.elevation.IElevationProfile;
import org.openstreetmap.josm.tools.CheckParameterUtil;

/**
 * Represents the top-level part of the elevation model. The elevation model
 * breaks done into the tracks/routes of a GPX file.
 *
 * @author Oliver Wieland &lt;oliver.wieland@online.de&gt;
 * @see IElevationModel
 */
public class ElevationModel implements IGpxVisitor, IElevationModel {
    private int trackCounter;
    private final GpxData gpxData;
    private final String name;
    private final WayPointMap profiles = new WayPointMap();
    private final List<IElevationModelListener> listeners = new ArrayList<>();
    private final List<WayPoint> buffer = new ArrayList<>();
    private int currentProfileIndex = 0;
    private ElevationProfile curProfile = null;

    /**
     * Instantiates a new elevation model.
     */
    public ElevationModel() {
        this("", null);
    }

    /**
     * Instantiates a new elevation model.
     *
     * @param name the name of the model
     * @param data the GPX data
     */
    public ElevationModel(String name, GpxData data) {
        gpxData = data;
        this.name = name;
        GpxIterator.visit(data, this);
    }

    /**
     * Gets the GPX data instance used by this model.
     */
    public GpxData getGpxData() {
        return gpxData;
    }

    /**
     * Return the tracks of the elevation model
     * @return the tracks
     */
    protected WayPointMap getTracks() {
        return profiles;
    }

    /**
     * Fires the 'model changed' event to all listeners.
     */
    protected void fireModelChanged() {
        for (IElevationModelListener listener : listeners) {
            if (profiles != null && profiles.size() > 0)
                listener.elevationProfileChanged(getCurrentProfile());
        }
    }

    @Override
    public void addModelListener(IElevationModelListener listener) {
        this.listeners.add(listener);
    }

    @Override
    public void removeModelListener(IElevationModelListener listener) {
        this.listeners.remove(listener);
    }

    @Override
    public void removeAllListeners() {
        this.listeners.clear();
    }

    @Override
    public List<IElevationProfile> getProfiles() {
        return profiles;
    }

    @Override
    public IElevationProfile getCurrentProfile() {
        if (currentProfileIndex < 0 || currentProfileIndex >= profileCount()) return null;

        return profiles.get(currentProfileIndex);
    }

    @Override
    public void setCurrentProfile(IElevationProfile newProfile) {
        CheckParameterUtil.ensureParameterNotNull(newProfile);

        if (!profiles.contains(newProfile)) {
            profiles.add(newProfile);
        }

        setCurrentProfile(profiles.indexOf(newProfile));
    }

    @Override
    public void setCurrentProfile(int index) {
        if (index < 0 || index >= profileCount())
            throw new RuntimeException("Invalid arg for setCurrentProfile: " + index + ", value must be 0.." + profileCount());

        currentProfileIndex = index;
        fireModelChanged();
    }

    @Override
    public int profileCount() {
        return profiles != null ? profiles.size() : 0;
    }

    // Visitor stuff starts here...

    @Override
    public void beginWayPoints() {
        // we ignore single way points (elevation profile is quite meaningless...)
    }

    @Override
    public void endWayPoints() {
        // we ignore single way points (elevation profile is quite meaningless...)
    }

    @Override
    public void visitWayPoint(WayPoint wp) {
        // we ignore single way points (elevation profile is quite meaningless...)
    }

    @Override
    public void beginTrack(IGpxTrack track) {
        createProfile(track);
    }

    @Override
    public void endTrack(IGpxTrack track) {
        if (curProfile == null) throw new RuntimeException("Internal error: No elevation profile");

        curProfile.setDistance(track.length());
        commitProfile();
    }

    @Override
    public void beginTrackSegment(IGpxTrack track, IGpxTrackSegment segment) {
        // Nothing to do here for now
    }

    @Override
    public void endTrackSegment(IGpxTrack track, IGpxTrackSegment segment) {
        // Nothing to do here for now
    }

    @Override
    public void visitTrackPoint(WayPoint wp, IGpxTrack track, IGpxTrackSegment segment) {
        processWayPoint(wp);
    }

    @Override
    public void beginRoute(GpxRoute route) {
        createProfile(route);
    }

    @Override
    public void endRoute(GpxRoute route) {
        if (curProfile == null) throw new RuntimeException("Internal error: No elevation profile");
        // a GpxRoute has no 'length' property
        curProfile.setDistance(0);
        commitProfile();
    }

    @Override
    public void visitRoutePoint(WayPoint wp, GpxRoute route) {
        processWayPoint(wp);
    }

    /**
     * Creates a new profile.
     *
     * @param trackOrRoute the track or route
     */
    private void createProfile(IWithAttributes trackOrRoute) {
        // check GPX data
        String trackName = (String) trackOrRoute.get("name");

        if (trackName == null) {
            trackName = (String) trackOrRoute.get(GpxData.META_NAME);
            if (trackName == null) {
                // no name given, build artificial one
                trackName = name + "." + trackCounter;
            }
        }

        curProfile = new ElevationProfile(trackName);
    }

    /**
     * Adds a track or route to the internal track list.
     */
    private void commitProfile() {
        if (buffer.size() > 0) {
            // assign way points to profile...
            curProfile.setWayPoints(buffer);
            // ... and add to profile list
            profiles.add(curProfile);
            buffer.clear();
        }
    }

    /**
     * Adds the given way point to the current buffer.
     *
     * @param wp the wp
     */
    private void processWayPoint(WayPoint wp) {
        if (wp == null) {
            throw new RuntimeException("WPT must not be null!");
        }

        buffer.add(wp);
    }
}
