package ru.yandex.webmaster3.core.semantic.semantic_document_parser.microformats;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.IDN;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ExecutorService;

import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.Header;
import org.apache.http.entity.ContentType;
import org.jetbrains.annotations.Nullable;
import org.json.JSONException;
import org.json.JSONObject;

import ru.yandex.common.util.CanonicalCharset;
import ru.yandex.common.util.IOUtils;
import ru.yandex.common.util.StringUtils;
import ru.yandex.common.util.URLUtils;
import ru.yandex.common.util.collections.CollectionFactory;
import ru.yandex.common.util.collections.MultiMap;
import ru.yandex.common.util.collections.Pair;
import ru.yandex.common.util.concurrent.Executors;
import ru.yandex.common.util.http.charset.CharsetDetector;
import ru.yandex.common.util.xml.Tagable;
import ru.yandex.common.util.xml.Xmler;
import ru.yandex.webmaster3.core.WebmasterException;
import ru.yandex.webmaster3.core.http.WebmasterErrorResponse;
import ru.yandex.webmaster3.core.semantic.semantic_document_parser.exceptions.ExceptionSerializerFactory;
import ru.yandex.webmaster3.core.semantic.semantic_document_parser.exceptions.TankerVerifierExceptionSerializer;
import ru.yandex.webmaster3.core.semantic.semantic_document_parser.microdata.MicrodataUtils;
import ru.yandex.webmaster3.core.semantic.semantic_document_parser.microdata.data.ComplexMicrodata;
import ru.yandex.webmaster3.core.semantic.semantic_document_parser.microdata.data.Microdata;
import ru.yandex.webmaster3.core.semantic.semantic_document_parser.microdata.exceptions.IslandsValidationMicrodataValidatorException;
import ru.yandex.webmaster3.core.semantic.semantic_document_parser.microdata.exceptions.MicrodataValidatorException;
import ru.yandex.webmaster3.core.semantic.semantic_document_parser.microdata.validators.BatchMicrodataValidator;
import ru.yandex.webmaster3.core.semantic.semantic_document_parser.microdata.validators.MicrodataValidator;
import ru.yandex.webmaster3.core.semantic.semantic_document_parser.microformats.data.MicroformatData;
import ru.yandex.webmaster3.core.semantic.semantic_document_parser.microformats.exceptions.MFException;
import ru.yandex.webmaster3.core.semantic.semantic_document_parser.microformats.exceptions.MFExceptions;
import ru.yandex.webmaster3.core.semantic.semantic_document_parser.microformats.spec.instances.HAtom;
import ru.yandex.webmaster3.core.semantic.semantic_document_parser.microformats.spec.instances.HAudio;
import ru.yandex.webmaster3.core.semantic.semantic_document_parser.microformats.spec.instances.HCalendar;
import ru.yandex.webmaster3.core.semantic.semantic_document_parser.microformats.spec.instances.HCalendarEvent;
import ru.yandex.webmaster3.core.semantic.semantic_document_parser.microformats.spec.instances.HCard;
import ru.yandex.webmaster3.core.semantic.semantic_document_parser.microformats.spec.instances.HMedia;
import ru.yandex.webmaster3.core.semantic.semantic_document_parser.microformats.spec.instances.HNews;
import ru.yandex.webmaster3.core.semantic.semantic_document_parser.microformats.spec.instances.HRecipe;
import ru.yandex.webmaster3.core.semantic.semantic_document_parser.microformats.spec.instances.HResume;
import ru.yandex.webmaster3.core.semantic.semantic_document_parser.microformats.spec.instances.HReview;
import ru.yandex.webmaster3.core.semantic.semantic_document_parser.microformats.spec.instances.HReviewAggregate;
import ru.yandex.webmaster3.core.semantic.semantic_document_parser.microformats.spec.instances.MicroformatsManager;
import ru.yandex.webmaster3.core.semantic.semantic_document_parser.microformats.validators.MicroformatValidator;
import ru.yandex.webmaster3.core.semantic.semantic_document_parser.ogp.exception.OGPValidator;
import ru.yandex.webmaster3.core.semantic.semantic_document_parser.rdfa.RDFaUtils;
import ru.yandex.webmaster3.core.semantic.semantic_document_parser.rdfa.data.JSONLDEntity;
import ru.yandex.webmaster3.core.semantic.semantic_document_parser.rdfa.data.RDFaEntity;
import ru.yandex.webmaster3.core.semantic.semantic_document_parser.rdfa.exceptions.JsonParsingRDFaException;
import ru.yandex.webmaster3.core.semantic.semantic_document_parser.rdfa.exceptions.RDFaException;
import ru.yandex.webmaster3.core.semantic.semantic_document_parser.rdfa.jsonld.JSONLDExpansionException;
import ru.yandex.webmaster3.core.semantic.semantic_document_parser.rdfa.jsonld.JSONLDParser;
import ru.yandex.webmaster3.core.semantic.semantic_document_parser.rdfa.jsonld.JSONLDParserFactory;
import ru.yandex.webmaster3.core.semantic.semantic_document_parser.rdfa.transformers.ExperimentalExtractor;
import ru.yandex.webmaster3.core.semantic.semantic_document_parser.rdfa.validator.RDFaValidator;
import ru.yandex.webmaster3.core.semantic.semantic_document_parser.rta.RTAExtractor;
import ru.yandex.webmaster3.core.semantic.semantic_document_parser.serialize.util.APIVersion;
import ru.yandex.webmaster3.core.semantic.semantic_document_parser.tanker.LanguageContext;
import ru.yandex.webmaster3.core.zora.GoZoraService;
import ru.yandex.webmaster3.core.zora.go_data.request.GoZoraRequest;

