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

import java.time.Instant;
import java.util.ArrayList;
import java.util.List;

import org.openstreetmap.josm.data.Bounds;
import org.openstreetmap.josm.data.gpx.WayPoint;
import org.openstreetmap.josm.plugins.elevation.ElevationHelper;
import org.openstreetmap.josm.plugins.elevation.IElevationProfile;
import org.openstreetmap.josm.tools.Logging;


/**
 * Base class for an elevation profile. An elevation profile is constructed out
 * of a set of way points. The profile computes min/max/average height from the
 * full way point set and then reduces the number of way points to a given
 * amount, if necessary.
 *
 * The computation is done via implementing {@link IGpxWaypointVisitor},
 * subclasses may override the {@link ElevationProfile#visitWayPoint(WayPoint)}
 * method to compute own values or run specific actions. The computation is
 * triggered by calling {@link ElevationProfile#updateValues()}.
 *
 * Elevation profiles can break down into further child profiles. This is
 * intended to show different levels of details, if the number of way points
 * exceed the display space (which is usually the case).
 *
 * {@link IElevationProfile} {@link IGpxWaypointVisitor} {@link GpxIterator}
 *
 * @author Oliver Wieland &lt;oliver.wieland@online.de&gt;
 *
 */
public class ElevationProfile implements IElevationProfile,
IGpxWaypointVisitor {
    public static final int WAYPOINT_START = 0;
    public static final int WAYPOINT_END = 1;
    public static final int WAYPOINT_MIN = 2;
    public static final int WAYPOINT_MAX = 3;

    private String name;
    private int minHeight;
    private int maxHeight;
    private int avrgHeight;
    private double dist;
    private Instant start;
    private Instant end;
    private final WayPoint[] importantWayPoints = new WayPoint[4];
    private IElevationProfile parent;
    private int sumEle; // temp var for average height
    private List<WayPoint> wayPoints;
    private int numWayPoints; // cached value
    private int gain;
    private int lastEle;
    private Bounds bounds;

    private static boolean ignoreZeroHeight = true;

    /**
     * Creates a name elevation profile without any way points.
     */
    public ElevationProfile(String name) {
        this(name, null, null, 0);
    }

    /**
     * Creates a name elevation profile with a given set of way points.
     *
     * @param name
     *            The name of the profile.
     * @param parent
     *            The (optional) parent profile.
     * @param wayPoints
     *            The list containing the way points of the profile.
     * @param sliceSize
     *            The requested target size of the profile.
     */
    public ElevationProfile(String name, IElevationProfile parent,
            List<WayPoint> wayPoints, int sliceSize) {
        super();
        this.name = name;
        this.parent = parent;

        setWayPoints(wayPoints);
    }

    /**
     * Checks if zero elevation should be ignored or not.
     *
     * @return true, if is ignore zero height
     */
    public static boolean isIgnoreZeroHeight() {
        return ignoreZeroHeight;
    }

    /**
     * Sets the ignore zero height.
     *
     * @param ignoreZeroHeight the new ignore zero height
     */
    public static void setIgnoreZeroHeight(boolean ignoreZeroHeight) {
        ElevationProfile.ignoreZeroHeight = ignoreZeroHeight;
    }

    @Override
    public void updateElevationData() {
        updateValues();
    }

    /**
     * Revisits all way points and recomputes the characteristic values like
     * min/max elevation.
     */
    protected void updateValues() {
        if (wayPoints == null)
            return;

        int n = this.wayPoints.size();
        if (n == 0)
            return;

        start = Instant.EPOCH;
        end = Instant.now();
        this.minHeight = Integer.MAX_VALUE;
        this.maxHeight = Integer.MIN_VALUE;
        sumEle = 0;
        gain = 0;
        lastEle = 0;

        for (WayPoint wayPoint : this.wayPoints) {
            visitWayPoint(wayPoint);
        }

        if (this.minHeight == Integer.MAX_VALUE && this.maxHeight == Integer.MIN_VALUE) {
            // file does not contain elevation data    at all
            minHeight = 0;
            maxHeight = 0;
            setMinWayPoint(wayPoints.get(0));
            setMaxWayPoint(wayPoints.get(n-1));
        }

        //if (start.after(end) || start.equals(end)) {
        // GPX does not contain time stamps -> use sequential order
        setStart(wayPoints.get(0));
        setEnd(wayPoints.get(n-1));
        //}

        avrgHeight = sumEle / n;
    }

    /**
     * Gets the name of the profile.
     */
    @Override
    public String getName() {
        return name;
    }

    /**
     * Sets the name of the profile.
     * @param name The new name of the profile.
     */
    public void setName(String name) {
        this.name = name;
    }

    /**
     * Sets the way point with the lowest elevation.
     * @param wp The way point instance having the lowest elevation.
     */
    protected void setMinWayPoint(WayPoint wp) {
        importantWayPoints[WAYPOINT_MIN] = wp;
        this.minHeight = (int) ElevationHelper.getElevation(wp);
    }

    /**
     * Sets the way point with the highest elevation.
     * @param wp The way point instance having the highest elevation.
     */
    protected void setMaxWayPoint(WayPoint wp) {
        importantWayPoints[WAYPOINT_MAX] = wp;
        this.maxHeight = (int) ElevationHelper.getElevation(wp);
    }

    /**
     * Sets the average height.
     */
    protected void setAvrgHeight(int avrgHeight) {
        this.avrgHeight = avrgHeight;
    }

    /**
     * Sets the very first way point.
     */
    protected void setStart(WayPoint wp) {
        importantWayPoints[WAYPOINT_START] = wp;
        if(wp.getInstant() != null)
            this.start = wp.getInstant();
    }

    /**
     * Sets the very last way point.
     */
    protected void setEnd(WayPoint wp) {
        importantWayPoints[WAYPOINT_END] = wp;
        if(wp.getInstant() != null)
            this.end = wp.getInstant();
    }

    public void setParent(IElevationProfile parent) {
        this.parent = parent;
    }

    /**
     * Sets the way points of this profile.
     */
    public void setWayPoints(List<WayPoint> wayPoints) {
        if (this.wayPoints != wayPoints) {
            this.wayPoints = new ArrayList<>(wayPoints);
            numWayPoints = wayPoints != null ? wayPoints.size() : 0;
            updateValues();
        }
    }

    /**
     * Checks if the given index is valid or not.
     *
     * @param index
     *            The index to check.
     * @return true, if the given index is valid; otherwise false.
     */
    protected boolean checkIndex(int index) {
        return index >= 0 && index < getNumberOfWayPoints();
    }

    @Override
    public int elevationValueAt(int i) {
        if (checkIndex(i)) {
            return (int) ElevationHelper.getElevation(wayPoints.get(i));
        } else {
            throw new IndexOutOfBoundsException(String.format(
                    "Invalid index: %d, expected 0..%d", i,
                    getNumberOfWayPoints()));
        }
    }

    @Override
    public int getAverageHeight() {
        return avrgHeight;
    }

    @Override
    public List<IElevationProfile> getChildren() {
        return null;
    }

    @Override
    public Instant getEnd() {
        return end;
    }

    @Override
    public int getMaxHeight() {
        return maxHeight;
    }

    @Override
    public int getMinHeight() {
        return minHeight;
    }

    /**
     * Gets the difference between min and max elevation.
     */
    @Override
    public int getHeightDifference() {
        return maxHeight - minHeight;
    }

    /**
     * Gets the elevation gain.
     */
    @Override
    public int getGain() {
        return gain;
    }

    @Override
    public double getDistance() {
        return dist; // dist is in meters
    }

    /**
     * Sets the distance of the elevation profile.
     */
    protected void setDistance(double dist) {
        this.dist = dist;
    }

    /**
     * Returns the time between start and end of the track.
     */
    @Override
    public long getTimeDifference() {
        WayPoint wp1 = getStartWayPoint();
        WayPoint wp2 = getEndWayPoint();

        if (wp1 != null && wp2 != null) {
            Instant wp1Date = wp1.getInstant();
            Instant wp2Date = wp2.getInstant();
            if (wp1Date != null && wp2Date != null) {
                return wp2Date.toEpochMilli() - wp1Date.toEpochMilli();
            } else {
                Logging.warn("Waypoints without date: " + wp1 + " / " + wp2);
            }
        }

        return 0L;
    }

    @Override
    public IElevationProfile getParent() {
        return parent;
    }

    @Override
    public Instant getStart() {
        return start;
    }

    @Override
    public WayPoint getEndWayPoint() {
        return importantWayPoints[WAYPOINT_END];
    }

    @Override
    public WayPoint getMaxWayPoint() {
        return importantWayPoints[WAYPOINT_MAX];
    }

    @Override
    public WayPoint getMinWayPoint() {
        return importantWayPoints[WAYPOINT_MIN];
    }

    @Override
    public WayPoint getStartWayPoint() {
        return importantWayPoints[WAYPOINT_START];
    }

    @Override
    public List<WayPoint> getWayPoints() {
        return wayPoints;
    }

    @Override
    public int getNumberOfWayPoints() {
        return numWayPoints; // wayPoints != null ? wayPoints.size() : 0;
    }

    /**
     * Gets the coordinate bounds of this profile. See {@link Bounds} for details.
     *
     * @return the bounds of this elevation profile
     */
    @Override
    public Bounds getBounds() {
        return bounds;
    }

    /**
     * Gets a flag indicating whether the associated way points contained
     * elevation data or not. This is the case if min and max height or both
     * zero.
     */
    @Override
    public boolean hasElevationData() {
        return minHeight != maxHeight;
    }

    /**
     * Visits a way point in order to update statistical values about the given
     * way point list.
     */
    @Override
    public void visitWayPoint(WayPoint wp) {
        if (wp == null)
            return;

        if (wp.hasDate()) {
            if (wp.getInstant().isAfter(this.end)) {
                setEnd(wp);
            }

            if (wp.getInstant().isBefore(start)) {
                setStart(wp);
            }
        }

        // update boundaries
        if (bounds == null) {
            bounds = new Bounds(wp.getCoor());
        } else {
            bounds.extend(wp.getCoor());
        }

        int ele = (int) ElevationHelper.getElevation(wp);

        if (!isIgnoreZeroHeight() || ele > 0) {
            if (ele > maxHeight) {
                setMaxWayPoint(wp);
            }
            if (ele < minHeight) {
                setMinWayPoint(wp);
            }

            if (ele > lastEle) {
                gain += ele - lastEle;
            }

            sumEle += ele;
            lastEle = ele;
        }
    }

    @Override
    public String toString() {
        return name; /*"ElevationProfileBase [start=" + getStart() + ", end=" + getEnd()
                + ", minHeight=" + getMinHeight() + ", maxHeight="
                + getMaxHeight() + "]";*/
    }
}
