package ru.yandex.autotests.directapi.steps.banners;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;

import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang3.RandomStringUtils;

/**
 * Created by andy-ilyin on 27.05.16.
 * <p>
 * Компонент, который случайным образом генерирует ключевые фразы с плюс- и минус-словами.
 * Все функции с названием на generate возвращают объект Result со словами. Фразу можно из
 * него получить методами toString, toStringWithOrderedWords.
 */
public final class KeywordPhraseGenerator {
    private final String characters;
    private final Comparator<String> wordSortOrder;

    /**
     * Тип объектов, в которых фразы. Большинство методов KeywordPhraseGenerator
     * возвращают объекты этого типа. Из объекта можно получить массивы
     * плюс- и минус-слов (методы plusWordArray, minusWordArray), фразу
     * целиком (toString, toStringWithOrderedWords) или слова через запятую
     * (commaSeparatedPlusWords, commaSeparatedMinusWords и т. п.)
     * <p>
     * Внутри объекта слова хранятся в каком-то порядке, который после создания объекта
     * не меняется. Методы, которые принимают аргумент boolean ordered, в зависимости
     * от него возвращают слова в таком порядке: если ordered=true, то в лексикографическом
     * без учёта регистра; если ordered=false, то в том порядке, в котором они в объекте.
     * Методы, которые возвращают слова, без аргумента ordered, возвращают слова в порядке,
     * в котором они внутри объекта. Поскольку слов внутри объекта не меняется,
     * разные методы, которые возвращают "неупорядоченные" слова, возвращают их
     * в одном и том же порядке.
     */
    public final static class Result {
        private final List<String> plusWords;
        private final List<String> minusWords;
        private final List<String> minusWordsWithHyphens;
        private final String separator;

        private final Comparator<String> wordSortOrder;

        private Result(List<String> plusWords, List<String> minusWords, int separatorLength,
                Comparator<String> wordSortOrder)
        {
            this.plusWords = Collections.unmodifiableList(new ArrayList<>(plusWords));
            this.minusWords = Collections.unmodifiableList(new ArrayList<>(minusWords));
            this.minusWordsWithHyphens = Collections.unmodifiableList(
                    this.minusWords.stream()
                            .map(minusWord -> "-" + minusWord)
                            .collect(Collectors.toList()));

            this.separator = StringUtils.repeat(" ", separatorLength);
            this.wordSortOrder = wordSortOrder;
        }

        /**
         * Массив плюс-слов
         *
         * @param ordered возвращать ли слова отсортированными лексикографически без учёта регистра.
         *                см. также документацию на класс Result про детали контракта про порядок слов,
         *                когда здесь false.
         * @return массив строк с плюс-словами, в каждом элементе одно плюс-слово
         */
        public String[] plusWordArray(boolean ordered) {
            if (ordered) {
                List<String> orderedWords = new ArrayList<>(plusWords);
                orderedWords.sort(wordSortOrder);
                return orderedWords.toArray(new String[0]);
            }

            return plusWords.toArray(new String[0]);
        }

        /**
         * Массив плюс-слов в том порядке, в котором они хранятся в объекте.
         * См. также документацию на класс Result про детали контракта про порядок слов
         *
         * @return массив строк с плюс-словами, в каждом элементе одно плюс-слово
         */
        public String[] plusWordArray() {
            return plusWordArray(false);
        }

        /**
         * Строка с плюс-словами, соответствующими предикату, через разделитель. В качестве разделителя
         * используется ", " (строка из двух символов, запятой и пробела).
         *
         * @param predicate предикат: функция от String, которая возвращает boolean.
         *                  Если функция возвращает true, слово включается в результат, иначе слово
         *                  не включается в результат.
         * @param ordered   возвращать ли слова отсортированными лексикографически без учёта регистра.
         *                  см. также документацию на класс Result про детали контракта про порядок слов,
         *                  когда здесь false.
         * @return строка из нужных плюс-слов
         */
        public String commaSeparatedFilteredPlusWords(Predicate<String> predicate, boolean ordered) {
            return Arrays.asList(plusWordArray(ordered)).stream()
                    .filter(predicate)
                    .collect(Collectors.joining(", "));
        }

        /**
         * Строка с плюс-словами, соответствующими предикату, через разделитель. В качестве разделителя
         * используется ", " (строка из двух символов, запятой и пробела).
         * См. также документацию на класс Result про детали контракта про порядок слов
         *
         * @param predicate предикат: функция от String, которая возвращает boolean.
         *                  Если функция возвращает true, слово включается в результат, иначе слово
         *                  не включается в результат.
         * @return строка из нужных плюс-слов
         */
        public String commaSeparatedFilteredPlusWords(Predicate<String> predicate) {
            return commaSeparatedFilteredPlusWords(predicate, false);
        }

