/*
 * Copyright (C) 2014 The Android Open Source Project
 *
 * 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.android.tools.lint.checks;

import static com.android.tools.lint.checks.PluralsDatabase.Quantity.few;
import static com.android.tools.lint.checks.PluralsDatabase.Quantity.many;
import static com.android.tools.lint.checks.PluralsDatabase.Quantity.one;
import static com.android.tools.lint.checks.PluralsDatabase.Quantity.two;
import static com.android.tools.lint.checks.PluralsDatabase.Quantity.zero;

import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.lint.detector.api.LintUtils;
import com.google.common.collect.Maps;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.List;
import java.util.Map;

/**
 * Database used by the {@link PluralsDetector} to get information
 * about plural forms for a given language
 */
public class PluralsDatabase {
    private static final EnumSet<Quantity> NONE = EnumSet.noneOf(Quantity.class);

    private static final PluralsDatabase sInstance = new PluralsDatabase();
    private final Map<String, EnumSet<Quantity>> mPlurals = Maps.newHashMap();

    /** Bit set if this language uses quantity zero */
    @SuppressWarnings("PointlessBitwiseExpression")
    static final int FLAG_ZERO  = 1 << 0;
    /** Bit set if this language uses quantity one */
    static final int FLAG_ONE   = 1 << 1;
    /** Bit set if this language uses quantity two */
    static final int FLAG_TWO   = 1 << 2;
    /** Bit set if this language uses quantity few */
    static final int FLAG_FEW   = 1 << 3;
    /** Bit set if this language uses quantity many */
    static final int FLAG_MANY  = 1 << 4;
    /** Bit set if this language has multiple values that match quantity zero */
    static final int FLAG_MULTIPLE_ZERO = 1 << 5;
    /** Bit set if this language has multiple values that match quantity one */
    static final int FLAG_MULTIPLE_ONE  = 1 << 6;
    /** Bit set if this language has multiple values that match quantity two */
    static final int FLAG_MULTIPLE_TWO  = 1 << 7;

    @NonNull
    public static PluralsDatabase get() {
        return sInstance;
    }

    private static int getFlags(@NonNull String language) {
        int index = getLanguageIndex(language);
        if (index != -1) {
            return FLAGS[index];
        }
        return 0;
    }

    private static int getLanguageIndex(@NonNull String language) {
        int index = Arrays.binarySearch(LANGUAGE_CODES, language);
        if (index >= 0) {
            assert LANGUAGE_CODES[index].equals(language);
            return index;
        } else {
            return -1;
        }
    }

    @Nullable
    public EnumSet<Quantity> getRelevant(@NonNull String language) {
        EnumSet<Quantity> set = mPlurals.get(language);
        if (set == null) {
            int index = getLanguageIndex(language);
            if (index == -1) {
                mPlurals.put(language, NONE);
                return null;
            }

            // Process each item and look for relevance
            int flag = FLAGS[index];

            set = EnumSet.noneOf(Quantity.class);
            if ((flag & FLAG_ZERO) != 0) {
                set.add(zero);
            }
            if ((flag & FLAG_ONE) != 0) {
                set.add(one);
            }
            if ((flag & FLAG_TWO) != 0) {
                set.add(two);
            }
            if ((flag & FLAG_FEW) != 0) {
                set.add(few);
            }
            if ((flag & FLAG_MANY) != 0) {
                set.add(many);
            }

            mPlurals.put(language, set);
        }
        return set == NONE ? null : set;
    }

    @SuppressWarnings("MethodMayBeStatic")
    public boolean hasMultipleValuesForQuantity(
            @NonNull String language,
            @NonNull Quantity quantity) {
        if (quantity == one) {
            return (getFlags(language) & FLAG_MULTIPLE_ONE) != 0;
        } else if (quantity == two) {
            return (getFlags(language) & FLAG_MULTIPLE_TWO) != 0;
        } else {
            return quantity == zero && (getFlags(language) & FLAG_MULTIPLE_ZERO) != 0;
        }
    }

