/*
 * Copyright (c) 2015 Spotify AB.
 *
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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 com.spotify.heroic.grammar;

import com.fasterxml.jackson.annotation.JsonTypeName;
import com.spotify.heroic.common.Duration;
import lombok.Data;

import java.util.concurrent.TimeUnit;

@Data
@JsonTypeName("duration")
public class DurationExpression implements Expression {
    private static final BinaryOperation ADD = (long a, long b) -> a + b;
    private static final BinaryOperation SUB = (long a, long b) -> a - b;

    private final Context context;
    private final TimeUnit unit;
    private final long value;

    @Override
    public <R> R visit(final Visitor<R> visitor) {
        return visitor.visitDuration(this);
    }

    @Override
    public DurationExpression sub(Expression other) {
        return operate(SUB, other);
    }

    @Override
    public DurationExpression add(Expression other) {
        return operate(ADD, other);
    }

    @Override
    public DurationExpression divide(final Expression other) {
        final long den = other.cast(IntegerExpression.class).getValue();

        long value = this.value;
        TimeUnit unit = this.unit;

        outer:
        while (value % den != 0) {
            if (unit == TimeUnit.MILLISECONDS) {
                break;
            }

            final TimeUnit next = nextSmallerUnit(unit);
            value = next.convert(value, unit);
            unit = next;
        }

        return new DurationExpression(context, unit, value / den);
    }

    private TimeUnit nextSmallerUnit(final TimeUnit unit) {
        switch (unit) {
            case DAYS:
                return TimeUnit.HOURS;
            case HOURS:
                return TimeUnit.MINUTES;
            case MINUTES:
                return TimeUnit.SECONDS;
            case SECONDS:
                return TimeUnit.MILLISECONDS;
            default:
                throw new IllegalArgumentException("No supported smaller unit: " + unit);
        }
    }

    @Override
    public DurationExpression negate() {
        return new DurationExpression(context, unit, -value);
    }

    private DurationExpression operate(BinaryOperation op, Expression other) {
        final DurationExpression o = other.cast(DurationExpression.class);

        final Context c = context.join(other.getContext());

        if (unit == o.unit) {
            return new DurationExpression(c, unit, op.calculate(value, o.value));
        }

        // decide which unit to convert to depending on which has the greatest magnitude in
        // milliseconds.
        if (unit.toMillis(1) < o.unit.toMillis(1)) {
            return new DurationExpression(c, unit,
                op.calculate(value, unit.convert(o.value, o.unit)));
        }

        return new DurationExpression(c, o.unit,
            op.calculate(o.unit.convert(value, unit), o.value));
    }

    @SuppressWarnings("unchecked")
    @Override
    public <T extends Expression> T cast(Class<T> to) {
        if (to.isAssignableFrom(DurationExpression.class)) {
            return (T) this;
        }

        if (to.isAssignableFrom(IntegerExpression.class)) {
            return (T) new IntegerExpression(context, this.toMilliseconds());
        }

        throw context.castError(this, to);
    }

    public Duration toDuration() {
        return new Duration(value, unit);
    }

    public long toMilliseconds() {
        return TimeUnit.MILLISECONDS.convert(value, unit);
    }

    @Override
    public String toRepr() {
        return String.format("<%d%s>", value, Duration.unitSuffix(unit));
    }

    private interface BinaryOperation {
        long calculate(long a, long b);
    }
}