        /**
         * Строка с плюс-словами через разделитель. В качестве разделителя используется ", "
         * (строка из двух символов, запятой и пробела).
         *
         * @param ordered возвращать ли слова отсортированными лексикографически без учёта регистра.
         *                см. также документацию на класс Result про детали контракта про порядок слов,
         *                когда здесь false.
         * @return строка с плюс-словами
         */
        public String commaSeparatedPlusWords(boolean ordered) {
            return commaSeparatedFilteredPlusWords(word -> true, ordered);
        }

        /**
         * Строка с плюс-словами через разделитель. В качестве разделителя используется ", "
         * (строка из двух символов, запятой и пробела).
         * См. также документацию на класс Result про детали контракта про порядок слов
         *
         * @return строка с плюс-словами
         */
        public String commaSeparatedPlusWords() {
            return commaSeparatedPlusWords(false);
        }

        /**
         * Массив минус-слов. Минус-слова возвращаются без предшествующего знака "-" (минуса).
         *
         * @param ordered возвращать ли слова отсортированными лексикографически без учёта регистра.
         *                см. также документацию на класс Result про детали контракта про порядок слов,
         *                когда здесь false.
         * @return массив строк с минус-словами, в каждом элементе одно минус-слово
         */
        public String[] minusWordArray(boolean ordered) {
            if (ordered) {
                List<String> orderedWords = new ArrayList<>(minusWords);
                orderedWords.sort(wordSortOrder);
                return orderedWords.toArray(new String[0]);
            }

            return minusWords.toArray(new String[0]);
        }

        /**
         * Массив минус-слов. Минус-слова возвращаются без предшествующего знака "-" (минуса).
         * См. также документацию на класс Result про детали контракта про порядок слов
         *
         * @return массив строк с минус-словами, в каждом элементе одно минус-слово
         */
        public String[] minusWordArray() {
            return minusWordArray(false);
        }

        /**
         * Строка с минус-словами, соответствующими предикату, через разделитель. В качестве разделителя
         * используется ", " (строка из двух символов, запятой и пробела). Минус-слова включаются
         * в результирующую строку без предшествующего знака "-" (минуса).
         *
         * @param predicate предикат: функция от String, которая возвращает boolean.
         *                  Если функция возвращает true, слово включается в результат, иначе слово
         *                  не включается в результат.
         * @param ordered   возвращать ли слова отсортированными лексикографически без учёта регистра.
         *                  см. также документацию на класс Result про детали контракта про порядок слов,
         *                  когда здесь false.
         * @return строка из нужных минус-слов
         */
        public String commaSeparatedFilteredMinusWords(Predicate<String> predicate, boolean ordered) {
            return Arrays.asList(minusWordArray(ordered)).stream()
                    .filter(predicate)
                    .collect(Collectors.joining(", "));
        }

        /**
         * Строка с минус-словами, соответствующими предикату, через разделитель. В качестве разделителя
         * используется ", " (строка из двух символов, запятой и пробела). Минус-слова включаются
         * в результирующую строку без предшествующего знака "-" (минуса).
         * См. также документацию на класс Result про детали контракта про порядок слов
         *
         * @param predicate предикат: функция от String, которая возвращает boolean.
         *                  Если функция возвращает true, слово включается в результат, иначе слово
         *                  не включается в результат.
         * @return строка из нужных минус-слов
         */
        public String commaSeparatedFilteredMinusWords(Predicate<String> predicate) {
            return commaSeparatedFilteredMinusWords(predicate, false);
        }

        /**
         * Строка с минус-словами через разделитель. В качестве разделителя используется ", "
         * (строка из двух символов, запятой и пробела). Минус-слова включаются в результирующую строку
         * без предшествующего знака "-" (минуса).
         *
         * @param ordered возвращать ли слова отсортированными лексикографически без учёта регистра.
         *                см. также документацию на класс Result про детали контракта про порядок слов,
         *                когда здесь false.
         * @return строка с минус-словами
         */
        public String commaSeparatedMinusWords(boolean ordered) {
            return commaSeparatedFilteredMinusWords(word -> true, ordered);
        }

