/*
 * Copyright 2016 higherfrequencytrading.com
 *
 * 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.
 */

/*
 * Based on java.util.concurrent.TimeUnit, which is
 * Written by Doug Lea with assistance from members of JCP JSR-166
 * Expert Group and released to the public domain, as explained at
 * http://creativecommons.org/publicdomain/zero/1.0/
 */

package net.openhft.lang;

import java.util.concurrent.TimeUnit;

/**
 * A {@code MemoryUnit} represents memory amounts at a given unit of
 * granularity and provides utility methods to convert across units.  A
 * {@code MemoryUnit} does not maintain memory information, but only
 * helps organize and use memory amounts representations that may be maintained
 * separately across various contexts.
 *
 * <p>Note than in this class kilo-, mega- and giga- prefixes means 2^10 = 1024 multiplexing,
 * that is more common in low-level programming, CPU and operation system contexts,
 * not 1000 as defined by International System of Units (SI).
 *
 * <p>A {@code MemoryUnit} is mainly used to inform memory amount-based methods
 * how a given memory amount parameter should be interpreted.
 *
 * <p>API of {@code MemoryUnit} is copied from {@link TimeUnit} enum.
 */
public enum MemoryUnit {

    /**
     * Memory unit representing one bit.
     */
    BITS {
        @Override
        public long toBits(long a) {
            return a;
        }

        @Override
        public long toBytes(long a) {
            return a / (C1 / C0);
        }

        @Override
        public long toLongs(long a) {
            return a / (C2 / C0);
        }

        @Override
        public long toCacheLines(long a) {
            return a / (C3 / C0);
        }

        @Override
        public long toKilobytes(long a) {
            return a / (C4 / C0);
        }

        @Override
        public long toPages(long a) {
            return a / (C5 / C0);
        }

        @Override
        public long toMegabytes(long a) {
            return a / (C6 / C0);
        }

        @Override
        public long toGigabytes(long a) {
            return a / (C7 / C0);
        }

        @Override
        public long convert(long a, MemoryUnit u) {
            return u.toBits(a);
        }

        @Override
        long alignToBytes(long a) {
            return y(a, C1 / C0);
        }

        @Override
        long alignToLongs(long a) {
            return y(a, C2 / C0);
        }

        @Override
        long alignToCacheLines(long a) {
            return y(a, C3 / C0);
        }

        @Override
        long alignToKilobytes(long a) {
            return y(a, C4 / C0);
        }

        @Override
        long alignToPages(long a) {
            return y(a, C5 / C0);
        }

        @Override
        long alignToMegabytes(long a) {
            return y(a, C6 / C0);
        }

        @Override
        long alignToGigabytes(long a) {
            return y(a, C7 / C0);
        }

        @Override
        public long align(long a, MemoryUnit u) {
            return ise(u, this);
        }
    },

    /**
     * Memory unit representing one byte, i. e. 8 bits.
     */
    BYTES {
        @Override
        public long toBits(long a) {
            return x(a, C1 / C0, MAX / (C1 / C0));
        }

        @Override
        public long toBytes(long a) {
            return a;
        }

        @Override
        public long toLongs(long a) {
            return a / (C2 / C1);
        }

        @Override
        public long toCacheLines(long a) {
            return a / (C3 / C1);
        }

        @Override
        public long toKilobytes(long a) {
            return a / (C4 / C1);
        }

        @Override
        public long toPages(long a) {
            return a / (C5 / C1);
        }

        @Override
        public long toMegabytes(long a) {
            return a / (C6 / C1);
        }

        @Override
        public long toGigabytes(long a) {
            return a / (C7 / C1);
        }

        @Override
        public long convert(long a, MemoryUnit u) {
            return u.toBytes(a);
        }

        @Override
        long alignToBytes(long a) {
            return ise(this, BYTES);
        }

        @Override
        long alignToLongs(long a) {
            return y(a, C2 / C1);
        }

        @Override
        long alignToCacheLines(long a) {
            return y(a, C3 / C1);
        }

        @Override
        long alignToKilobytes(long a) {
            return y(a, C4 / C1);
        }

        @Override
        long alignToPages(long a) {
            return y(a, C5 / C1);
        }

        @Override
        long alignToMegabytes(long a) {
            return y(a, C6 / C1);
        }

        @Override
        long alignToGigabytes(long a) {
            return y(a, C7 / C1);
        }

        @Override
        public long align(long a, MemoryUnit u) {
            return u.alignToBytes(a);
        }
    },