import static ru.yandex.common.util.StringUtils.isEmpty;
import static ru.yandex.common.util.collections.CollectionFactory.pair;
import static ru.yandex.common.util.xml.Xmler.Tag;
import static ru.yandex.common.util.xml.Xmler.attribute;
import static ru.yandex.common.util.xml.Xmler.header;
import static ru.yandex.common.util.xml.Xmler.tag;

/**
 * Created by IntelliJ IDEA.
 * User: rasifiel
 * Date: 7/28/11
 * Time: 1:15 PM
 * To change this template use File | Settings | File Templates.
 */
@Data
@Slf4j
public class FrontEnd {
    private static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
    public static final int MAX_SIZE = 1000000;
    private static final int MAX_BYTE_SIZE = 8000000;
    public static final int LOAD_TIMEOUT = 8000;
    public static final int CONNECT_TIMEOUT = 8000;
    public static final Integer HASH_OF_JSON_EXPANSION = -34;
    private static final int DOWNLOAD_THREADS = 100;
    private static final int CROP_LENGTH = 150;
    public static final String DOCUMENT_IS_TOO_COMPLEX_LENGTH_LIMIT_EXCEEDED = "Document is too complex, length limit exceeded";


    private GoZoraService goZoraService;
    private JSONLDParserFactory jsonldParserFactory;
    private ExceptionSerializerFactory exceptionSerializerFactory;
    private List<MicrodataValidator> microdataValidators = Collections.emptyList();
    private List<OGPValidator> ogpValidators = Collections.emptyList();
    private Map<String, LanguageContext> contextMap;
    private List<MicroformatValidator> microformatValidators = Collections.emptyList();
    private List<RDFaValidator> rdfaValidators = Collections.emptyList();
    private RequestReporter requestReporter;
    private BatchMicrodataValidator batchMicrodataValidator;
    private List<MicrodataValidator> islandsValidators = Collections.emptyList();

    ExecutorService downloadExecutor = Executors.newFixedThreadPool(DOWNLOAD_THREADS);

    public String downloadWithTimeout(final URL url) throws IOException {
        Pair<byte[], Charset> raw = downloadRaw(url);
        return new String(raw.first, raw.second);
    }