        /**
         * Строка с минус-словами через разделитель. В качестве разделителя используется ", "
         * (строка из двух символов, запятой и пробела). Минус-слова включаются в результирующую строку
         * без предшествующего знака "-" (минуса).
         * См. также документацию на класс Result про детали контракта про порядок слов
         *
         * @return строка с минус-словами
         */
        public String commaSeparatedMinusWords() {
            return commaSeparatedMinusWords(false);
        }

        /**
         * Массив минус-слов. Минус-слова возвращаются с предшествующим знаком "-" (минуса).
         *
         * @param ordered возвращать ли слова отсортированными лексикографически без учёта регистра.
         *                см. также документацию на класс Result про детали контракта про порядок слов,
         *                когда здесь false.
         * @return массив строк с минус-словами, в каждом элементе одно минус-слово
         */
        public String[] minusWordArrayWithHyphens(boolean ordered) {
            if (ordered) {
                List<String> orderedWords = new ArrayList<>(minusWordsWithHyphens);
                orderedWords.sort(wordSortOrder);
                return orderedWords.toArray(new String[0]);
            }

            return minusWordsWithHyphens.toArray(new String[0]);
        }

        /**
         * Массив минус-слов. Минус-слова возвращаются с предшествующим знаком "-" (минуса).
         * См. также документацию на класс Result про детали контракта про порядок слов
         *
         * @return массив строк с минус-словами, в каждом элементе одно минус-слово
         */
        public String[] minusWordArrayWithHyphens() {
            return minusWordArrayWithHyphens(false);
        }

        /**
         * Строка с плюс-словами через разделитель. В качестве разделителя используется строка из одного
         * или нескольких пробелов. Если у функции, которая вернула этот объект, был аргумент
         * extraInnerSpaceCount, в разделителе 1 + extraInnerSpaceCount пробелов; иначе в ней
         * один пробел.
         *
         * @param ordered возвращать ли слова отсортированными лексикографически без учёта регистра.
         *                см. также документацию на класс Result про детали контракта про порядок слов,
         *                когда здесь false.
         * @return строка с плюс-словами
         */
        public String plusWordString(boolean ordered) {
            return String.join(separator, plusWordArray(ordered));
        }

        /**
         * Строка с плюс-словами через разделитель. В качестве разделителя используется строка из одного
         * или нескольких пробелов. Если у функции, которая вернула этот объект, был аргумент
         * extraInnerSpaceCount, в разделителе 1 + extraInnerSpaceCount пробелов; иначе в ней
         * один пробел.
         * См. также документацию на класс Result про детали контракта про порядок слов
         *
         * @return строка с плюс-словами
         */
        public String plusWordString() {
            return plusWordString(false);
        }

        /**
         * Строка с минус-словами через разделитель.
         * Минус-слова включаются в строку с предшествующим знаком "-" (минуса).
         * В качестве разделителя используется строка из одного или нескольких пробелов. Если
         * у функции, которая вернула этот объект, был аргумент extraInnerSpaceCount,
         * в разделителе 1 + extraInnerSpaceCount пробелов; иначе в нём один пробел.
         *
         * @param ordered возвращать ли слова отсортированными лексикографически без учёта регистра.
         *                см. также документацию на класс Result про детали контракта про порядок слов,
         *                когда здесь false.
         * @return строка с минус-словами
         */
        public String minusWordString(boolean ordered) {
            return String.join(separator, minusWordArrayWithHyphens(ordered));
        }

        /**
         * Строка с минус-словами через разделитель.
         * Минус-слова включаются в строку с предшествующим знаком "-" (минуса).
         * В качестве разделителя используется строка из одного или нескольких пробелов. Если
         * у функции, которая вернула этот объект, был аргумент extraInnerSpaceCount,
         * в разделителе 1 + extraInnerSpaceCount пробелов; иначе в нём один пробел.
         * См. также документацию на класс Result про детали контракта про порядок слов
         *
         * @return строка с минус-словами
         */
        public String minusWordString() {
            return minusWordString(false);
        }

        private String joinNonEmptyStrings(String... strings) {
            return Arrays.stream(strings).filter(s -> !s.isEmpty()).collect(Collectors.joining(separator));
        }

        /**
         * Строка со всеми плюс- и минус-словами через разделитель, сначала плюс-слова, потом
         * минус-слова.
         * Минус-слова включаются в строку с предшествующим знаком "-" (минуса).
         * В качестве разделителя используется строка из одного или нескольких пробелов. Если
         * у функции, которая вернула этот объект, был аргумент extraInnerSpaceCount,
         * в разделителе 1 + extraInnerSpaceCount пробелов; иначе в нём один пробел.
         * См. также документацию на класс Result про детали контракта про порядок слов
         *
         * @return строка со всеми словами
         */
        @Override
        public String toString() {
            return joinNonEmptyStrings(plusWordString(), minusWordString());
        }