    /**
     * Memory unit representing 8 bytes, i. e. 64-bit word,
     * the width of Java's primitive {@code long} type.
     */
    LONGS {
        @Override
        public long toBits(long a) {
            return x(a, C2 / C0, MAX / (C2 / C0));
        }

        @Override
        public long toBytes(long a) {
            return x(a, C2 / C1, MAX / (C2 / C1));
        }

        @Override
        public long toLongs(long a) {
            return a;
        }

        @Override
        public long toCacheLines(long a) {
            return a / (C3 / C2);
        }

        @Override
        public long toKilobytes(long a) {
            return a / (C4 / C2);
        }

        @Override
        public long toPages(long a) {
            return a / (C5 / C2);
        }

        @Override
        public long toMegabytes(long a) {
            return a / (C6 / C2);
        }

        @Override
        public long toGigabytes(long a) {
            return a / (C7 / C2);
        }

        @Override
        public long convert(long a, MemoryUnit u) {
            return u.toLongs(a);
        }

        @Override
        long alignToBytes(long a) {
            return ise(this, BYTES);
        }

        @Override
        long alignToLongs(long a) {
            return ise(this, LONGS);
        }

        @Override
        long alignToCacheLines(long a) {
            return y(a, C3 / C2);
        }

        @Override
        long alignToKilobytes(long a) {
            return y(a, C4 / C2);
        }

        @Override
        long alignToPages(long a) {
            return y(a, C5 / C2);
        }

        @Override
        long alignToMegabytes(long a) {
            return y(a, C6 / C2);
        }

        @Override
        long alignToGigabytes(long a) {
            return y(a, C7 / C2);
        }

        @Override
        public long align(long a, MemoryUnit u) {
            return u.alignToLongs(a);
        }
    },

    /**
     * Memory unit representing 64 bytes, i. e. the most common CPU cache line size.
     */
    CACHE_LINES {
        @Override
        public long toBits(long a) {
            return x(a, C3 / C0, MAX / (C3 / C0));
        }

        @Override
        public long toBytes(long a) {
            return x(a, C3 / C1, MAX / (C3 / C1));
        }

        @Override
        public long toLongs(long a) {
            return x(a, C3 / C2, MAX / (C3 / C2));
        }

        @Override
        public long toCacheLines(long a) {
            return a;
        }

        @Override
        public long toKilobytes(long a) {
            return a / (C4 / C3);
        }

        @Override
        public long toPages(long a) {
            return a / (C5 / C3);
        }

        @Override
        public long toMegabytes(long a) {
            return a / (C6 / C3);
        }

        @Override
        public long toGigabytes(long a) {
            return a / (C7 / C3);
        }

        @Override
        public long convert(long a, MemoryUnit u) {
            return u.toCacheLines(a);
        }

        @Override
        long alignToBytes(long a) {
            return ise(this, BYTES);
        }

        @Override
        long alignToLongs(long a) {
            return ise(this, LONGS);
        }

        @Override
        long alignToCacheLines(long a) {
            return ise(this, CACHE_LINES);
        }

        @Override
        long alignToKilobytes(long a) {
            return y(a, C4 / C3);
        }

        @Override
        long alignToPages(long a) {
            return y(a, C5 / C3);
        }

        @Override
        long alignToMegabytes(long a) {
            return y(a, C6 / C3);
        }

        @Override
        long alignToGigabytes(long a) {
            return y(a, C7 / C3);
        }

        @Override
        public long align(long a, MemoryUnit u) {
            return u.alignToCacheLines(a);
        }
    },

