// © 2016 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
/*
 *******************************************************************************
 * Copyright (C) 2015-2016, International Business Machines Corporation and
 * others. All Rights Reserved.
 *******************************************************************************
 */
package com.ibm.icu.impl;

import java.nio.ByteBuffer;

import com.ibm.icu.util.UResourceBundle;
import com.ibm.icu.util.UResourceTypeMismatchException;

// Class UResource is named consistently with the public class UResourceBundle,
// in case we want to make it public at some point.

/**
 * ICU resource bundle key and value types.
 */
public final class UResource {
    /**
     * Represents a resource bundle item's key string.
     * Avoids object creations as much as possible.
     * Mutable, not thread-safe.
     * For permanent storage, use clone() or toString().
     */
    public static final class Key implements CharSequence, Cloneable, Comparable<Key> {
        // Stores a reference to the resource bundle key string bytes array,
        // with an offset of the key, to avoid creating a String object
        // until one is really needed.
        // Alternatively, we could try to always just get the key String object,
        // and cache it in the reader, and see if that performs better or worse.
        private byte[] bytes;
        private int offset;
        private int length;
        private String s;

        /**
         * Constructs an empty resource key string object.
         */
        public Key() {
            s = "";
        }

        /**
         * Constructs a resource key object equal to the given string.
         */
        public Key(String s) {
            setString(s);
        }

        private Key(byte[] keyBytes, int keyOffset, int keyLength) {
            bytes = keyBytes;
            offset = keyOffset;
            length = keyLength;
        }

        /**
         * Mutates this key for a new NUL-terminated resource key string.
         * The corresponding ASCII-character bytes are not copied and
         * must not be changed during the lifetime of this key
         * (or until the next setBytes() call)
         * and lifetimes of subSequences created from this key.
         *
         * @param keyBytes new key string byte array
         * @param keyOffset new key string offset
         */
        public Key setBytes(byte[] keyBytes, int keyOffset) {
            bytes = keyBytes;
            offset = keyOffset;
            for (length = 0; keyBytes[keyOffset + length] != 0; ++length) {}
            s = null;
            return this;
        }

        /**
         * Mutates this key to an empty resource key string.
         */
        public Key setToEmpty() {
            bytes = null;
            offset = length = 0;
            s = "";
            return this;
        }

        /**
         * Mutates this key to be equal to the given string.
         */
        public Key setString(String s) {
            if (s.isEmpty()) {
                setToEmpty();
            } else {
                bytes = new byte[s.length()];
                offset = 0;
                length = s.length();
                for (int i = 0; i < length; ++i) {
                    char c = s.charAt(i);
                    if (c <= 0x7f) {
                        bytes[i] = (byte)c;
                    } else {
                        throw new IllegalArgumentException('\"' + s + "\" is not an ASCII string");
                    }
                }
                this.s = s;
            }
            return this;
        }

        /**
         * {@inheritDoc}
         * Does not clone the byte array.
         */
        @Override
        public Key clone() {
            try {
                return (Key)super.clone();
            } catch (CloneNotSupportedException cannotOccur) {
                return null;
            }
        }

        @Override
        public char charAt(int i) {
            assert(0 <= i && i < length);
            return (char)bytes[offset + i];
        }

        @Override
        public int length() {
            return length;
        }

        @Override
        public Key subSequence(int start, int end) {
            assert(0 <= start && start < length);
            assert(start <= end && end <= length);
            return new Key(bytes, offset + start, end - start);
        }

        /**
         * Creates/caches/returns this resource key string as a Java String.
         */
        @Override
        public String toString() {
            if (s == null) {
                s = internalSubString(0, length);
            }
            return s;
        }

        private String internalSubString(int start, int end) {
            StringBuilder sb = new StringBuilder(end - start);
            for (int i = start; i < end; ++i) {
                sb.append((char)bytes[offset + i]);
            }
            return sb.toString();
        }

        /**
         * Creates a new Java String for a sub-sequence of this resource key string.
         */
        public String substring(int start) {
            assert(0 <= start && start < length);
            return internalSubString(start, length);
        }

