/*
 * Copyright 2012 the original author or authors.
 *
 * 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.gradle.performance.measure;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public abstract class Units<Q> implements Comparable<Units<Q>> {
    private final Class<Q> type;
    private final String displaySingular;
    private final String displayPlural;

    protected Units(Class<Q> type, String displaySingular, String displayPlural) {
        this.type = type;
        this.displaySingular = displaySingular;
        this.displayPlural = displayPlural;
    }

    /**
     * Creates the base units for a given quantity.
     */
    public static <Q> Units<Q> base(Class<Q> type, String displaySingular) {
        return new BaseUnits<>(type, displaySingular, displaySingular);
    }

    /**
     * Creates the base units for a given quantity.
     */
    public static <Q> Units<Q> base(Class<Q> type, String displaySingular, String displayPlural) {
        return new BaseUnits<>(type, displaySingular, displayPlural);
    }

    protected Class<Q> getType() {
        return type;
    }

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

    /**
     * Creates units that are some multiple of this units.
     */
    public abstract Units<Q> times(long value, String displaySingular, String displayPlural);

    /**
     * Creates units that are some multiple of this units.
     */
    public Units<Q> times(long value, String displaySingular) {
        return times(value, displaySingular, displaySingular);
    }

    protected abstract Units<Q> getBaseUnits();

    protected abstract List<Units<Q>> getUnitsForQuantity();

    protected abstract BigDecimal getFactor();

    protected abstract BigDecimal scaleTo(BigDecimal value, Units<Q> units);

    protected String format(BigDecimal value) {
        return value.compareTo(BigDecimal.ONE) == 0 ? displaySingular : displayPlural;
    }

    private static class BaseUnits<Q> extends Units<Q> {
        private final List<Units<Q>> units = new ArrayList<>();

        protected BaseUnits(Class<Q> type, String displaySingular, String displayPlural) {
            super(type, displaySingular, displayPlural);
            units.add(this);
        }

        @Override
        public BigDecimal scaleTo(BigDecimal value, Units<Q> units) {
            if (units == this) {
                return value;
            }
            ScaledUnits<Q> scaledUnits = (ScaledUnits<Q>) units;
            return value.divide(scaledUnits.factor, 6, RoundingMode.HALF_UP).stripTrailingZeros();
        }

        @Override
        protected List<Units<Q>> getUnitsForQuantity() {
            return units;
        }

        @Override
        public Units<Q> times(long value, String displaySingular, String displayPlural) {
            return new ScaledUnits<>(this, displaySingular, displayPlural, BigDecimal.valueOf(value));
        }

        @Override
        protected Units<Q> getBaseUnits() {
            return this;
        }

        @Override
        protected BigDecimal getFactor() {
            return BigDecimal.ONE;
        }

        @Override
        public int compareTo(Units<Q> o) {
            if (o == this) {
                return 0;
            }
            if (o.getType() != getType()) {
                throw new IllegalArgumentException(String.format("Cannot compare units of %s with units of %s.", getType(), o.getType()));
            }
            return -1;
        }

        public void add(ScaledUnits<Q> units) {
            this.units.add(units);
            Collections.sort(this.units);
        }
    }

    private static class ScaledUnits<Q> extends Units<Q> {
        private final BaseUnits<Q> baseUnits;
        private final BigDecimal factor;

        public ScaledUnits(BaseUnits<Q> baseUnits, String displaySingular, String displayPlural, BigDecimal factor) {
            super(baseUnits.getType(), displaySingular, displayPlural);
            assert factor.compareTo(BigDecimal.ONE) > 0;
            this.baseUnits = baseUnits;
            this.factor = factor;
            baseUnits.add(this);
        }

        @Override
        public Units<Q> times(long value, String displaySingular, String displayPlural) {
            return new ScaledUnits<>(baseUnits, displaySingular, displayPlural, factor.multiply(BigDecimal.valueOf(value)));
        }

        @Override
        public BigDecimal scaleTo(BigDecimal value, Units<Q> units) {
            if (units == this) {
                return value;
            }
            if (units.equals(baseUnits)) {
                return value.multiply(factor);
            }
            ScaledUnits<Q> other = (ScaledUnits<Q>) units;
            return value.multiply(factor).divide(other.factor, 6, RoundingMode.HALF_UP).stripTrailingZeros();
        }

        @Override
        protected List<Units<Q>> getUnitsForQuantity() {
            return baseUnits.getUnitsForQuantity();
        }

        @Override
        protected Units<Q> getBaseUnits() {
            return baseUnits;
        }

        @Override
        protected BigDecimal getFactor() {
            return factor;
        }

        @Override
        public int compareTo(Units<Q> o) {
            if (o.getType() != getType()) {
                throw new IllegalArgumentException(String.format("Cannot compare units of %s with units of %s.", getType(), o.getType()));
            }
            if (o.equals(baseUnits)) {
                return 1;
            }
            ScaledUnits<Q> other = (ScaledUnits<Q>) o;
            if (!other.baseUnits.equals(baseUnits)) {
                throw new IllegalArgumentException("Cannot compare units with different base units.");
            }
            return factor.compareTo(other.factor);
        }
    }
}