    /**
     * Memory unit representing 1024 bytes.
     */
    KILOBYTES {
        @Override
        public long toBits(long a) {
            return x(a, C4 / C0, MAX / (C4 / C0));
        }

        @Override
        public long toBytes(long a) {
            return x(a, C4 / C1, MAX / (C4 / C1));
        }

        @Override
        public long toLongs(long a) {
            return x(a, C4 / C2, MAX / (C4 / C2));
        }

        @Override
        public long toCacheLines(long a) {
            return x(a, C4 / C3, MAX / (C4 / C3));
        }

        @Override
        public long toKilobytes(long a) {
            return a;
        }

        @Override
        public long toPages(long a) {
            return a / (C5 / C4);
        }

        @Override
        public long toMegabytes(long a) {
            return a / (C6 / C4);
        }

        @Override
        public long toGigabytes(long a) {
            return a / (C7 / C4);
        }

        @Override
        public long convert(long a, MemoryUnit u) {
            return u.toKilobytes(a);
        }

        @Override
        long alignToBytes(long a) {
            return ise(this, BYTES);
        }

        @Override
        long alignToLongs(long a) {
            return ise(this, LONGS);
        }

        @Override
        long alignToCacheLines(long a) {
            return ise(this, CACHE_LINES);
        }

        @Override
        long alignToKilobytes(long a) {
            return ise(this, KILOBYTES);
        }

        @Override
        long alignToPages(long a) {
            return y(a, C5 / C4);
        }

        @Override
        long alignToMegabytes(long a) {
            return y(a, C6 / C4);
        }

        @Override
        long alignToGigabytes(long a) {
            return y(a, C7 / C4);
        }

        @Override
        public long align(long a, MemoryUnit u) {
            return u.alignToKilobytes(a);
        }
    },

    /**
     * Memory unit representing 4096 bytes, i. e. the most common native memory page size.
     */
    PAGES {
        @Override
        public long toBits(long a) {
            return x(a, C5 / C0, MAX / (C5 / C0));
        }

        @Override
        public long toBytes(long a) {
            return x(a, C5 / C1, MAX / (C5 / C1));
        }

        @Override
        public long toLongs(long a) {
            return x(a, C5 / C2, MAX / (C5 / C2));
        }

        @Override
        public long toCacheLines(long a) {
            return x(a, C5 / C3, MAX / (C5 / C3));
        }

        @Override
        public long toKilobytes(long a) {
            return x(a, C5 / C4, MAX / (C5 / C4));
        }

        @Override
        public long toPages(long a) {
            return a;
        }

        @Override
        public long toMegabytes(long a) {
            return a / (C6 / C5);
        }

        @Override
        public long toGigabytes(long a) {
            return a / (C7 / C5);
        }

        @Override
        public long convert(long a, MemoryUnit u) {
            return u.toPages(a);
        }

        @Override
        long alignToBytes(long a) {
            return ise(this, BYTES);
        }

        @Override
        long alignToLongs(long a) {
            return ise(this, LONGS);
        }

        @Override
        long alignToCacheLines(long a) {
            return ise(this, CACHE_LINES);
        }

        @Override
        long alignToKilobytes(long a) {
            return ise(this, KILOBYTES);
        }

        @Override
        long alignToPages(long a) {
            return ise(this, PAGES);
        }

        @Override
        long alignToMegabytes(long a) {
            return y(a, C6 / C5);
        }

        @Override
        long alignToGigabytes(long a) {
            return y(a, C7 / C5);
        }

        @Override
        public long align(long a, MemoryUnit u) {
            return u.alignToPages(a);
        }
    },