        /**
         * Строка со всеми плюс- и минус-словами через разделитель, сначала плюс-слова, потом
         * минус-слова.
         * Минус-слова включаются в строку с предшествующим знаком "-" (минуса).
         * В качестве разделителя используется строка из одного или нескольких пробелов. Если
         * у функции, которая вернула этот объект, был аргумент extraInnerSpaceCount,
         * в разделителе 1 + extraInnerSpaceCount пробелов; иначе в нём один пробел.
         * И плюс-слова, и минус-слова возвращаются упорядоченными лексикографически
         * без учёта регистра.
         *
         * @return строка со всеми словами
         */
        public String toStringWithOrderedWords() {
            return joinNonEmptyStrings(plusWordString(true), minusWordString(true));
        }
    }

    /**
     * @param characters какие символы могут быть в словах: здесь должна быть строка без символа пробела
     * @throws IllegalArgumentException если в characters есть пробел
     */
    public KeywordPhraseGenerator(String characters) {
        this(characters, Comparator.naturalOrder());
    }

    /**
     * @param characters    какие символы могут быть в словах: здесь должна быть строка без символа пробела
     * @param wordSortOrder порядок сортировки слов
     * @throws IllegalArgumentException если в characters есть пробел
     */
    public KeywordPhraseGenerator(String characters, Comparator<String> wordSortOrder) {
        if (characters.contains(" ")) {
            throw new IllegalArgumentException("среди допустимых символов не может быть пробела");
        }

        this.characters = characters;
        this.wordSortOrder = wordSortOrder;
    }

    /**
     * Генерирует одно плюс-слово
     *
     * @param length длина слова
     */
    public String generateKeyword(Integer length) {
        return RandomStringUtils.random(length, characters);
    }

    /**
     * Генерирует фразу из нескольких плюс-слов
     *
     * @param keywordLengths длины слов
     */
    public Result generatePlusWordPhrase(Integer... keywordLengths) {
        return generatePhraseWithPlusAndMinusWords(keywordLengths, new Integer[0]);
    }

    /**
     * Генерирует фразу из нескольких плюс-слов одной и той же длины
     *
     * @param count         длина каждого слова
     * @param keywordLength сколько плюс-слов
     */
    public Result generatePlusWordPhraseOfSameLengthWords(Integer count, Integer keywordLength) {
        Integer[] plusWordLengths = new Integer[count];
        Arrays.fill(plusWordLengths, keywordLength);
        return generatePlusWordPhrase(plusWordLengths);
    }

    /**
     * Генерирует фразу из нескольких плюс-слов и нескольких минус-слов.
     *
     * @param plusWordLengths      длины плюс-слов
     * @param minusWordLengths     длины минус слов; если здесь пустой массив, минус-слов не будет
     * @param extraInnerSpaceCount сколько дополнительных пробелов вставлять между словами:
     *                             если здесь 0, слова будут разделены одним пробелом;
     *                             если 1, слова будут разделены двумя пробелами
     */
    public Result generatePhraseWithPlusAndMinusWords(
            Integer[] plusWordLengths,
            Integer[] minusWordLengths,
            Integer extraInnerSpaceCount)
    {
        Stream<String> plusWordStream = Arrays.asList(plusWordLengths).stream()
                .map(keywordLength -> generateKeyword(keywordLength));
        Stream<String> minusWordStream = Arrays.asList(minusWordLengths).stream()
                .map(keywordLength -> generateKeyword(keywordLength));

        return new Result(
                plusWordStream.collect(Collectors.toList()),
                minusWordStream.collect(Collectors.toList()),
                1 + extraInnerSpaceCount,
                wordSortOrder);
    }

    /**
     * Генерирует фразу из нескольких плюс-слов и нескольких минус-слов. Слова разделяются одним пробелом.
     *
     * @param plusWordLengths  длины плюс-слов
     * @param minusWordLengths длины минус слов; если здесь пустой массив, минус-слов не будет
     */
    public Result generatePhraseWithPlusAndMinusWords(Integer[] plusWordLengths, Integer[] minusWordLengths) {
        return generatePhraseWithPlusAndMinusWords(plusWordLengths, minusWordLengths, 0);
    }