    public Pair<byte[], Charset> downloadRaw(final URL url) throws IOException {
        final long startTime = System.currentTimeMillis();
        String idnHost = IDN.toASCII(url.getHost());
        URI idnUri = null;
        boolean requestFromURL = false;
        try {
            final URI uri = url.toURI();
            if (idnHost.contains("_")) {
                requestFromURL = true;
            } else {
                idnUri = new URI(uri.getScheme(), uri.getUserInfo(), idnHost, uri.getPort(), uri.getPath(),
                        uri.getQuery(), uri.getFragment());
            }
        } catch (URISyntaxException e) {
            log.error(e.getMessage(), e);
            throw IOUtils.wrapIO("URI Syntax Error", e);
        }

        String urlString = requestFromURL ? url.toString() : idnUri.toASCIIString();
        GoZoraRequest request = new GoZoraRequest(urlString, true, true, true);
        final Pair<ContentType, byte[]> contentAndEntity = goZoraService.processResponse(request, httpResponse -> {
            Header[] headers = httpResponse.getAllHeaders();
            for (Header header : headers) {
                if ("Content-Length".equals(header.getName()) && Long.parseLong(header.getValue()) > MAX_SIZE) {
                    throw new WebmasterException(DOCUMENT_IS_TOO_COMPLEX_LENGTH_LIMIT_EXCEEDED,
                            new WebmasterErrorResponse.SitaErrorResponse(getClass(),
                                    DOCUMENT_IS_TOO_COMPLEX_LENGTH_LIMIT_EXCEEDED, null));
                }
            }
            ContentType contentType = ContentType.get(httpResponse.getEntity());
            ByteArrayOutputStream resultWriter = new ByteArrayOutputStream();
            httpResponse.getEntity().writeTo(resultWriter);
            final byte[] contents = resultWriter.toByteArray();
            return Pair.of(contentType, contents);
        });
        log.info("{} : Loaded in {} ms", url, System.currentTimeMillis() - startTime);

        byte[] contents = contentAndEntity.getSecond();
        ContentType contentType = contentAndEntity.getFirst();

        if (contents.length > MAX_BYTE_SIZE) {
            throw new IllegalArgumentException(DOCUMENT_IS_TOO_COMPLEX_LENGTH_LIMIT_EXCEEDED);
        }
        final Charset enc;
        if (contentType != null && contentType.getCharset() != null) {
            enc = contentType.getCharset();
        } else {
            enc = DEFAULT_CHARSET;
        }
        log.info("Proposed charset - {}", enc.displayName());
        final CharsetDetector detector = new RussianHTMLCharsetDetector();
        final Charset trueCharset = detector.detectActualCharset(contents, enc);
        return pair(contents, trueCharset);
    }

    public static Xmler.Tag verifierNode(final String caption, final Integer hash, final boolean isNode) {
        return verifierNode(caption, hash, isNode, Collections.<Tagable>emptyList());
    }

    public static Xmler.Tag verifierNode(final String caption, @Nullable final Integer hash, final boolean isNode, final Tagable... tags) {
        Xmler.Attribute attributes = attribute("name", caption);
        if (hash != null) {
            attributes = attributes.and("hash", hash);
        }
        if (isNode) {
            attributes = attributes.and("format", "format");
        }
        return tag("node", attributes, tags);
    }

    public static Xmler.Tag verifierNode(final String caption, final Integer hash, final boolean isNode, final List<? extends Tagable> tags) {
        return verifierNode(caption, hash, isNode, tags.toArray(new Tagable[tags.size()]));
    }

    public static String cropString(final String str) {
        return str.length() <= CROP_LENGTH ? str : str.substring(0, CROP_LENGTH) + "...";
    }

    public String downloadByURL(final String url) throws IOException {
        final URL uri = new URL(URLUtils.fixEncodingAtWholeUrl(url, DEFAULT_CHARSET));
        if (!"http".equals(uri.getProtocol()) && !"https".equals(uri.getProtocol())) {
            throw new IOException("Wrong protocol");
        }
        return downloadWithTimeout(uri);
    }

    public VerifierProcessor getProcessor(final LanguageContext context) {
        return new VerifierProcessor(context);
    }

    public class VerifierProcessor {

        private final LanguageContext context;

        public VerifierProcessor(final LanguageContext context) {
            this.context = context;
        }

        public String processByURL(final String url, final boolean isIslandVerifier) throws IOException {
            final URL uri;
            try {
                uri = new URL(URLUtils.fixEncodingAtWholeUrl(url, DEFAULT_CHARSET));
            } catch (Exception e) {
                throw IOUtils.wrapIO("Invalid url", e);
            }
            if (!"http".equals(uri.getProtocol()) && !"https".equals(uri.getProtocol())) {
                throw new IOException("Wrong protocol");
            }
            final String content;

            content = downloadWithTimeout(uri);
            return process(content, url, isIslandVerifier);

        }

        public String processByContent(final String content, final String url, final boolean isIslandVerifier) throws IOException {
            try {
                return process(content, url == null ? "" : url, isIslandVerifier);
            } catch (Exception e) {
                throw IOUtils.wrapIO("Parsing error", e);
            }
        }

        //https://st.yandex-team.ru/WMCSUPPORT-1259
        private String filterBadSymbols(String content) {
            return content.replaceAll("\\u2028", "").replaceAll("\\u2029", "");
        }