    @SuppressWarnings("MethodMayBeStatic")
    @Nullable
    public String findIntegerExamples(@NonNull String language, @NonNull Quantity quantity) {
        if (quantity == one) {
            return getExampleForQuantityOne(language);
        } else if (quantity == two) {
            return getExampleForQuantityTwo(language);
        } else if (quantity == zero) {
            return getExampleForQuantityZero(language);
        } else {
            return null;
        }
    }

    public enum Quantity {
        // deliberately lower case to match attribute names
        few, many, one, two, zero, other;

        @Nullable
        public static Quantity get(@NonNull String name) {
            for (Quantity quantity : values()) {
                if (name.equals(quantity.name())) {
                    return quantity;
                }
            }

            return null;
        }

        public static String formatSet(@NonNull EnumSet<Quantity> set) {
            List<String> list = new ArrayList<String>(set.size());
            for (Quantity quantity : set) {
                list.add('`' + quantity.name() + '`');
            }
            return LintUtils.formatList(list, Integer.MAX_VALUE);
        }
    }

    // GENERATED DATA.
    // This data is generated by the #testDatabaseAccurate method in PluralsDatabaseTest
    // which will generate the following if it can find an ICU plurals database file
    // in the unit test data folder.

    /** Set of language codes relevant to plurals data */
    private static final String[] LANGUAGE_CODES = new String[] {
            "af", "ak", "am", "ar", "as", "az", "be", "bg", "bh", "bm",
            "bn", "bo", "br", "bs", "ca", "ce", "cs", "cy", "da", "de",
            "dv", "dz", "ee", "el", "en", "eo", "es", "et", "eu", "fa",
            "ff", "fi", "fo", "fr", "fy", "ga", "gd", "gl", "gu", "gv",
            "ha", "he", "hi", "hr", "hu", "hy", "id", "ig", "ii", "in",
            "is", "it", "iu", "iw", "ja", "ji", "jv", "ka", "kk", "kl",
            "km", "kn", "ko", "ks", "ku", "kw", "ky", "lb", "lg", "ln",
            "lo", "lt", "lv", "mg", "mk", "ml", "mn", "mr", "ms", "mt",
            "my", "nb", "nd", "ne", "nl", "nn", "no", "nr", "ny", "om",
            "or", "os", "pa", "pl", "ps", "pt", "rm", "ro", "ru", "se",
            "sg", "si", "sk", "sl", "sn", "so", "sq", "sr", "ss", "st",
            "sv", "sw", "ta", "te", "th", "ti", "tk", "tl", "tn", "to",
            "tr", "ts", "ug", "uk", "ur", "uz", "ve", "vi", "vo", "wa",
            "wo", "xh", "yi", "yo", "zh", "zu"
    };

    /**
     * Relevant flags for each language (corresponding to each language listed
     * in the same position in {@link #LANGUAGE_CODES})
     */
    private static final int[] FLAGS = new int[] {
            0x0002, 0x0042, 0x0042, 0x001f, 0x0042, 0x0002, 0x005a, 0x0002,
            0x0042, 0x0000, 0x0042, 0x0000, 0x00de, 0x004a, 0x0002, 0x0002,
            0x000a, 0x001f, 0x0002, 0x0002, 0x0002, 0x0000, 0x0002, 0x0002,
            0x0002, 0x0002, 0x0002, 0x0002, 0x0002, 0x0042, 0x0042, 0x0002,
            0x0002, 0x0042, 0x0002, 0x001e, 0x00ce, 0x0002, 0x0042, 0x00ce,
            0x0002, 0x0016, 0x0042, 0x004a, 0x0002, 0x0042, 0x0000, 0x0000,
            0x0000, 0x0000, 0x0042, 0x0002, 0x0006, 0x0016, 0x0000, 0x0002,
            0x0000, 0x0002, 0x0002, 0x0002, 0x0000, 0x0042, 0x0000, 0x0002,
            0x0002, 0x0006, 0x0002, 0x0002, 0x0002, 0x0042, 0x0000, 0x004a,
            0x0063, 0x0042, 0x0042, 0x0002, 0x0002, 0x0042, 0x0000, 0x001a,
            0x0000, 0x0002, 0x0002, 0x0002, 0x0002, 0x0002, 0x0002, 0x0002,
            0x0002, 0x0002, 0x0002, 0x0002, 0x0042, 0x001a, 0x0002, 0x0042,
            0x0002, 0x000a, 0x005a, 0x0006, 0x0000, 0x0042, 0x000a, 0x00ce,
            0x0002, 0x0002, 0x0002, 0x004a, 0x0002, 0x0002, 0x0002, 0x0002,
            0x0002, 0x0002, 0x0000, 0x0042, 0x0002, 0x0042, 0x0002, 0x0000,
            0x0002, 0x0002, 0x0002, 0x005a, 0x0002, 0x0002, 0x0002, 0x0000,
            0x0002, 0x0042, 0x0000, 0x0002, 0x0002, 0x0000, 0x0000, 0x0042
    };

