/*
 * 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 accord.maelstrom;

import java.io.IOException;
import java.util.Objects;
import java.util.function.BiFunction;
import java.util.zip.CRC32;

import com.google.gson.TypeAdapter;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;

public class Datum implements Comparable<Datum>
{
    public static final boolean COMPARE_BY_HASH = true;

    public static class Hash implements Comparable<Hash>
    {
        final int hash;
        public Hash(int hash)
        {
            this.hash = hash;
        }

        @Override
        public int compareTo(Hash that)
        {
            return Integer.compare(this.hash, that.hash);
        }

        @Override
        public boolean equals(Object o)
        {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            Hash hash1 = (Hash) o;
            return hash == hash1.hash;
        }

        @Override
        public int hashCode()
        {
            return Objects.hash(hash);
        }

        public String toString()
        {
            return "#" + hash;
        }
    }

    public enum Kind
    {
        STRING, LONG, DOUBLE, HASH;

        public MaelstromKey.Routing[] split(int count)
        {
            if (count <= 1)
                throw new IllegalArgumentException();

            MaelstromKey.Routing[] result = new MaelstromKey.Routing[count];
            if (this != Kind.HASH)
                throw new UnsupportedOperationException();

            int delta = 2 * (Integer.MAX_VALUE / count);
            int start = Integer.MIN_VALUE;
            for (int i = 0; i < count; ++i)
                result[i] = new MaelstromKey.Routing(start + i * delta);

            return result;
        }

        private static final int CHARS = 63;

        private static String toString(long v)
        {
            if (v == 0) return "";
            --v;
            char[] buf = new char[4];
            for (int i = 3 ; i >= 0 ; --i)
            {
                buf[i] = toChar(v % CHARS);
                v /= CHARS;
            }
            return new String(buf);
        }

        private static char toChar(long v)
        {
            if (v == 0) return ' ';
            v -= 1;
            if (v < 10) return (char) ('0' + v);
            v -= 10;
            if (v < 26) return (char) ('A' + v);
            v -= 26;
            return (char) ('a' + v);
        }

    }

    public final Kind kind;
    public final Object value;

    Datum(Kind kind, Object value)
    {
        this.kind = kind;
        this.value = value;
    }

    public Datum(String value)
    {
        this(Kind.STRING, value);
    }

    public Datum(Long value)
    {
        this(Kind.LONG, value);
    }

    public Datum(Double value)
    {
        this(Kind.DOUBLE, value);
    }

    public Datum(Hash value)
    {
        this(Kind.HASH, value);
    }

    @Override
    public int hashCode()
    {
        return value == null ? 0 : hash(value);
    }

    @Override
    public boolean equals(Object that)
    {
        return that == this || (that instanceof Datum && equals((Datum) that));
    }

    public boolean equals(Datum that)
    {
        return this.kind.equals(that.kind) && this.value.equals(that.value);
    }

    @Override
    public String toString()
    {
        return value == null ? kind + ":+Inf" : value.toString();
    }

    @Override
    public int compareTo(Datum that)
    {
        int c = 0;
        if (COMPARE_BY_HASH)
            c = Integer.compare(hash(this.value), hash(that.value));
        if (c == 0) c = this.kind.compareTo(that.kind);
        if (c != 0) return c;
        if (this.value == null || that.value == null)
        {
            if (this.value == null && that.value == null)
                return 0;
            return this.value == null ? 1 : -1;
        }
        return ((Comparable)this.value).compareTo(that.value);
    }

    static int hash(Object object)
    {
        if (object == null)
            return Integer.MAX_VALUE;

        if (object instanceof Hash)
            return ((Hash) object).hash;

        CRC32 crc32c = new CRC32();
        int i = object.hashCode();
        crc32c.update(i);
        crc32c.update(i >> 8);
        crc32c.update(i >> 16);
        crc32c.update(i >> 24);
        return (int)crc32c.getValue();
    }

    public static Datum read(JsonReader in) throws IOException
    {
        return read(in, Datum::new);
    }

    public void write(JsonWriter out) throws IOException
    {
        if (!isSimple())
        {
            out.beginArray();
            out.value(kind.toString());
            if (kind == Kind.HASH)
            {
                out.value(value != null);
                if (value != null)
                    out.value(((Hash)value).hash);
            }
            out.endArray();
            return;
        }
        switch (kind)
        {
            default: throw new IllegalStateException();
            case LONG: out.value((Long) value); break;
            case DOUBLE: out.value((Double) value); break;
            case STRING: out.value((String) value); break;
        }
    }

    public boolean isSimple()
    {
        return value != null && kind != Kind.HASH;
    }

    protected static <V> V read(JsonReader in, BiFunction<Kind, Object, V> constructor) throws IOException
    {
        Datum.Kind type;
        Object value;
        switch (in.peek())
        {
            default:
                throw new IllegalStateException();
            case BEGIN_ARRAY:
                in.beginArray();
                type = Kind.valueOf(in.nextString());
                if (type == Kind.HASH && in.nextBoolean()) value = new Hash(in.nextInt());
                else value = null;
                in.endArray();
                break;
            case STRING:
                value = in.nextString();
                type = Datum.Kind.STRING;
                break;
            case NUMBER:
                try { value = in.nextLong(); type = Datum.Kind.LONG; }
                catch (IllegalArgumentException iae) { value = in.nextDouble(); type = Datum.Kind.DOUBLE; }
                break;
        }
        return constructor.apply(type, value);
    }

    public static final TypeAdapter<Datum> GSON_ADAPTER = new TypeAdapter<Datum>()
    {
        @Override
        public void write(JsonWriter out, Datum value) throws IOException
        {
            value.write(out);
        }

        @Override
        public Datum read(JsonReader in) throws IOException
        {
            return Datum.read(in);
        }
    };
}