    /**
     * Memory unit representing 1024 kilobytes.
     */
    MEGABYTES {
        @Override
        public long toBits(long a) {
            return x(a, C6 / C0, MAX / (C6 / C0));
        }

        @Override
        public long toBytes(long a) {
            return x(a, C6 / C1, MAX / (C6 / C1));
        }

        @Override
        public long toLongs(long a) {
            return x(a, C6 / C2, MAX / (C6 / C2));
        }

        @Override
        public long toCacheLines(long a) {
            return x(a, C6 / C3, MAX / (C6 / C3));
        }

        @Override
        public long toKilobytes(long a) {
            return x(a, C6 / C4, MAX / (C6 / C4));
        }

        @Override
        public long toPages(long a) {
            return x(a, C6 / C5, MAX / (C6 / C5));
        }

        @Override
        public long toMegabytes(long a) {
            return a;
        }

        @Override
        public long toGigabytes(long a) {
            return a / (C7 / C6);
        }

        @Override
        public long convert(long a, MemoryUnit u) {
            return u.toMegabytes(a);
        }

        @Override
        long alignToBytes(long a) {
            return ise(this, BYTES);
        }

        @Override
        long alignToLongs(long a) {
            return ise(this, LONGS);
        }

        @Override
        long alignToCacheLines(long a) {
            return ise(this, CACHE_LINES);
        }

        @Override
        long alignToKilobytes(long a) {
            return ise(this, KILOBYTES);
        }

        @Override
        long alignToPages(long a) {
            return ise(this, PAGES);
        }

        @Override
        long alignToMegabytes(long a) {
            return ise(this, MEGABYTES);
        }

        @Override
        long alignToGigabytes(long a) {
            return y(a, C7 / C6);
        }

        @Override
        public long align(long a, MemoryUnit u) {
            return u.alignToMegabytes(a);
        }
    },

    /**
     * Memory unit representing 1024 megabytes.
     */
    GIGABYTES {
        @Override
        public long toBits(long a) {
            return x(a, C7 / C0, MAX / (C7 / C0));
        }

        @Override
        public long toBytes(long a) {
            return x(a, C7 / C1, MAX / (C7 / C1));
        }

        @Override
        public long toLongs(long a) {
            return x(a, C7 / C2, MAX / (C7 / C2));
        }

        @Override
        public long toCacheLines(long a) {
            return x(a, C7 / C3, MAX / (C7 / C3));
        }

        @Override
        public long toKilobytes(long a) {
            return x(a, C7 / C4, MAX / (C7 / C4));
        }

        @Override
        public long toPages(long a) {
            return x(a, C7 / C5, MAX / (C7 / C5));
        }

        @Override
        public long toMegabytes(long a) {
            return x(a, C7 / C6, MAX / (C7 / C6));
        }

        @Override
        public long toGigabytes(long a) {
            return a;
        }

        @Override
        public long convert(long a, MemoryUnit u) {
            return u.toGigabytes(a);
        }

        @Override
        long alignToBytes(long a) {
            return ise(this, BYTES);
        }

        @Override
        long alignToLongs(long a) {
            return ise(this, LONGS);
        }

        @Override
        long alignToCacheLines(long a) {
            return ise(this, CACHE_LINES);
        }

        @Override
        long alignToKilobytes(long a) {
            return ise(this, KILOBYTES);
        }

        @Override
        long alignToPages(long a) {
            return ise(this, PAGES);
        }

        @Override
        long alignToMegabytes(long a) {
            return ise(this, MEGABYTES);
        }

        @Override
        long alignToGigabytes(long a) {
            return ise(this, GIGABYTES);
        }

        @Override
        public long align(long a, MemoryUnit u) {
            return u.alignToGigabytes(a);
        }
    };

