/*
 * Copyright 2014, The OpenNMS Group
 * 
 * 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 org.opennms.newts.api;


import static com.google.common.base.Preconditions.checkNotNull;

import java.io.Serializable;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;


public class Duration implements Comparable<Duration>, Serializable {
    private static final long serialVersionUID = -6758879321662337772L;

    private static final Pattern s_pattern = Pattern.compile("([\\d]+)(ms|[wdhms])");

    private final long m_duration;
    private final TimeUnit m_unit;

    public Duration(long duration, TimeUnit unit) {
        m_duration = duration;
        m_unit = unit;
    }

    public long convert(TimeUnit unit) {
        return unit.convert(getDuration(), getUnit());
    }

    public long asMillis() {
        return convert(TimeUnit.MILLISECONDS);
    }

    public long asSeconds() {
        return convert(TimeUnit.SECONDS);
    }

    public static Duration days(long days) {
        return new Duration(days, TimeUnit.DAYS);
    }

    public static Duration hours(long hours) {
        return new Duration(hours, TimeUnit.HOURS);
    }

    public static Duration minutes(long minutes) {
        return new Duration(minutes, TimeUnit.MINUTES);
    }

    public static Duration seconds(long seconds) {
        return new Duration(seconds, TimeUnit.SECONDS);
    }

    public static Duration millis(long millis) {
        return new Duration(millis, TimeUnit.MILLISECONDS);
    }

    public static Duration parse(String durationSpec) {
        checkNotNull(durationSpec, "durationSpec argument");

        // If we replace every matched term with an empty string, the result should be
        // an empty string, otherwise there are invalid characters in the given duration
        if (!"".equals(s_pattern.matcher(durationSpec).replaceAll(""))) {
            throw new IllegalArgumentException(String.format("%s is not a valid duration", durationSpec));
        }

        Matcher matcher = s_pattern.matcher(durationSpec);

        Duration tally = null;

        while (matcher.find()) {
            Integer value = Integer.parseInt(matcher.group(1));
            String unit = matcher.group(2);

            Duration r;

            switch (unit) {
                case "w":
                    r = Duration.days((long)value * 7);
                    break;
                case "d":
                    r = Duration.days(value);
                    break;
                case "h":
                    r = Duration.hours(value);
                    break;
                case "m":
                    r = Duration.minutes(value);
                    break;
                case "s":
                    r = Duration.seconds(value);
                    break;
                case "ms":
                    r = Duration.millis(value);
                    break;
                default:
                    throw new RuntimeException(String.format("%s is an unknown unit; this is a bug!", unit));
            }

            tally = tally != null ? tally.plus(r) : r;
        }

        if (tally == null) {
            // We didn't find any terms
            throw new IllegalArgumentException(String.format("%s is not a valid duration", durationSpec));
        }

        return tally;
    }

    public long getDuration() {
        return m_duration;
    }

    public TimeUnit getUnit() {
        return m_unit;
    }

    public Duration plus(Duration o) {
        TimeUnit u = Timestamp.finest(getUnit(), o.getUnit());
        return new Duration(convert(u) + o.convert(u), u);
    }

    public Duration times(long value) {
        return new Duration(getDuration() * value, getUnit());
    }

    public long divideBy(Duration o) {
        TimeUnit u = Timestamp.finest(getUnit(), o.getUnit());
        return convert(u) / o.convert(u);
    }

    public boolean isMultiple(Duration o) {
        TimeUnit u = Timestamp.finest(getUnit(), o.getUnit());
        return this.gte(o) && ((convert(u) % o.convert(u)) == 0);
    }

    @Override
    public String toString() {
        return String.format("%s[%d, %s]", getClass().getSimpleName(), m_duration, getUnit());
    }

    @Override
    public int compareTo(Duration o) {
        TimeUnit unit = Timestamp.finest(getUnit(), o.getUnit());
        return convert(unit) < o.convert(unit) ? -1 : (convert(unit) > o.convert(unit) ? 1 : 0);
    }

    @Override
    public boolean equals(Object o) {
        if (!(o instanceof Duration)) return false;
        return compareTo((Duration) o) == 0;
    }

    @Override
    public int hashCode() {
        return Objects.hashCode(Long.valueOf(convert(TimeUnit.NANOSECONDS)));
    }

    public boolean lt(Duration o) {
        return compareTo(o) == -1;
    }

    public boolean gt(Duration o) {
        return compareTo(o) == 1;
    }

    public boolean lte(Duration o) {
        return compareTo(o) <= 0;
    }

    public boolean gte(Duration o) {
        return compareTo(o) >= 0;
    }

}