    /**
     * Какой длины нужны минус-слова, чтобы посчитанная тем или иным способом
     * суммарная длина была равной заданной.
     *
     * @param maxMinusWordLength                 максимальная длина одного минус-слова, без минуса перед ним
     * @param spaceTakenByMinusWordWithMaxLength сколько "места" занимает минус-слово максимальной
     *                                           длины при подсчёте общей длины; считается, что если
     *                                           слово короче на 1 символ, при подсчёте максимальной длины
     *                                           "места" оно тоже занимает на 1 символ меньше
     * @param spaceToFill                        нужная суммарная длина
     * @return
     */
    public static List<Integer> minusWordLengthsWithSpecificTotalLength(
            Integer maxMinusWordLength,
            Integer spaceTakenByMinusWordWithMaxLength,
            Integer spaceToFill)
    {
        if (spaceToFill % spaceTakenByMinusWordWithMaxLength == 0) {
            // здесь простой случай: можно забить фразу минус-словами одной и той же длины
            return IntStream.rangeClosed(1, spaceToFill / spaceTakenByMinusWordWithMaxLength)
                    .mapToObj(idx -> maxMinusWordLength)
                    .collect(Collectors.toList());
        } else {
            // если оставшееся место не делится нацело на длину минус-слова + разделителя, возьмём столько слов,
            // чтобы заполнить фразу с избытком, и первые несколько из этих слов сделаем короче на 1 символ
            Integer minusWordCount = spaceToFill / spaceTakenByMinusWordWithMaxLength + 1;
            Integer shorterMinusWordCount = minusWordCount * spaceTakenByMinusWordWithMaxLength - spaceToFill;

            return IntStream.rangeClosed(1, minusWordCount)
                    .mapToObj(idx -> idx <= shorterMinusWordCount ? maxMinusWordLength - 1 : maxMinusWordLength)
                    .collect(Collectors.toList());
        }
    }

    /**
     * Генерирует фразу заданной длины. Фраза составляется из плюс-слов, длина которых передаётся явно,
     * и минус-слов, длина которых как-то считается, чтобы вся фраза получилась нужной длины.
     *
     * @param plusWordLengths      длины плюс-слов
     * @param phraseLength         длина всей фразы
     * @param maxMinusWordLength   максимальная длина минус-слова: минус-слова будут либо такой же длины,
     *                             либо на один символ короче
     * @param extraInnerSpaceCount сколько дополнительных пробелов вставлять между словами:
     *                             если здесь 0, слова будут разделены одним пробелом;
     */
    public Result generatePhraseOfSpecificLength(
            Integer[] plusWordLengths,
            Integer phraseLength,
            Integer maxMinusWordLength,
            Integer extraInnerSpaceCount)
    {
        Integer spaceDelimiterLength = extraInnerSpaceCount + 1;

        // сколько места во фразе займут плюс-слова: каждое займёт свою длину + у всех, кроме последнего, справа
        // будет пробел
        Integer spaceTakenByPlusWords =
                Arrays.asList(plusWordLengths).stream().mapToInt(i -> i + spaceDelimiterLength).sum()
                        - 1;

        Integer spaceRemainingForMinusWords = phraseLength - spaceTakenByPlusWords;

        // остальные слова будут минус-словами, из которых почти все будут длины defaultKeywordLength
        // вот столько во фразе займёт минус-слово длины по умолчанию
        Integer spaceTakenByRegularMinusWord = maxMinusWordLength + 1 + spaceDelimiterLength;

        List<Integer> minusWordLengths = minusWordLengthsWithSpecificTotalLength(
                maxMinusWordLength,
                spaceTakenByRegularMinusWord,
                spaceRemainingForMinusWords);

        return generatePhraseWithPlusAndMinusWords(
                plusWordLengths,
                minusWordLengths.toArray(new Integer[0]),
                extraInnerSpaceCount);
    }

    /**
     * Генерирует фразу заданной длины. Фраза составляется из плюс-слов, длина которых передаётся явно,
     * и минус-слов, длина которых как-то считается, чтобы вся фраза получилась нужной длины.
     *
     * @param plusWordLengths    длины плюс-слов
     * @param phraseLength       длина всей фразы
     * @param maxMinusWordLength максимальная длина минус-слова: минус-слова будут либо такой же длины,
     *                           либо на один символ короче
     */
    public Result generatePhraseOfSpecificLength(
            Integer[] plusWordLengths,
            Integer phraseLength,
            Integer maxMinusWordLength)
    {
        return generatePhraseOfSpecificLength(plusWordLengths, phraseLength, maxMinusWordLength, 0);
    }


}