    // Handy constants for conversion methods
    static final long C0 = 1L;
    static final long C1 = C0 * 8L;
    static final long C2 = C1 * 8L;
    static final long C3 = C2 * 8L;
    static final long C4 = C3 * 16L;
    static final long C5 = C4 * 4L;
    static final long C6 = C5 * 256L;
    static final long C7 = C6 * 1024L;

    static final long MAX = Long.MAX_VALUE;

    /**
     * Scale d by m, checking for overflow.
     * This has a short name to make above code more readable.
     */
    static long x(long a, long m, long over) {
        if (a > over) return Long.MAX_VALUE;
        if (a < -over) return Long.MIN_VALUE;
        return a * m;
    }

    static long y(long amount, long align) {
        if (amount == 0L)
            return 0L;
        long mask = ~(align - 1L);
        if (amount > 0L) {
            long filled = amount + align - 1L;
            if (filled > 0L) {
                return filled & mask;

            } else {
                long maxAlignedLong = Long.MAX_VALUE & mask;
                if (amount <= maxAlignedLong)
                    return maxAlignedLong;
            }
        } else {
            // amount is negative
            long filled = amount - align + 1L;
            if (filled < 0L) {
                return filled & mask;

            } else {
                long minAlignedLong = Long.MIN_VALUE & mask;
                if (amount >= minAlignedLong)
                    return minAlignedLong;
            }
        }
        throw new IllegalArgumentException("Couldn't align " + amount + " by " + align);
    }

    static long ise(MemoryUnit unitToAlign, MemoryUnit alignmentUnit) {
        throw new IllegalStateException("Couldn't align " + unitToAlign + " by " + alignmentUnit);
    }

    // To maintain full signature compatibility with 1.5, and to improve the
    // clarity of the generated javadoc (see 6287639: Abstract methods in
    // enum classes should not be listed as abstract), method convert
    // etc. are not declared abstract but otherwise act as abstract methods.

    /**
     * Converts the given memory amount in the given unit to this unit.
     * Conversions from finer to coarser granularities truncate, so
     * lose precision. For example, converting {@code 7} bits
     * to bytes results in {@code 0}. Conversions from coarser to
     * finer granularities with arguments that would numerically
     * overflow saturate to {@code Long.MIN_VALUE} if negative or
     * {@code Long.MAX_VALUE} if positive.
     *
     * <p>For example, to convert 4096 bytes to cache lines, use:
     * {@code MemoryUnit.CACHE_LINES.convert(4096L, MemoryUnit.BYTES)}
     *
     * @param sourceAmount the memory amount in the given {@code sourceUnit}
     * @param sourceUnit   the unit of the {@code sourceAmount} argument
     * @return the converted amount in this unit,
     * or {@code Long.MIN_VALUE} if conversion would negatively
     * overflow, or {@code Long.MAX_VALUE} if it would positively overflow.
     */
    public long convert(long sourceAmount, MemoryUnit sourceUnit) {
        throw new AbstractMethodError();
    }

    /**
     * Aligns the given memory amount in the given unit to this unit. For example, aligning
     * {@code 1000} bytes to kilobytes results in {@code 1024}. Negative values are aligned towards
     * negative infinity: e. g. aligning {@code -5} longs to cache lines results in {@code -8}.
     *
     * @param amountToAlign the memory amount in the given {@code unit}
     * @param unit          the unit of the {@code amountToAlign} argument
     * @return the aligned amount, still in the given unit
     * @throws IllegalArgumentException if the given {@code unit} is finer than this unit,
     *                                  or if the aligned value overflows {@code long} bounds
     */
    public long align(long amountToAlign, MemoryUnit unit) {
        throw new AbstractMethodError();
    }