        /**
         * Creates a new Java String for a sub-sequence of this resource key string.
         */
        public String substring(int start, int end) {
            assert(0 <= start && start < length);
            assert(start <= end && end <= length);
            return internalSubString(start, end);
        }

        private boolean regionMatches(byte[] otherBytes, int otherOffset, int n) {
            for (int i = 0; i < n; ++i) {
                if (bytes[offset + i] != otherBytes[otherOffset + i]) {
                    return false;
                }
            }
            return true;
        }

        private boolean regionMatches(int start, CharSequence cs, int n) {
            for (int i = 0; i < n; ++i) {
                if (bytes[offset + start + i] != cs.charAt(i)) {
                    return false;
                }
            }
            return true;
        }

        @Override
        public boolean equals(Object other) {
            if (other == null) {
                return false;
            } else if (this == other) {
                return true;
            } else if (other instanceof Key) {
                Key otherKey = (Key)other;
                return length == otherKey.length &&
                        regionMatches(otherKey.bytes, otherKey.offset, length);
            } else {
                return false;
            }
        }

        public boolean contentEquals(CharSequence cs) {
            if (cs == null) {
                return false;
            }
            return this == cs || (cs.length() == length && regionMatches(0, cs, length));
        }

        public boolean startsWith(CharSequence cs) {
            int csLength = cs.length();
            return csLength <= length && regionMatches(0, cs, csLength);
        }

        public boolean endsWith(CharSequence cs) {
            int csLength = cs.length();
            return csLength <= length && regionMatches(length - csLength, cs, csLength);
        }

        /**
         * @return true if the substring of this key starting from the offset
         *         contains the same characters as the other sequence.
         */
        public boolean regionMatches(int start, CharSequence cs) {
            int csLength = cs.length();
            return csLength == (length - start) && regionMatches(start, cs, csLength);
        }

        @Override
        public int hashCode() {
            // Never return s.hashCode(), so that
            // Key.hashCode() is the same whether we have cached s or not.
            if (length == 0) {
                return 0;
            }

            int h = bytes[offset];
            for (int i = 1; i < length; ++i) {
                h = 37 * h + bytes[offset];
            }
            return h;
        }

        @Override
        public int compareTo(Key other) {
            return compareTo((CharSequence)other);
        }

        public int compareTo(CharSequence cs) {
            int csLength = cs.length();
            int minLength = length <= csLength ? length : csLength;
            for (int i = 0; i < minLength; ++i) {
                int diff = charAt(i) - cs.charAt(i);
                if (diff != 0) {
                    return diff;
                }
            }
            return length - csLength;
        }
    }

    /**
     * Interface for iterating over a resource bundle array resource.
     * Does not use Java Iterator to reduce object creations.
     */
    public interface Array {
        /**
         * @return The number of items in the array resource.
         */
        public int getSize();
        /**
         * @param i Array item index.
         * @param value Output-only, receives the value of the i'th item.
         * @return true if i is non-negative and less than getSize().
         */
        public boolean getValue(int i, Value value);
    }

    /**
     * Interface for iterating over a resource bundle table resource.
     * Does not use Java Iterator to reduce object creations.
     */
    public interface Table {
        /**
         * @return The number of items in the table resource.
         */
        public int getSize();
        /**
         * @param i Table item index.
         * @param key Output-only, receives the key of the i'th item.
         * @param value Output-only, receives the value of the i'th item.
         * @return true if i is non-negative and less than getSize().
         */
        public boolean getKeyAndValue(int i, Key key, Value value);
        /**
         * @param key Key string to find in the table.
         * @param value Output-only, receives the value of the item with that key.
         * @return true if the table contains the key.
         */
        public boolean findValue(CharSequence key, Value value);
    }

    /**
     * Represents a resource bundle item's value.
     * Avoids object creations as much as possible.
     * Mutable, not thread-safe.
     */
    public static abstract class Value {
        protected Value() {}

        /**
         * @return ICU resource type like {@link UResourceBundle#getType()},
         *     for example, {@link UResourceBundle#STRING}
         */
        public abstract int getType();

        /**
         * @see UResourceBundle#getString()
         * @throws UResourceTypeMismatchException if this is not a string resource
         */
        public abstract String getString();

