/*
 * Copyright (c) 2022 MiLaboratories Inc.
 *
 * 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 com.milaboratory.util;

import com.google.common.hash.Hashing;

import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;

/** Immutable wrapper around byte array, implementing equals, hashcode and comparable. */
public final class ByteString implements Comparable<ByteString> {
    private final byte[] data;

    private ByteString(byte[] data) {
        this.data = data;
    }

    public byte get(int idx) {
        return data[idx];
    }

    @Override
    public int compareTo(ByteString o) {
        return compareLexicographically(data, o.data);
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof ByteString)) return false;
        ByteString that = (ByteString) o;
        return Arrays.equals(data, that.data);
    }

    @Override
    public int hashCode() {
        // Quick comparison shows that murmur hash performs computationally the same as Java-style hash
        return Hashing.murmur3_32_fixed().hashBytes(data).hashCode();
    }

    public String asString() {
        return asString(Charset.defaultCharset());
    }

    public String asString(Charset charset) {
        return new String(data, charset);
    }

    @Override
    public String toString() {
        boolean ascii = true;
        for (byte d : data)
            if (d < 33 || d > 126) {
                ascii = false;
                break;
            }
        if (ascii)
            return asString(StandardCharsets.US_ASCII);
        else {
            StringBuilder sb = new StringBuilder(data.length * 2);
            for (byte d : data) {
                String s = Integer.toString(0xFF & d, 16);
                if (s.length() == 1)
                    sb.append('0');
                sb.append(s);
            }
            return sb.toString();
        }
    }

    public static ByteString createNoCopy(byte[] data) {
        return new ByteString(data);
    }

    public static ByteString fromString(String data) {
        return new ByteString(data.getBytes());
    }

    public static ByteString create(byte... data) {
        return new ByteString(data.clone());
    }

    public static byte[] extractDataNoCopy(ByteString str) {
        return str.data;
    }

    public static byte[] extractData(ByteString str) {
        return str.data.clone();
    }

    public static int compareLexicographically(byte[] a, byte[] b) {
        // Implements lexical ordering
        int c;
        int l = Math.min(a.length, b.length);
        for (int i = 0; i < l; i++)
            if ((c = Byte.compare(a[i], b[i])) != 0)
                return c;

        // Shorter sequences are considered to be less than longer,
        // in other words any sequence goes after all it's prefixes
        return Integer.compare(a.length, b.length);
    }
}