        private List<Xmler.Tag> rdfaValidation(TankerVerifierExceptionSerializer exceptionSerializer, RDFaEntity e) {
            List<Xmler.Tag> result = new ArrayList<>();
            for (final RDFaValidator validator : rdfaValidators) {
                for (final RDFaException exception : validator.validate(e)) {
                    result.addAll(exceptionSerializer.toXML(exception));
                }
            }
            return result;
        }

        private String process(final String content, final String url, final boolean isIslandVerifier) {
            String result = null;
            if (content.length() > MAX_SIZE) {
                throw new IllegalArgumentException(DOCUMENT_IS_TOO_COMPLEX_LENGTH_LIMIT_EXCEEDED);
            }
            final TankerVerifierExceptionSerializer exceptionSerializer =
                    exceptionSerializerFactory.createSerializer(context);
            final long timeStart = System.currentTimeMillis();
            final StringBuilder responseBuilder = new StringBuilder();
            header().toXml(responseBuilder);
            final List<Xmler.Tag> tags = new LinkedList<>();
            tags.add(verifierNode("", 0, false));
            tags.addAll(RTAExtractor.extractRTA(content, url));
            final String clearContent = filterBadSymbols(content);
            final List<Microdata> microdatas = MicrodataUtils.extractMD(clearContent, url, false, true);

            final Pair<List<RDFaEntity>, List<RDFaException>> rdfaEntities =
                    ExperimentalExtractor.getResults(clearContent, url, APIVersion.VERSION_1_0);
            if (rdfaEntities.first.size() != 1 || !rdfaEntities.first.get(0).isEmpty()) {
                tags.addAll(parseRDFaEntity(url, exceptionSerializer, rdfaEntities));
            }
            for (final RDFaException exception : rdfaEntities.second) {
                tags.addAll(exceptionSerializer.toXML(exception));
            }
            final List<MicrodataValidatorException> exceptions = parseMicrodatas(isIslandVerifier, tags, microdatas);
            final Pair<List<MicroformatData>, List<MFException>> results = MicroformatsUtils.extractMF(clearContent, url,
                    MicroformatsManager.managerForMFsAndIncluded(HCard.getInstance(), HReview.getInstance(),
                            HRecipe.getInstance(), HMedia.getInstance(), HCalendarEvent.getInstance(),
                            HResume.getInstance(), HNews.getInstance(), HAtom.getInstance(), HAudio.getInstance(),
                            HCalendar.getInstance(), HReviewAggregate.getInstance()), false);
            final HashSet<MFException> exs = new HashSet<>(results.second);
            for (final MicroformatData data : results.first) {
                for (final MicroformatValidator validator : microformatValidators) {
                    exs.addAll(validator.validate(data).getExceptionsByListView());
                }
                tags.add(MicroformatsUtils.toXmlTag(data, null, true));
            }
            exs.forEach(e -> tags.addAll(exceptionSerializer.toXML(e)));
            exceptions.forEach(e -> tags.addAll(exceptionSerializer.toXML(e)));
            if (tags.size() == 1) {
                tags.remove(0);
            }
            tag("cards", tags).toXml(responseBuilder.append("\n"));
            log.info("{}: Processed in {}", url, (System.currentTimeMillis() - timeStart) + " ms");
            log.info("From {} extracted:{}", url, responseBuilder);
            result = responseBuilder.toString();
            if (requestReporter != null) {
                requestReporter.report(url, isEmpty(url) ? clearContent : null, result);
            }
            return result;
        }

        private List<Tag> parseRDFaEntity(String url, TankerVerifierExceptionSerializer exceptionSerializer, Pair<List<RDFaEntity>, List<RDFaException>> rdfaEntities) {
            List<Tag> result = new ArrayList<>();
            JSONLDParser jsonldParser = jsonldParserFactory.createParser();
            for (final RDFaEntity entity : rdfaEntities.first) {
                if (entity.isRoot && entity instanceof JSONLDEntity && entity.hasProperty("@context")) {
                    try {
                        List<RDFaEntity> entities = jsonldParser.expandDocument(entity, url);
                        result.addAll(RDFaUtils.getVerifierNodes(entities, true));
                        for (final RDFaEntity e : entities) {
                            result.addAll(rdfaValidation(exceptionSerializer, e));
                        }
                    } catch (JSONLDExpansionException e) {
                        result.addAll(RDFaUtils.getVerifierNodes(List.of(entity), false));
                        result.addAll(exceptionSerializer.toXML(e.getRDFaException(entity)));
                        result.addAll(rdfaValidation(exceptionSerializer, entity));
                    }
                } else {
                    result.addAll(RDFaUtils.getVerifierNodes(List.of(entity), false));
                    result.addAll(rdfaValidation(exceptionSerializer, entity));
                }
            }
            return result;
        }