        /**
         * @throws UResourceTypeMismatchException if this is not an alias resource
         */
        public abstract String getAliasString();

        /**
         * @see UResourceBundle#getInt()
         * @throws UResourceTypeMismatchException if this is not an integer resource
         */
        public abstract int getInt();

        /**
         * @see UResourceBundle#getUInt()
         * @throws UResourceTypeMismatchException if this is not an integer resource
         */
        public abstract int getUInt();

        /**
         * @see UResourceBundle#getIntVector()
         * @throws UResourceTypeMismatchException if this is not an intvector resource
         */
        public abstract int[] getIntVector();

        /**
         * @see UResourceBundle#getBinary()
         * @throws UResourceTypeMismatchException if this is not a binary-blob resource
         */
        public abstract ByteBuffer getBinary();

        /**
         * @throws UResourceTypeMismatchException if this is not an array resource
         */
        public abstract Array getArray();

        /**
         * @throws UResourceTypeMismatchException if this is not a table resource
         */
        public abstract Table getTable();

        /**
         * Is this a no-fallback/no-inheritance marker string?
         * Such a marker is used for CLDR no-fallback data values of "∅∅∅"
         * when enumerating tables with fallback from the specific resource bundle to root.
         *
         * @return true if this is a no-inheritance marker string
         */
        public abstract boolean isNoInheritanceMarker();

        /**
         * @return the array of strings in this array resource.
         * @see UResourceBundle#getStringArray()
         * @throws UResourceTypeMismatchException if this is not an array resource
         *     or if any of the array items is not a string
         */
        public abstract String[] getStringArray();

        /**
         * Same as
         * <pre>
         * if (getType() == STRING) {
         *     return new String[] { getString(); }
         * } else {
         *     return getStringArray();
         * }
         * </pre>
         *
         * @see #getString()
         * @see #getStringArray()
         * @throws UResourceTypeMismatchException if this is
         *     neither a string resource nor an array resource containing strings
         */
        public abstract String[] getStringArrayOrStringAsArray();

        /**
         * Same as
         * <pre>
         * if (getType() == STRING) {
         *     return getString();
         * } else {
         *     return getStringArray()[0];
         * }
         * </pre>
         *
         * @see #getString()
         * @see #getStringArray()
         * @throws UResourceTypeMismatchException if this is
         *     neither a string resource nor an array resource containing strings
         */
        public abstract String getStringOrFirstOfArray();

        /**
         * Only for debugging.
         */
        @Override
        public String toString() {
            switch(getType()) {
            case UResourceBundle.STRING:
                return getString();
            case UResourceBundle.INT:
                return Integer.toString(getInt());
            case UResourceBundle.INT_VECTOR:
                int[] iv = getIntVector();
                StringBuilder sb = new StringBuilder("[");
                sb.append(iv.length).append("]{");
                if (iv.length != 0) {
                    sb.append(iv[0]);
                    for (int i = 1; i < iv.length; ++i) {
                        sb.append(", ").append(iv[i]);
                    }
                }
                return sb.append('}').toString();
            case UResourceBundle.BINARY:
                return "(binary blob)";
            case UResourceBundle.ARRAY:
                return "(array)";
            case UResourceBundle.TABLE:
                return "(table)";
            default:  // should not occur
                return "???";
            }
        }
    }

    /**
     * Sink for ICU resource bundle contents.
     */
    public static abstract class Sink {
        /**
         * Called once for each bundle (child-parent-...-root).
         * The value is normally an array or table resource,
         * and implementations of this method normally iterate over the
         * tree of resource items stored there.
         *
         * @param key Initially the key string of the enumeration-start resource.
         *     Empty if the enumeration starts at the top level of the bundle.
         *     Reuse for output values from Array and Table getters.
         * @param value Call getArray() or getTable() as appropriate.
         *     Then reuse for output values from Array and Table getters.
         * @param noFallback true if the bundle has no parent;
         *     that is, its top-level table has the nofallback attribute,
         *     or it is the root bundle of a locale tree.
         */
        public abstract void put(Key key, Value value, boolean noFallback);
    }
}