    @Nullable
    private static String getExampleForQuantityZero(@NonNull String language) {
        int index = getLanguageIndex(language);
        switch (index) {
            // set14
            case 72: // lv
                return "0, 10~20, 30, 40, 50, 60, 100, 1000, 10000, 100000, 1000000, \u2026";
            case -1:
            default:
                return null;
        }
    }

    @Nullable
    private static String getExampleForQuantityOne(@NonNull String language) {
        int index = getLanguageIndex(language);
        switch (index) {
            // set1
            case 2: // am
            case 4: // as
            case 10: // bn
            case 29: // fa
            case 38: // gu
            case 42: // hi
            case 61: // kn
            case 77: // mr
            case 135: // zu
                return "0, 1";
            // set11
            case 50: // is
                return "1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, \u2026";
            // set12
            case 74: // mk
                return "1, 11, 21, 31, 41, 51, 61, 71, 101, 1001, \u2026";
            // set13
            case 117: // tl
                return "0~3, 5, 7, 8, 10~13, 15, 17, 18, 20, 21, 100, 1000, 10000, 100000, 1000000, \u2026";
            // set14
            case 72: // lv
                return "1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, \u2026";
            // set2
            case 30: // ff
            case 33: // fr
            case 45: // hy
                return "0, 1";
            // set20
            case 13: // bs
            case 43: // hr
            case 107: // sr
                return "1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, \u2026";
            // set21
            case 36: // gd
                return "1, 11";
            // set22
            case 103: // sl
                return "1, 101, 201, 301, 401, 501, 601, 701, 1001, \u2026";
            // set27
            case 6: // be
                return "1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, \u2026";
            // set28
            case 71: // lt
                return "1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, \u2026";
            // set30
            case 98: // ru
            case 123: // uk
                return "1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, \u2026";
            // set31
            case 12: // br
                return "1, 21, 31, 41, 51, 61, 81, 101, 1001, \u2026";
            // set33
            case 39: // gv
                return "1, 11, 21, 31, 41, 51, 61, 71, 101, 1001, \u2026";
            // set4
            case 101: // si
                return "0, 1";
            // set5
            case 1: // ak
            case 8: // bh
            case 69: // ln
            case 73: // mg
            case 92: // pa
            case 115: // ti
            case 129: // wa
                return "0, 1";
            // set7
            case 95: // pt
                return "0, 1";
            case -1:
            default:
                return null;
        }
    }

    @Nullable
    private static String getExampleForQuantityTwo(@NonNull String language) {
        int index = getLanguageIndex(language);
        switch (index) {
            // set21
            case 36: // gd
                return "2, 12";
            // set22
            case 103: // sl
                return "2, 102, 202, 302, 402, 502, 602, 702, 1002, \u2026";
            // set31
            case 12: // br
                return "2, 22, 32, 42, 52, 62, 82, 102, 1002, \u2026";
            // set33
            case 39: // gv
                return "2, 12, 22, 32, 42, 52, 62, 72, 102, 1002, \u2026";
            case -1:
            default:
                return null;
        }
    }
}
