/*
 * Copyright 2018 GoDataDriven B.V.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package io.divolte.server.config;

import static com.fasterxml.jackson.core.JsonToken.*;

import java.io.IOException;
import java.time.Duration;
import java.util.concurrent.TimeUnit;

import javax.annotation.ParametersAreNonnullByDefault;

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.deser.std.StdScalarDeserializer;
import com.fasterxml.jackson.databind.exc.InvalidFormatException;
import com.typesafe.config.impl.ConfigImplUtil;

@ParametersAreNonnullByDefault
public class DurationDeserializer extends StdScalarDeserializer<Duration> {
    private static final long serialVersionUID = 1L;

    public DurationDeserializer() {
        super(Duration.class);
    }

    @Override
    public Duration deserialize(final JsonParser p,
                                final DeserializationContext ctx) throws IOException {
        if (VALUE_STRING != p.getCurrentToken()) {
            ctx.reportWrongTokenException(this, VALUE_STRING, "Expected string value for Duration mapping.");
        }
        long result;
        try {
            result = parse(p.getText());
        } catch(final DurationFormatException e) {
            throw InvalidFormatException.from(p, e.getMessage(), e);
        }
        return Duration.ofNanos(result);
    }

    public static Duration parseDuration(final String input) {
        return Duration.ofNanos(parse(input));
    }

    // Inspired by Typesafe Config parseDuration(...)
    private static long parse(final String input) {
        final String s = ConfigImplUtil.unicodeTrim(input);
        final String originalUnitString = getUnits(s);
        String unitString = originalUnitString;
        final String numberString = ConfigImplUtil.unicodeTrim(s.substring(0, s.length() - unitString.length()));

        // this would be caught later anyway, but the error message
        // is more helpful if we check it here.
        if (numberString.isEmpty()) {
            final String msg = String.format("No number in duration value '%s'", input);
            throw new DurationFormatException(msg);
        }

        // All units longer than 2 characters are accepted in singular or plural form.
        // This normalizes to plural so we only need to check that below.
        if (unitString.length() > 2 && !unitString.endsWith("s")) {
            unitString += 's';
        }

        // note that this is deliberately case-sensitive
        final TimeUnit units;
        switch (unitString) {
            case "":
            case "ms":
            case "millis":
            case "milliseconds":
                units = TimeUnit.MILLISECONDS;
                break;
            case "us":
            case "micros":
            case "microseconds":
                units = TimeUnit.MICROSECONDS;
                break;
            case "ns":
            case "nanos":
            case "nanoseconds":
                units = TimeUnit.NANOSECONDS;
                break;
            case "d":
            case "days":
                units = TimeUnit.DAYS;
                break;
            case "h":
            case "hours":
                units = TimeUnit.HOURS;
                break;
            case "s":
            case "seconds":
                units = TimeUnit.SECONDS;
                break;
            case "m":
            case "minutes":
                units = TimeUnit.MINUTES;
                break;
            default:
                final String msg = String.format("Could not parse time unit '%s' (try ns, us, ms, s, m, h, d)", originalUnitString);
                throw new DurationFormatException(msg);
        }

        try {
            // if the string is purely digits, parse as an integer to avoid
            // possible precision loss; otherwise as a double.
            return numberString.matches("[0-9]+")
                    ? units.toNanos(Long.parseLong(numberString))
                    : (long) (Double.parseDouble(numberString) * units.toNanos(1));
        } catch (final NumberFormatException e) {
            final String msg = String.format("Could not parse duration number '%s'", numberString);
            throw new DurationFormatException(msg);
        }
    }

    private static String getUnits(final String s) {
        int i = s.length() - 1;
        while (i >= 0) {
            final char c = s.charAt(i);
            if (!Character.isLetter(c)) {
                break;
            }
            i -= 1;
        }
        return s.substring(i + 1);
    }
}