/**********************************************************************
 * Copyright (c) 2015, 2016 Ericsson
 *
 * All rights reserved. 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
 **********************************************************************/

package org.eclipse.tracecompass.common.core.format;

import java.math.BigDecimal;
import java.text.DecimalFormat;
import java.text.FieldPosition;
import java.text.Format;
import java.text.NumberFormat;
import java.text.ParsePosition;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.tracecompass.common.core.NonNullUtils;

/**
 * Time format, it will take a time in nano seconds and convert it to a string
 * with 3 decimals max.
 *
 * examples:
 * <ul>
 * <li>100 -> "100 ns"</li>
 * <li>1001 -> "1.001 us" (mu)</li>
 * <li>314159264 -> "314.159 ms"</li>
 * <li>10000002000000 -> "1000.002 s"</li>
 * </ul>
 * @since 4.1
 */
public final class SubSecondTimeWithUnitFormat extends Format {

    private static final @NonNull Format INSTANCE = new SubSecondTimeWithUnitFormat();

    private static final long serialVersionUID = -5147827135781459548L;

    private static final Pattern UNIT_PATTERN = Pattern.compile("^[\\s]*([nmu\u00B5]?s)"); //$NON-NLS-1$

    private static final String SECONDS = "s"; //$NON-NLS-1$
    private static final String NANOSECONDS = "ns"; //$NON-NLS-1$
    private static final String MILLISECONDS = "ms"; //$NON-NLS-1$
    private static final String MICROSECONDS = "\u00B5" + SECONDS; //$NON-NLS-1$
    private static final String MICROSECONDS_FORMAT = "us"; //$NON-NLS-1$

    private static final int NANOS_PER_SEC = 1000000000;
    private static final int NANOS_PER_MILLI = 1000000;
    private static final int NANOS_PER_MICRO = 1000;

    private final DecimalFormat fDecimalFormat = new DecimalFormat("#.###"); //$NON-NLS-1$

    /**
     * Protected constructor
     */
    protected SubSecondTimeWithUnitFormat() {
        super();
    }

    /**
     * Returns the instance of this formatter
     *
     * @return The instance of this formatter
     */
    public static @NonNull Format getInstance() {
        return INSTANCE;
    }

    @Override
    public Object parseObject(String source, ParsePosition pos) {
        Number number = NumberFormat.getInstance().parse(source, pos);
        if (number == null) {
            return null;
        }
        // Try to match the unit, if no match, assume nanoseconds and don't
        // update the position
        String remaining = source.substring(pos.getIndex()).toLowerCase();
        Matcher matcher = UNIT_PATTERN.matcher(remaining);
        long multiplier = 1;
        if (matcher.find()) {
            String unitString = matcher.group();
            String prefix = matcher.group(1);
            if (prefix.equals(SECONDS)) {
                multiplier = NANOS_PER_SEC;
            } else if (prefix.equals(MILLISECONDS)) {
                multiplier = NANOS_PER_MILLI;
            } else if (prefix.equals(MICROSECONDS)) {
                multiplier = NANOS_PER_MICRO;
            } else if (prefix.equals(MICROSECONDS_FORMAT)) {
                multiplier = NANOS_PER_MICRO;
            }

            // Update the position given the unit string
            pos.setIndex(pos.getIndex() + unitString.length());
        }
        // Get the number given the multiplier
        if (multiplier != 1 && Double.isFinite(number.doubleValue())) {
            BigDecimal bd = new BigDecimal(number.toString());
            bd = bd.multiply(BigDecimal.valueOf(multiplier));
            // This parses in ns, so just convert to the long value if within long range
            if (bd.abs().compareTo(new BigDecimal(Long.MAX_VALUE)) < 0) {
                return bd.longValue();
            }
            return bd.doubleValue();
        }
        return number;
    }

    @Override
    public StringBuffer format(@Nullable Object obj, @Nullable StringBuffer toAppendTo, @Nullable FieldPosition pos) {
        final @Nullable StringBuffer appender = toAppendTo;
        if ((obj != null) && (obj instanceof Number)) {
            double formattedTime =((Number) obj).doubleValue();
            if (Double.isNaN(formattedTime)) {
                return appender == null ? new StringBuffer() : NonNullUtils.checkNotNull(appender.append("---")); //$NON-NLS-1$
            }
            String unit = NANOSECONDS;
            double absFormattedTime = Math.abs(formattedTime);
            if (absFormattedTime >= NANOS_PER_SEC) {
                unit = SECONDS;
                formattedTime /= NANOS_PER_SEC;
            } else if (absFormattedTime >= NANOS_PER_MILLI) {
                unit = MILLISECONDS;
                formattedTime /= NANOS_PER_MILLI;
            } else if (absFormattedTime >= NANOS_PER_MICRO) {
                unit = MICROSECONDS;
                formattedTime /= NANOS_PER_MICRO;
            }
            if (formattedTime == 0) {
                return appender == null ? new StringBuffer() : NonNullUtils.checkNotNull(appender.append(0));
            }
            String timeString = unit.equals(NANOSECONDS) ? Long.toString((long) formattedTime) : fDecimalFormat.format(formattedTime);
            return appender == null ? new StringBuffer() : NonNullUtils.checkNotNull(appender.append(timeString).append(' ').append(unit));
        }
        return new StringBuffer();
    }
}