        public MultiMap<Integer, JSONObject> getExceptions(final List<Microdata> microdatas,
                                                           final List<MicroformatData> microformatDatas,
                                                           final List<RDFaEntity> rdfaEntities,
                                                           final List<RDFaEntity> jsonldEntities,
                                                           final List<MFException> additionalMFExceptions,
                                                           final List<RDFaException> rdFaExceptions) throws JSONException {
            final TankerVerifierExceptionSerializer exceptionSerializer =
                    exceptionSerializerFactory.createSerializer(context);
            MultiMap<Integer, JSONObject> result = new MultiMap<>();
            for (final Microdata md : microdatas) {
                for (final MicrodataValidator validator : microdataValidators) {
                    for (final MicrodataValidatorException ex : validator.validate(md)) {
                        if (!(ex instanceof IslandsValidationMicrodataValidatorException)) {
                            result.appendAll(ex.microdata.number, exceptionSerializer.toJson(ex));
                        }
                    }
                }
            }
            if (batchMicrodataValidator != null) {
                for (final MicrodataValidatorException ex : batchMicrodataValidator.validate(microdatas)) {
                    if (!(ex instanceof IslandsValidationMicrodataValidatorException)) {
                        result.appendAll(ex.microdata.number, exceptionSerializer.toJson(ex));
                    }
                }
            }
            for (final RDFaEntity e : rdfaEntities) {
                for (final RDFaValidator validator : rdfaValidators) {
                    for (final RDFaException exception : validator.validate(e)) {
                        result.appendAll(exception.card.count, exceptionSerializer.toJson(exception));
                    }
                }
            }

            for (final RDFaEntity entity : jsonldEntities) {
                for (final RDFaValidator validator : rdfaValidators) {
                    for (final RDFaException exception : validator.validate(entity)) {
                        result.appendAll(exception.card.count, exceptionSerializer.toJson(exception));
                    }
                }
            }


            for (final MicroformatData data : microformatDatas) {
                for (final MicroformatValidator validator : microformatValidators) {
                    for (final MFException ex : validator.validate(data).getExceptionsByListView()) {
                        result.appendAll(data.number, exceptionSerializer.toJson(ex));
                    }
                }
            }
            if (additionalMFExceptions != null) {
                for (final MFException ex : additionalMFExceptions) {
                    if (ex instanceof MFExceptions) {
                        for (final MFException e : ((MFExceptions) ex).getExceptionsByListView()) {
                            result.appendAll(e.getCard().number, exceptionSerializer.toJson(e));
                        }
                    } else {
                        result.appendAll(ex.getCard().number, exceptionSerializer.toJson(ex));
                    }
                }
            }
            if (rdFaExceptions != null) {
                for (final RDFaException e : rdFaExceptions) {
                    if (e.card != null) {
                        result.appendAll(e.card.count, exceptionSerializer.toJson(e));
                    }
                    if (e instanceof JsonParsingRDFaException) {
                        result.appendAll(HASH_OF_JSON_EXPANSION, exceptionSerializer.toJson(e));
                    }
                }
            }
            return result;
        }
    }

    private List<MicrodataValidatorException> parseMicrodatas(boolean isIslandVerifier, List<Tag> tags, List<Microdata> microdatas) {
        List<MicrodataValidatorException> exceptions = new ArrayList<>();
        for (final Microdata element : microdatas) {
            if (element instanceof ComplexMicrodata) {
                final String baseItemType = ((ComplexMicrodata) element).getType();
                final String type = MicrodataUtils.extractType(baseItemType);
                if (microdataValidators != null) {
                    for (final MicrodataValidator validator : microdataValidators) {
                        exceptions.addAll(validator.validate(element));
                    }
                }
                tags.add(MicrodataUtils.toXml(element, Optional.of(type).map(String::toLowerCase).orElse(null), true));
            }
        }
        if (isIslandVerifier && batchMicrodataValidator != null) {
            exceptions.addAll(batchMicrodataValidator.validate(microdatas));
        }
        return exceptions;
    }