    /**
     * Equivalent to {@code convert(align(sourceAmount, sourceUnit), sourceUnit)}.
     *
     * @param sourceAmount the memory amount in the given {@code sourceUnit}
     * @param sourceUnit   the unit of the {@code sourceAmount} argument
     * @return the saturated converted amount in this unit
     * @throws IllegalArgumentException if the given {@code sourceUnit} is finer than this unit,
     *                                  or if the aligned value overflows {@code long} bounds
     */
    public long alignAndConvert(long sourceAmount, MemoryUnit sourceUnit) {
        return convert(align(sourceAmount, sourceUnit), sourceUnit);
    }

    /**
     * Equivalent to
     * {@link #convert(long, MemoryUnit) BITS.convert(amount, this)}.
     *
     * @param amount the amount
     * @return the converted amount,
     * or {@code Long.MIN_VALUE} if conversion would negatively
     * overflow, or {@code Long.MAX_VALUE} if it would positively overflow.
     */
    public long toBits(long amount) {
        throw new AbstractMethodError();
    }

    /**
     * Equivalent to
     * {@link #convert(long, MemoryUnit) BYTES.convert(amount, this)}.
     *
     * @param amount the amount
     * @return the converted amount,
     * or {@code Long.MIN_VALUE} if conversion would negatively
     * overflow, or {@code Long.MAX_VALUE} if it would positively overflow.
     */
    public long toBytes(long amount) {
        throw new AbstractMethodError();
    }

    /**
     * Equivalent to
     * {@link #convert(long, MemoryUnit) LONGS.convert(amount, this)}.
     *
     * @param amount the amount
     * @return the converted amount,
     * or {@code Long.MIN_VALUE} if conversion would negatively
     * overflow, or {@code Long.MAX_VALUE} if it would positively overflow.
     */
    public long toLongs(long amount) {
        throw new AbstractMethodError();
    }

    /**
     * Equivalent to
     * {@link #convert(long, MemoryUnit) CACHE_LINES.convert(amount, this)}.
     *
     * @param amount the amount
     * @return the converted amount,
     * or {@code Long.MIN_VALUE} if conversion would negatively
     * overflow, or {@code Long.MAX_VALUE} if it would positively overflow.
     */
    public long toCacheLines(long amount) {
        throw new AbstractMethodError();
    }

    /**
     * Equivalent to
     * {@link #convert(long, MemoryUnit) KILOBYTES.convert(amount, this)}.
     *
     * @param amount the amount
     * @return the converted amount,
     * or {@code Long.MIN_VALUE} if conversion would negatively
     * overflow, or {@code Long.MAX_VALUE} if it would positively overflow.
     */
    public long toKilobytes(long amount) {
        throw new AbstractMethodError();
    }

    /**
     * Equivalent to
     * {@link #convert(long, MemoryUnit) PAGES.convert(amount, this)}.
     *
     * @param amount the amount
     * @return the converted amount,
     * or {@code Long.MIN_VALUE} if conversion would negatively
     * overflow, or {@code Long.MAX_VALUE} if it would positively overflow.
     */
    public long toPages(long amount) {
        throw new AbstractMethodError();
    }

    /**
     * Equivalent to
     * {@link #convert(long, MemoryUnit) MEGABYTES.convert(amount, this)}.
     *
     * @param amount the amount
     * @return the converted amount,
     * or {@code Long.MIN_VALUE} if conversion would negatively
     * overflow, or {@code Long.MAX_VALUE} if it would positively overflow.
     */
    public long toMegabytes(long amount) {
        throw new AbstractMethodError();
    }

    /**
     * Equivalent to
     * {@link #convert(long, MemoryUnit) GIGABYTES.convert(amount, this)}.
     *
     * @param amount the amount
     * @return the converted amount
     */
    public long toGigabytes(long amount) {
        throw new AbstractMethodError();
    }

    abstract long alignToBytes(long amount);

    abstract long alignToLongs(long amount);

    abstract long alignToCacheLines(long amount);

    abstract long alignToKilobytes(long amount);

    abstract long alignToPages(long amount);

    abstract long alignToMegabytes(long amount);

    abstract long alignToGigabytes(long amount);
}
