/*******************************************************************************
 * Copyright (c) 2006, 2018 Cloudsmith Inc.
 *
 *  This program and the accompanying materials
 *  are made available under the terms of the Eclipse Public License 2.0
 *  which accompanies this distribution, and is available at
 *  https://www.eclipse.org/legal/epl-2.0/
 *
 *  SPDX-License-Identifier: EPL-2.0
 *
 *  Contributors:
 * 	Cloudsmith Inc - initial API and implementation
 * 	IBM Corporation - ongoing development
 * 	Genuitec - Bug 291926
 ******************************************************************************/
package org.eclipse.equinox.internal.p2.repository;

import java.net.URI;
import java.text.NumberFormat;
import java.util.SortedMap;
import java.util.TreeMap;
import org.eclipse.equinox.internal.provisional.p2.core.eventbus.IProvisioningEventBus;
import org.eclipse.equinox.p2.core.IProvisioningAgent;
import org.eclipse.osgi.util.NLS;

/**
 * Converts progress of file download to average download speed and keeps track of
 * when it is suitable to update a progress monitor. A suitably scaled and formatted string for use
 * in progress monitoring is provided. It will publishes {@link DownloadProgressEvent} if {@link IProvisioningAgent}
 * is given and {@link IProvisioningEventBus} is available.
 */
public class ProgressStatistics {
	private static final int DEFAULT_REPORT_INTERVAL = 1000;

	private static final int SPEED_INTERVAL = 5000;

	private static final int SPEED_RESOLUTION = 1000;

	private static String convert(long amount) {
		NumberFormat fmt = NumberFormat.getInstance();
		if (amount < 1024)
			return fmt.format(amount) + "B"; //$NON-NLS-1$

		fmt.setMaximumFractionDigits(2);
		if (amount < 1024 * 1024)
			return fmt.format(((double) amount) / 1024) + "kB"; //$NON-NLS-1$

		return fmt.format(((double) amount) / (1024 * 1024)) + "MB"; //$NON-NLS-1$
	}

	private void publishEvent(DownloadProgressEvent event) {
		if (m_agent != null) {
			IProvisioningEventBus eventBus = m_agent.getService(IProvisioningEventBus.class);
			if (eventBus != null) {
				eventBus.publishEvent(event);
			}
		}
	}

	final String m_fileName;

	private final long m_total;

	private final long m_startTime;

	long m_current;

	private long m_lastReportTime;

	private int m_reportInterval;

	private SortedMap<Long, Long> m_recentSpeedMap;

	private long m_recentSpeedMapKey;

	URI m_uri;

	IProvisioningAgent m_agent;

	public ProgressStatistics(IProvisioningAgent agent, URI uri, String fileName, long total) {
		m_startTime = System.currentTimeMillis();
		m_fileName = fileName;

		m_total = total;

		m_current = 0;
		m_lastReportTime = 0;
		m_reportInterval = DEFAULT_REPORT_INTERVAL;
		m_recentSpeedMap = new TreeMap<>();
		m_recentSpeedMapKey = 0L;
		m_uri = uri;
		m_agent = agent;
	}

	public long getAverageSpeed() {
		long dur = getDuration();
		if (dur > 0)
			return (int) (m_current / (dur / 1000.0));
		return 0L;
	}

	public long getDuration() {
		return System.currentTimeMillis() - m_startTime;
	}

	public double getPercentage() {
		if (m_total > 0)
			return ((double) m_current) / ((double) m_total);

		return 0.0;
	}

	synchronized public long getRecentSpeed() {
		removeObsoleteRecentSpeedData(getDuration() / SPEED_RESOLUTION);
		long dur = 0L;
		long amount = 0L;
		SortedMap<Long, Long> relevantData = m_recentSpeedMap.headMap(Long.valueOf(m_recentSpeedMapKey));

		if (!relevantData.isEmpty()) {
			for (Long rl : relevantData.values()) {
				dur += SPEED_RESOLUTION;
				amount += rl.longValue();
			}
		}

		if (dur >= 1000)
			return amount / (dur / 1000);

		return 0L;
	}

	public int getReportInterval() {
		return m_reportInterval;
	}

	public long getTotal() {
		return m_total;
	}

	public void increase(long inc) {
		registerRecentSpeed(getDuration() / SPEED_RESOLUTION, inc);
		m_current += inc;
	}

	public synchronized String report() {
		publishEvent(new DownloadProgressEvent(this));
		return createReportString();
	}

	private String createReportString() {
		String uriString = m_uri.toString();
		if (m_fileName != null && uriString.endsWith(m_fileName))
			uriString = uriString.substring(0, uriString.lastIndexOf(m_fileName));
		if (m_current == 0L || getRecentSpeed() == 0L) {
			// no meaningful speed data available
			if (m_total == -1) {
				return NLS.bind(Messages.fetching_0_from_1, new String[] {m_fileName, uriString});
			}
			return NLS.bind(Messages.fetching_0_from_1_2, new String[] {m_fileName, uriString, convert(m_total)});
		}
		return m_total != -1 ? NLS.bind(Messages.fetching_0_from_1_2_of_3_at_4, new String[] {m_fileName, uriString, convert(m_current), convert(m_total), convert(getRecentSpeed())}) : NLS.bind(Messages.fetching_0_from_1_2_at_3, new String[] {m_fileName, uriString, convert(m_current), convert(getRecentSpeed())});
	}

	public void setReportInterval(int reportInterval) {
		m_reportInterval = reportInterval;
	}

	public boolean shouldReport() {
		long currentTime = System.currentTimeMillis();
		if (m_lastReportTime == 0 || currentTime - m_lastReportTime >= m_reportInterval) {
			m_lastReportTime = currentTime;
			return true;
		}
		return false;
	}

	@Override
	public String toString() {
		return createReportString();
	}

	synchronized private void registerRecentSpeed(long key, long inc) {
		Long keyL = Long.valueOf(key);
		Long currentValueL = m_recentSpeedMap.get(keyL);
		long currentValue = 0L;
		if (currentValueL != null)
			currentValue = currentValueL.longValue();

		m_recentSpeedMap.put(keyL, Long.valueOf(inc + currentValue));

		if (m_recentSpeedMapKey != key) {
			m_recentSpeedMapKey = key;
			removeObsoleteRecentSpeedData(key);
		}
	}

	synchronized private void removeObsoleteRecentSpeedData(long lastKey) {
		long threshold = lastKey - SPEED_INTERVAL / SPEED_RESOLUTION;
		m_recentSpeedMap.headMap(Long.valueOf(threshold)).clear();
	}
}