    public static class RussianHTMLCharsetDetector implements CharsetDetector {
        private static final boolean[] LATIN_ALPHANUMS_AND_PUNCTUATION = StringUtils.asBitset(
                "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567890" +
                        "|`~!@#$%^&*()[]{}<>-=_+/\\'\",.?;:\r\n\t  ");
        private static final boolean[] CYRILLIC_LETTERS =
                StringUtils.asBitset("абвгдеёжзиклмнопрстуфхцчшщъыьэюяАБВГДЕЁЖЗИКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯ");
        private static final boolean[] CYRILLIC_CONSONANTS =
                StringUtils.asBitset("бвгджзклмнпрстфхцчшщъьБВГДЖЗКЛМНПРСТФХЦЧШЩЪЬ");
        private static final Set<String> OK_CONSONANTS = Set.of("льств", "тств");


        private static final List<Charset> CHARSETS =
                CollectionFactory.list(CanonicalCharset.forName("windows-1251"), CanonicalCharset.forName("koi8-r"),
                        CanonicalCharset.forName("cp-1252"),
                        CanonicalCharset.forName("utf-8"));

        public Charset detectActualCharset(byte[] s, @Nullable Charset proposedCharset) {
            Charset checkFirst = proposedCharset == null ? Charset.defaultCharset() : proposedCharset;
            if (isGood(s, checkFirst)) {
                return checkFirst;
            }
            for (Charset charset : CHARSETS) {
                if (!charset.equals(checkFirst) && isGood(s, charset)) {
                    return charset;
                }
            }
            return checkFirst;
        }

        private boolean isGood(final byte[] b, Charset c) {
            if (c == null) {
                return false;
            }
            return isGood(new String(b, c));
        }

        private boolean isGood(final String s) {
            int cyrillicLowercase = 0;
            int cyrillicUppercase = 0;
            int specials = 0;
            int strangeCaseTransitions = 0;
            int normalCaseTransitions = 0;
            int longConsonantSequences = 0;
            boolean lastWasLowercase = false;
            boolean lastWasUppercase = false;
            int consonantSequence = 0;
            for (int i = 0; i < s.length(); ++i) {
                char c = s.charAt(i);
                if (isCyrillic(c)) {
                    if (isUppercase(c)) {
                        cyrillicUppercase++;
                        if (lastWasLowercase) {
                            strangeCaseTransitions++;
                        }
                        lastWasLowercase = false;
                        lastWasUppercase = true;
                    } else {
                        cyrillicLowercase++;
                        if (lastWasUppercase) {
                            normalCaseTransitions++;
                        }
                        lastWasLowercase = true;
                        lastWasUppercase = false;
                    }

                    if (isConsonant(c)) {
                        consonantSequence++;
                    } else {
                        if (consonantSequence > 3) {
                            String sequence = s.substring(i - consonantSequence, i);
                            if (!OK_CONSONANTS.contains(sequence.toLowerCase())) {
                                consonantSequence = Math.min(consonantSequence, 6);
                                longConsonantSequences += (consonantSequence - 2) * (consonantSequence - 2);
                            }
                        }
                        consonantSequence = 0;
                    }
                } else {
                    if (!isLatinAlphanumOrPunctuation(c)) {
                        specials++;
                    }
                    lastWasLowercase = false;
                    lastWasUppercase = false;
                    consonantSequence = 0;
                }
            }

            boolean isSpecialValueBigger = specials > Math.max(cyrillicLowercase, cyrillicUppercase) / 2;
            boolean orderingIsGood = cyrillicUppercase > cyrillicLowercase && strangeCaseTransitions > 3 * normalCaseTransitions;
            boolean longConstantSequencyIsNormal = longConsonantSequences > Math.max(cyrillicLowercase, cyrillicUppercase) / 10;
            return !(isSpecialValueBigger || orderingIsGood || longConstantSequencyIsNormal);
        }

        private static boolean isLatinAlphanumOrPunctuation(char c) {
            return LATIN_ALPHANUMS_AND_PUNCTUATION[c];
        }

        private static boolean isUppercase(char c) {
            return Character.isUpperCase(c);
        }

        private static boolean isCyrillic(char c) {
            return CYRILLIC_LETTERS[c];
        }

        private static boolean isConsonant(char c) {
            return CYRILLIC_CONSONANTS[c];
        }

    }


}
