package ru.yandex.webmaster3.core.semantic.schema_org_information_extractor;

import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;

import ru.yandex.common.util.IOUtils;
import ru.yandex.common.util.StringUtils;
import ru.yandex.common.util.collections.Cu;
import ru.yandex.common.util.collections.Pair;
import ru.yandex.common.util.functional.Filter;
import ru.yandex.webmaster3.core.util.json.JsonMapping;

/**
 * Created by IntelliJ IDEA.
 * User: rasifiel
 * Date: 25.07.12
 * Time: 16:48
 */
public class RDFsSchemaUtils {

    public static Map<String, SchemaClass> getSchemaClassMap(final String filename) {
        try {
            if (filename.endsWith("jsonld")) {
                return readAndMergeJsonLd(Collections.emptyMap(), getContentFromResource(filename));
            } else {
                return readAndMerge(Collections.emptyMap(), getContentFromResource(filename));
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    public static Map<String, SchemaClass> getOgpClassMap(final String filename, final String className, final String classLabel ) {
        try {
            return readAndMergeProps(Collections.emptyMap(), getContentFromResource(filename),className,classLabel);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    public static String getContentFromResource(final String name) throws IOException {
        ClassLoader classLoader = RDFsSchemaUtils.class.getClassLoader();
        InputStream file = classLoader.getResourceAsStream(name);
        return IOUtils.readInputStream(file);
    }

    public static Map<String, SchemaClass> getDublinClassMap(final String filename, final String className, final String classLabel ) {
        try {
            return readAndMerge(Collections.emptyMap(), getContentFromResource(filename), className, classLabel);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    public static HashMap<String, SchemaClass> readAndMergeJsonLd(final Map<String, SchemaClass> classMap, final String content) throws IOException {
        HashMap<String, SchemaClass> classes = new HashMap<>(classMap);
        ObjectNode root = JsonMapping.OM.readValue(content, ObjectNode.class);
        // читаем графф и собираем классы
        ArrayNode graph = (ArrayNode) root.get("@graph");
        // 1-ой проход - собираем все классы
        for (Iterator<JsonNode> graphIterator = graph.elements(); graphIterator.hasNext(); ) {
            ObjectNode node = (ObjectNode) graphIterator.next();
            if (node.get("@type").asText().equals("rdfs:Class")) {
                String id = node.get("@id").asText();
                for (String name : protocolAliases(id)) {
                    SchemaClass schemaClass = classes.computeIfAbsent(name, k -> new SchemaClass(name));
                    if (node.has("rdfs:label")) {
                        schemaClass.setLabel(node.get("rdfs:label").asText().trim());
                    } else {
                        int idx = name.lastIndexOf("/");
                        schemaClass.setLabel(name.substring(idx == -1 ? 0 : idx + 1));
                    }
                    classes.put(name, schemaClass);
                }
            }
        }
        // TODO зачем это?
        SchemaClass classWithAllFields = new SchemaClass("AllFieldsClass");
        // 2-ой проход - проставляем родителей для классов
        Map<String, List<SchemaClass>> childClasses = new HashMap<>();
        for (Iterator<JsonNode> graphIterator = graph.elements(); graphIterator.hasNext(); ) {
            ObjectNode node = (ObjectNode) graphIterator.next();
            if (node.get("@type").asText().equals("rdfs:Class")) {
                String id = node.get("@id").asText();
                for (String name : protocolAliases(id)) {
                    SchemaClass schemaClass = classes.get(name);
                    for (String subclassOf : getIds(node.get("rdfs:subClassOf"))) {
                        for (String subclassName : protocolAliases(subclassOf)) {
                            SchemaClass subclass = classes.get(subclassName);
                            if (subclass != null) {
                                schemaClass.addSubClassOf(classes.get(subclassName));
                                childClasses.computeIfAbsent(subclassName, k -> new ArrayList<>()).add(schemaClass);
                            }
                        }
                    }
                }
            }
        }
        // 3-ий проход, проставляем проперти, в том числе всем дочерним классам
        for (Iterator<JsonNode> graphIterator = graph.elements(); graphIterator.hasNext(); ) {
            ObjectNode node = (ObjectNode) graphIterator.next();
            if (node.get("@type").asText().equals("rdf:Property")) {
                String id = node.get("@id").asText();
                for (String name : protocolAliases(id)) {
                    SchemaField field = classWithAllFields.fields.getOrDefault(name, new SchemaField(name));
                    for (String domain : getIds(node.get("http://schema.org/domainIncludes"), node.get("rdfs:domain"))) {
                        for(String aliasDomain : protocolAliases(domain)) {
                            field.addDomain(classes.get(aliasDomain));
                            // размножаем все проперти для всех потомков (мб надо это делать при использовании, чтобы не жрать бесцельно память?)
                            addFieldToClassAndAllItsChildren(field, classes.get(aliasDomain), childClasses);
                        }
                    }
                    for (final String range : getIds(node.get("http://schema.org/rangeIncludes"), node.get("rdfs:range"))) {
                        for(String aliasRange : protocolAliases(range)) {
                            field.addRange(aliasRange);
                        }
                    }
                }
            }
        }
        classes.put(classWithAllFields.name, classWithAllFields);


        return classes;
    }

    private static List<String> getIds(JsonNode... nodes) {
        List<String> result = new ArrayList<>();
        for (JsonNode node : nodes) {
            if (node instanceof ArrayNode) {
                for (var iterator = node.iterator(); iterator.hasNext(); ) {
                    result.add(iterator.next().get("@id").asText());
                }
            } else if (node instanceof ObjectNode) {
                result.add(node.get("@id").asText());
            }
        }
        return result;
    }

    private static void addFieldToClassAndAllItsChildren(SchemaField field, SchemaClass schemaClass, Map<String, List<SchemaClass>> childClasses) {
        if (schemaClass == null) {
            return;
        }
        schemaClass.addField(field);
        for (SchemaClass childClass : childClasses.getOrDefault(schemaClass.name, Collections.emptyList())) {
            addFieldToClassAndAllItsChildren(field, childClass, childClasses);
        }
    }

    public static HashMap<String, SchemaClass> readAndMerge(final Map<String, SchemaClass> classHashMap, final String content) {
        Parser parser = new Parser(content);
        List<RDFsEntity> entities = parser.getEntities();
        Filter<RDFsEntity> isClass = new Filter<RDFsEntity>() {
            @Override
            public boolean fits(final RDFsEntity rdFsEntity) {
                return rdFsEntity.typeOf.endsWith("Class");
            }
        };
        Pair<List<RDFsEntity>, List<RDFsEntity>> all = Cu.split(entities, isClass);
        HashMap<String, SchemaClass> classes = new HashMap<String, SchemaClass>(classHashMap);
        for (final RDFsEntity entity : all.first) {
            for(String about : protocolAliases(entity.about)) {
                SchemaClass newClass = classes.get(about);
                if (newClass == null) {
                    newClass = new SchemaClass(about);
                }

                if (newClass.label == null) {
                    if (entity.getFirst("rdfs:label") == null) {

                        newClass.setLabel("defaultlabel");
                        if (newClass.label.equals("defaultlabel")) {
                            if (null != newClass.name) {
                                List<String> parts = new LinkedList<String>(Arrays.asList(newClass.name.split("/")));
                                newClass.setLabel(parts.get(parts.size() - 1));
                            }
                        }
                    } else {
                        String label = entity.getFirst("rdfs:label").trim();
                        newClass.setLabel(label);

                    }
                }

                if (entity.getValue("rdfs:subClassOf") != null) {
                    for (final String sclass : entity.getValue("rdfs:subClassOf")) {
                        for(String sclassAliases : protocolAliases(sclass)) {
                            SchemaClass sc = classes.get(sclassAliases);
                            if (sc == null) {
                                sc = new SchemaClass(sclassAliases);
                                classes.put(sclassAliases, sc);
                            }
                            newClass.addSubClassOf(sc);
                        }
                    }
                }
                if (newClass.name != null) {
                    classes.put(newClass.name, newClass);
                }
            }
        }
        Filter<RDFsEntity> propsFilter = new Filter<RDFsEntity>() {
            @Override
            public boolean fits(final RDFsEntity rdFsEntity) {
                return rdFsEntity.typeOf.endsWith("Property");
            }
        };
        SchemaClass classWithAllFields = new SchemaClass("AllFieldsClass");
        for (final RDFsEntity entity : Cu.filter(all.second, propsFilter)) {
            for(String about : protocolAliases(entity.about)) {

                SchemaField field = new SchemaField(about);



                if(classWithAllFields.fields.containsKey(about)){
                    field = classWithAllFields.fields.get(about);
                }
                for (final String domain : entity.getValue("http://schema.org/domainIncludes")) {
                    for(String aliasDomain : protocolAliases(domain)) {
                        field.addDomain(classes.get(aliasDomain));
                        classes.get(aliasDomain).addField(field);
                    }
                }
                for (final String domain : entity.getValue("rdfs:domain")) {
                    for(String aliasDomain : protocolAliases(domain)) {
                        try {
                            field.addDomain(classes.get(aliasDomain));
                            classes.get(aliasDomain).addField(field);
                        } catch (NullPointerException e) {
                        }
                    }
                }
                for (final String range : entity.getValue("http://schema.org/rangeIncludes")) {
                    for(String aliasRange : protocolAliases(range)) {
                        field.addRange(aliasRange);
                    }
                }
                for (final String range : entity.getValue("rdfs:range")) {
                    for(String aliasRange : protocolAliases(range)) {
                        field.addRange(aliasRange);
                    }
                }
                classWithAllFields.addField(field);
            }
        }
        classes.put(classWithAllFields.name,classWithAllFields);
        LinkedList<String> qu = new LinkedList<String>();
        HashMap<String, Integer> chc = new HashMap<String, Integer>();
        HashMap<String, List<String>> children = new HashMap<String, List<String>>();
        for (final String key : classes.keySet()) {
            SchemaClass val = classes.get(key);
            chc.put(key, val.subClassOf.size());
            if (val.subClassOf.size() == 0) {
                qu.add(key);
            }
            for (final SchemaClass parent : val.subClassOf) {
                List<String> childList = children.get(parent.name);
                if (childList == null) {
                    childList = new LinkedList<String>();
                    children.put(parent.name, childList);
                }
                childList.add(key);
            }
        }

        while (!qu.isEmpty()) {
            final String key = qu.poll();
            SchemaClass cls = classes.get(key);
            for (final SchemaClass sup : cls.subClassOf) {
                cls.fields.putAll(sup.fields);
            }
            if (children.containsKey(key)) {
                for (final String child : children.get(key)) {
                    Integer pcnt = chc.get(child);
                    pcnt--;
                    chc.put(child, pcnt);
                    if (pcnt == 0) {
                        qu.add(child);
                    }
                }
            }
        }
        return classes;
    }

    public static HashMap<String, SchemaClass> readAndMerge(final Map<String, SchemaClass> classHashMap, final String content, final String className, final String classLabel) {
        Parser parser = new Parser(content);
        List<RDFsEntity> entities = parser.getEntities();
        Filter<RDFsEntity> isClass = new Filter<RDFsEntity>() {
            @Override
            public boolean fits(final RDFsEntity rdFsEntity) {
                return rdFsEntity.typeOf.endsWith("Class");
            }
        };
        Pair<List<RDFsEntity>, List<RDFsEntity>> all = Cu.split(entities, isClass);
        HashMap<String, SchemaClass> classes = new HashMap<String, SchemaClass>(classHashMap);
        for(String classNameAlias : protocolAliases(className)) {
            SchemaClass commonClass = new SchemaClass(classNameAlias);
            commonClass.setLabel(classLabel);
            classes.put(commonClass.name, commonClass);
        }
        for (final RDFsEntity entity : all.first) {
            for(String about: protocolAliases(entity.about)) {
                SchemaClass newClass = classes.get(about);
                if (newClass == null) {
                    newClass = new SchemaClass(about);
                }
                if (newClass.label == null) {
                    if (entity.getFirst("rdfs:label") == null) {
                        newClass.setLabel("defaultlabel");
                        if (newClass.label.equals("defaultlabel")) {
                            if (null != newClass.name) {
                                List<String> parts = new LinkedList<String>(Arrays.asList(newClass.name.split("/")));
                                newClass.setLabel(parts.get(parts.size() - 1));
                            }
                        }
                    } else {
                        String label = entity.getFirst("rdfs:label").trim();
                        newClass.setLabel(label);

                    }
                }
                if (entity.getValue("rdfs:subClassOf") != null) {
                    for (final String sclass : entity.getValue("rdfs:subClassOf")) {
                        for(String sclassAliases : protocolAliases(sclass)) {
                            SchemaClass sc = classes.get(sclassAliases);
                            if (sc == null) {
                                sc = new SchemaClass(sclassAliases);
                                classes.put(sclassAliases, sc);
                            }
                            newClass.addSubClassOf(sc);
                        }
                    }
                }
                if (newClass.name != null) {
                    classes.put(newClass.name, newClass);
                }
            }
        }
        Filter<RDFsEntity> propsFilter = new Filter<RDFsEntity>() {
            @Override
            public boolean fits(final RDFsEntity rdFsEntity) {
                return rdFsEntity.typeOf.endsWith("Property");
            }
        };
        SchemaClass classWithAllFields = new SchemaClass("AllFieldsClass");
        for (final RDFsEntity entity : Cu.filter(all.second, propsFilter)) {
            SchemaField field = new SchemaField(entity.about);

            if(classWithAllFields.fields.containsKey(entity.about)){
                field = classWithAllFields.fields.get(entity.about);
            }

            for (final String domain : entity.getValue("http://schema.org/domainIncludes")) {
                for(String aliasDomain : protocolAliases(domain)) {
                    field.addDomain(classes.get(aliasDomain));
                    classes.get(aliasDomain).addField(field);
                }
            }
            for (final String domain : entity.getValue("rdfs:domain")) {
                for(String aliasDomain : protocolAliases(domain)) {
                    try {
                        field.addDomain(classes.get(aliasDomain));
                        classes.get(aliasDomain).addField(field);
                    } catch (NullPointerException e) {
                    }
                }
            }
            for (final String range : entity.getValue("http://schema.org/rangeIncludes")) {
                for(String aliasRange : protocolAliases(range)) {
                    field.addRange(aliasRange);
                }
            }
            for (final String range : entity.getValue("rdfs:range")) {
                for(String aliasRange : protocolAliases(range)) {
                    field.addRange(aliasRange);
                }
            }
            if( field.domains.isEmpty()){
                for(String classNameAlias : protocolAliases(className)) {
                    field.addDomain(classes.get(classNameAlias));
                    classes.get(classNameAlias).addField(field);
                }
            }
            classWithAllFields.addField(field);
        }
        classes.put(classWithAllFields.name,classWithAllFields);
        LinkedList<String> qu = new LinkedList<String>();
        HashMap<String, Integer> chc = new HashMap<String, Integer>();
        HashMap<String, List<String>> children = new HashMap<String, List<String>>();
        for (final String key : classes.keySet()) {
            SchemaClass val = classes.get(key);
            chc.put(key, val.subClassOf.size());
            if (val.subClassOf.size() == 0) {
                qu.add(key);
            }
            for (final SchemaClass parent : val.subClassOf) {
                List<String> childList = children.get(parent.name);
                if (childList == null) {
                    childList = new LinkedList<String>();
                    children.put(parent.name, childList);
                }
                childList.add(key);
            }
        }

        while (!qu.isEmpty()) {
            final String key = qu.poll();
            SchemaClass cls = classes.get(key);
            for (final SchemaClass sup : cls.subClassOf) {
                cls.fields.putAll(sup.fields);
            }
            if (children.containsKey(key)) {
                for (final String child : children.get(key)) {
                    Integer pcnt = chc.get(child);
                    pcnt--;
                    chc.put(child, pcnt);
                    if (pcnt == 0) {
                        qu.add(child);
                    }
                }
            }
        }
        return classes;
    }

    public static HashMap<String, SchemaClass> readAndMergeProps(final Map<String, SchemaClass> classHashMap, final String content, final String className, final String classLabel) {
        Parser parser = new Parser(content);
        List<RDFsEntity> entities = parser.getEntities();

        HashMap<String, SchemaClass> classes = new HashMap<String, SchemaClass>(classHashMap);

        SchemaClass commonClass = new SchemaClass(className);
        commonClass.setLabel(classLabel);
        classes.put(commonClass.name, commonClass);
        Filter<RDFsEntity> propsFilter = new Filter<RDFsEntity>() {
            @Override
            public boolean fits(final RDFsEntity rdFsEntity) {
                return rdFsEntity.typeOf.equals("rdf:Property");
            }
        };
        Pair<List<RDFsEntity>, List<RDFsEntity>> all = Cu.split(entities, propsFilter);
        for (final RDFsEntity entity : all.first) {
            for(String about: protocolAliases(entity.about)) {
                SchemaField field = new SchemaField(about);
                for (final String domain : entity.getValue("rdfs:isDefinedBy")) {
                    for(String domainAlias: protocolAliases(domain)) {
                        field.addDomain(classes.get(domain));
                        classes.get(domain).addField(field);
                    }
                }
                for (final String range : entity.getValue("rdfs:range")) {
                    for(String aliasRange : protocolAliases(range)) {
                        field.addRange(aliasRange);
                    }
                }
            }
        }

        LinkedList<String> qu = new LinkedList<String>();
        HashMap<String, Integer> chc = new HashMap<String, Integer>();
        HashMap<String, List<String>> children = new HashMap<String, List<String>>();
        for (final String key : classes.keySet()) {
            SchemaClass val = classes.get(key);
            chc.put(key, val.subClassOf.size());
            if (val.subClassOf.size() == 0) {
                qu.add(key);
            }
            for (final SchemaClass parent : val.subClassOf) {
                List<String> childList = children.get(parent.name);
                if (childList == null) {
                    childList = new LinkedList<String>();
                    children.put(parent.name, childList);
                }
                childList.add(key);
            }
        }

        while (!qu.isEmpty()) {
            final String key = qu.poll();
            SchemaClass cls = classes.get(key);
            for (final SchemaClass sup : cls.subClassOf) {
                cls.fields.putAll(sup.fields);
            }
            if (children.containsKey(key)) {
                for (final String child : children.get(key)) {
                    Integer pcnt = chc.get(child);
                    pcnt--;
                    chc.put(child, pcnt);
                    if (pcnt == 0) {
                        qu.add(child);
                    }
                }
            }
        }
        return classes;
    }

    /**
     * Размножает адрес типа http://schema.org/Thing в https/www-варианты
     * @param about
     * @return
     */
    private static Iterable<? extends String> protocolAliases(String about) {
        ArrayList<String> variants = new ArrayList<>();
        variants.add(about);
        if (null != about) {
            variants.add(StringUtils.replaceFirst(about, "http", "https"));
            if (about.contains("schema.org")) {
                variants.add(StringUtils.replaceFirst(about, "http://", "http://www."));
                variants.add(StringUtils.replaceFirst(about, "http://", "https://www."));
            }
        }
        return variants;
    }

}
